diff --git a/js/app/controllers/RepoController.js b/js/app/controllers/RepoController.js index b1b5ee2..8b37481 100644 --- a/js/app/controllers/RepoController.js +++ b/js/app/controllers/RepoController.js @@ -23,7 +23,7 @@ define([ var favorite = $.inArray(this.get('repo.id'), this.get('faves')) !== -1; utils.debug('RepoController::favorite:> favorite: ' + favorite); return favorite; - }.property('repo.id', 'faves').volatile(), + }.property('repo.id', 'faves'), toggleFavorite : function () { this.set('faves', ''); Favorites.toggle(this.get('repo.id')); diff --git a/js/app/ext/ExpandableRecordArray.js b/js/app/ext/ExpandableRecordArray.js index 516c59e..1c53353 100644 --- a/js/app/ext/ExpandableRecordArray.js +++ b/js/app/ext/ExpandableRecordArray.js @@ -14,7 +14,9 @@ define([ content = self.get('content'); array.removeObserver('isLoaded', observer); array.forEach(function (record) { - return self.pushObject(record); + if (!self.contains(record)) { + return self.pushObject(record); + } }); self.set('isLoading', false); return self.set('isLoaded', true); diff --git a/js/app/ext/Helpers.js b/js/app/ext/Helpers.js index 21c7eaf..4f0e840 100644 --- a/js/app/ext/Helpers.js +++ b/js/app/ext/Helpers.js @@ -70,6 +70,11 @@ define([ if (options.short) { message = message.split(/\n/)[0]; } +// message = this._emojize(this._escape(message)); +// if (!!options.repo) { +// message = this.githubify(message, options.repo.get('owner'), options.repo.get('name')); +// } +// return message.replace(/\n/g, '
'); return this._emojize(this._escape(message)).replace(/\n/g, '
'); }, pathFrom : function (url) { @@ -116,6 +121,35 @@ define([ } } }, +// githubify : function (text, owner, repo) { +// var self; +// self = this; +// text = text.replace(this._githubReferenceRegexp, function (reference, matchedOwner, matchedRepo, matchedNumber) { +// return self._githubReferenceLink(reference, { +// owner : owner, +// repo : repo +// }, { +// owner : matchedOwner, +// repo : matchedRepo, +// number : matchedNumber +// }); +// }); +// text = text.replace(this._githubUserRegexp, function (reference, username) { +// return self._githubUserLink(reference, username); +// }); +// return text; +// }, +// _githubReferenceLink : function (reference, current, matched) { +// var owner, repo; +// owner = matched.owner || current.owner; +// repo = matched.repo || current.repo; +// return "" + reference + ""; +// }, +// _githubReferenceRegexp : new RegExp("([\\w-]+)?\\/?([\\w-]+)?(?:#|gh-)(\\d+)", 'g'), +// _githubUserRegexp : new RegExp("@([\\w-]+)", 'g'), +// _githubUserLink : function (reference, username) { +// return "" + reference + ""; +// }, _normalizeDateString : function (string) { if (window.JHW) { string = string.replace('T', ' ').replace(/-/g, '/'); diff --git a/js/app/ext/ember/handlebars.js b/js/app/ext/ember/handlebars.js index 6d4c9ac..46d9836 100644 --- a/js/app/ext/ember/handlebars.js +++ b/js/app/ext/ember/handlebars.js @@ -52,8 +52,8 @@ define([ return safe(Helpers.pathFrom(url)); }); - Ember.registerBoundHelper('formatMessage', function (message, options) { - return safe(Helpers.formatMessage(message, options)); + Ember.Handlebars.helper('formatMessage', function(message, options) { + return safe(Helpers.formatMessage(message, options.hash)); }); Ember.registerBoundHelper('formatConfig', function (config) { diff --git a/js/app/models/Build.js b/js/app/models/Build.js index 0fb0d20..531cdfb 100644 --- a/js/app/models/Build.js +++ b/js/app/models/Build.js @@ -29,7 +29,7 @@ define([ return Helpers.compact(this.get('_config')); }.property('_config'), isPullRequest : function () { - return this.get('eventType') === 'pull_request'; + return this.get('eventType') === 'pull_request' || this.get('pullRequest'); }.property('eventType'), isMatrix : function () { return this.get('jobs.length') > 1; diff --git a/js/app/models/Job.js b/js/app/models/Job.js index e70dabf..351fb3c 100644 --- a/js/app/models/Job.js +++ b/js/app/models/Job.js @@ -23,33 +23,33 @@ define([ finishedAt : Ember.attr('string'), allowFailure : Ember.attr('boolean'), - repositorySlug : Ember.attr('string'), - repo : Ember.belongsTo('App.Repo', {key : 'repository_id'}), - build : Ember.belongsTo('App.Build'), - commit : Ember.belongsTo('App.Commit'), - _config : Ember.attr('object', {key : 'config'}), - log : function () { + repositorySlug : Ember.attr('string'), + repo : Ember.belongsTo('App.Repo', {key : 'repository_id'}), + build : Ember.belongsTo('App.Build'), + commit : Ember.belongsTo('App.Commit'), + _config : Ember.attr('object', {key : 'config'}), + log : function () { this.set('isLogAccessed', true); return Log.create({ job : this }); }.property(), - repoSlug : function () { + repoSlug : function () { return this.get('repositorySlug'); }.property('repositorySlug'), - config : function () { + config : function () { return Helpers.compact(this.get('_config')); }.property('_config'), - isFinished : function () { + isFinished : function () { var _ref; return (_ref = this.get('state')) === 'passed' || _ref === 'failed' || _ref === 'errored' || _ref === 'canceled'; }.property('state'), - clearLog : function () { + clearLog : function () { if (this.get('isLogAccessed')) { return this.get('log').clear(); } }, - configValues : function () { + configValues : function () { var buildConfig, config, keys; config = this.get('config'); buildConfig = this.get('build.config'); @@ -62,37 +62,37 @@ define([ return []; } }.property('config'), - canCancel : function () { + canCancel : function () { return this.get('state') === 'created' || this.get('state') === 'queued'; }.property('state'), - cancel : function () { + cancel : function () { return TravisAjax.post("/jobs/" + (this.get('id')), { _method : 'delete' }); }, - requeue : function () { + requeue : function () { return TravisAjax.post('/requests', { job_id : this.get('id') }); }, - appendLog : function (part) { + appendLog : function (part) { return this.get('log').append(part); }, - subscribe : function () { + subscribe : function () { if (this.get('subscribed')) { return; } this.set('subscribed', true); return App.pusher.subscribe("job-" + (this.get('id'))); }, - unsubscribe : function () { + unsubscribe : function () { if (!this.get('subscribed')) { return; } this.set('subscribed', false); return App.pusher.unsubscribe("job-" + (this.get('id'))); }, - onStateChange : function () { + onStateChange : function () { if (this.get('state') === 'finished' && App.pusher) { return App.pusher.unsubscribe("job-" + (this.get('id'))); } @@ -109,29 +109,37 @@ define([ }); Job.reopenClass({ - queued : function (queue) { - this.find(); - return Ember.FilteredRecordArray.create({ + queued : function () { + var filtered; + filtered = Ember.FilteredRecordArray.create({ modelClass : App.Job, filterFunction : function (job) { - var queued; - queued = ['created', 'queued'].indexOf(job.get('state')) !== -1; - return queued && (!queue || job.get('queue') === ("builds." + queue) || job.get('queue') === queue); + return ['created', 'queued'].indexOf(job.get('state')) !== -1; }, filterProperties : ['state', 'queue'] }); + this.fetch().then(function (/* array */) { + filtered.updateFilter(); + return filtered.set('isLoaded', true); + }); + return filtered; }, running : function () { - this.find({ - state : 'started' - }); - return Ember.FilteredRecordArray.create({ + var filtered; + filtered = Ember.FilteredRecordArray.create({ modelClass : App.Job, filterFunction : function (job) { return job.get('state') === 'started'; }, filterProperties : ['state'] }); + this.fetch({ + state : 'started' + }).then(function (/* array */) { + filtered.updateFilter(); + return filtered.set('isLoaded', true); + }); + return filtered; } }); diff --git a/js/app/models/Repo.js b/js/app/models/Repo.js index ed04e88..babb8ec 100644 --- a/js/app/models/Repo.js +++ b/js/app/models/Repo.js @@ -1,3 +1,4 @@ +/* global App */ define([ 'jquery', 'ember-model', @@ -19,6 +20,7 @@ define([ lastBuildState : Ember.attr('string'), lastBuildStartedAt : Ember.attr('string'), lastBuildFinishedAt : Ember.attr('string'), + githubLanguage : Ember.attr('string'), _lastBuildDuration : Ember.attr(Number, { key : 'last_build_duration' }), @@ -34,9 +36,14 @@ define([ }.property('lastBuildId', 'lastBuildNumber'), allBuilds : function () { utils.debug('Repo::allBuilds_:>'); - return Build.find(); + var recordArray; + recordArray = Ember.RecordArray.create({ + modelClass : App.Build, + content : Ember.A([]) + }); + Build.registerRecordArray(recordArray); + return recordArray; }.property(), - //TODO: this should not belong in the model builds : function () { utils.debug('Repo::builds_:>'); var array, builds, id; diff --git a/js/app/routes/BuildRoute.js b/js/app/routes/BuildRoute.js index 3c049d6..c21ae24 100644 --- a/js/app/routes/BuildRoute.js +++ b/js/app/routes/BuildRoute.js @@ -26,6 +26,9 @@ define([ repo.activate('build'); this.controllerFor('build').set('build', model); repo.set('build', model); + }, + model : function (params) { + return Build.find(params.build_id); } }); }); \ No newline at end of file diff --git a/js/app/routes/JobRoute.js b/js/app/routes/JobRoute.js index 065f98b..ce8cba3 100644 --- a/js/app/routes/JobRoute.js +++ b/js/app/routes/JobRoute.js @@ -33,6 +33,9 @@ define([ } }; return model.addObserver('build', this, buildObserver); + }, + model : function (params) { + return Job.find(params.job_id); } }); }); \ No newline at end of file diff --git a/js/app/routes/ReposRoute.js b/js/app/routes/ReposRoute.js index be9a209..dc48d8c 100644 --- a/js/app/routes/ReposRoute.js +++ b/js/app/routes/ReposRoute.js @@ -25,6 +25,7 @@ define([ } } else { if (navigator && navigator.splashscreen && navigator.splashscreen.hide) { + utils.debug('ReposRoute::afterModel:> calling splashscreen hide'); navigator.splashscreen.hide(); } } diff --git a/js/app/templates/jobs/running.hbs b/js/app/templates/jobs/running.hbs deleted file mode 100644 index 297e79c..0000000 --- a/js/app/templates/jobs/running.hbs +++ /dev/null @@ -1,9 +0,0 @@ -

Running Jobs ({{view.jobs.length}})

- - diff --git a/js/app/templates/jobs/show.hbs b/js/app/templates/jobs/show.hbs index 9bffa94..ece9c7c 100644 --- a/js/app/templates/jobs/show.hbs +++ b/js/app/templates/jobs/show.hbs @@ -56,7 +56,7 @@
Message
-
{{formatMessage job.commit.message}}
+
{{formatMessage job.commit.message repoBinding=job.repo}}
Config
diff --git a/js/app/templates/repo/build.hbs b/js/app/templates/repo/build.hbs index 0595868..e9711a6 100644 --- a/js/app/templates/repo/build.hbs +++ b/js/app/templates/repo/build.hbs @@ -58,7 +58,7 @@
Message
-
{{formatMessage build.commit.message}}
+
{{formatMessage build.commit.message repoBinding=build.repo}}
{{#unless isMatrix}} diff --git a/js/app/templates/repo/builds.hbs b/js/app/templates/repo/builds.hbs index ad44e94..9ed9e98 100644 --- a/js/app/templates/repo/builds.hbs +++ b/js/app/templates/repo/builds.hbs @@ -36,7 +36,7 @@ {{/if}}
-
{{{formatMessage commit.message short="true"}}}
+
{{{formatMessage commit.message short="true" repoBinding=build.repo}}}
{{/linkTo}} {{/if}} diff --git a/js/app/views/BuildViews.js b/js/app/views/BuildViews.js index 6632fb3..249c50c 100644 --- a/js/app/views/BuildViews.js +++ b/js/app/views/BuildViews.js @@ -8,7 +8,7 @@ define([ ], function ($, Ember, Helpers, TravisUrls) { var BuildViews = { - BuildView : Ember.View.extend({ + BuildView : Ember.View.extend({ templateName : 'repo/build', classNames : ['build'], classNameBindings : ['color'], @@ -16,7 +16,7 @@ define([ return Helpers.colorForState(this.get('controller.build.state')); }.property('controller.build.state') }), - BuildsView : Ember.View.extend({ + BuildsView : Ember.View.extend({ templateName : 'repo/builds', classNames : ['build'], buildsBinding : 'controller.builds', @@ -48,24 +48,24 @@ define([ }) }), BuildsItemView : Ember.View.extend({ - tagName : 'div', - classNameBindings : ['color'], - repoBinding : 'controller.repo', - buildBinding : 'context', - commitBinding : 'build.commit', - isPullRequestsList : function () { - return this.get('parentView.isPullRequestsList'); - }.property('parentView.isPullRequestsList'), - color : function () { - return Helpers.colorForState(this.get('build.state')); - }.property('build.state'), - urlGithubCommit : function () { - return TravisUrls.githubCommit(this.get('repo.slug'), this.get('commit.sha')); - }.property('repo.slug', 'commit.sha'), - urlGithubPullRequest : function () { - return TravisUrls.githubPullRequest(this.get('repo.slug'), this.get('commit.pullRequestNumber')); - }.property('repo.slug', 'commit.pullRequestNumber') - }) + tagName : 'div', + classNameBindings : ['color'], + repoBinding : 'controller.repo', + buildBinding : 'context', + commitBinding : 'build.commit', + isPullRequestsList : function () { + return this.get('parentView.isPullRequestsList'); + }.property('parentView.isPullRequestsList'), + color : function () { + return Helpers.colorForState(this.get('build.state')); + }.property('build.state'), + urlGithubCommit : function () { + return TravisUrls.githubCommit(this.get('repo.slug'), this.get('commit.sha')); + }.property('repo.slug', 'commit.sha'), + urlGithubPullRequest : function () { + return TravisUrls.githubPullRequest(this.get('repo.slug'), this.get('build.pullRequestNumber')); + }.property('repo.slug', 'build.pullRequestNumber') + }) }; return BuildViews; diff --git a/js/lib/ember-model.js b/js/lib/ember-model.js index 4bc8830..a5eea67 100644 --- a/js/lib/ember-model.js +++ b/js/lib/ember-model.js @@ -186,6 +186,8 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ get(this, 'modelClass').forEachCachedRecord(function(record) { if (self.filterFunction(record)) { results.push(record); + } else { + results.removeObject(record); } }); this.set('content', Ember.A(results)); @@ -193,8 +195,12 @@ Ember.FilteredRecordArray = Ember.RecordArray.extend({ updateFilterForRecord: function(record) { var results = get(this, 'content'); - if (this.filterFunction(record) && !results.contains(record)) { - results.pushObject(record); + if (this.filterFunction(record)) { + if(!results.contains(record)) { + results.pushObject(record); + } + } else { + results.removeObject(record); } }, @@ -432,6 +438,10 @@ Ember.Model = Ember.Object.extend(Ember.Evented, { init: function() { this._createReference(); this._super(); + + this.one('didLoad', function() { + this.constructor.addToRecordArrays(this); + }); }, _createReference: function() { diff --git a/js/lib/ember.js b/js/lib/ember.js index 9bcdb78..33248fc 100644 --- a/js/lib/ember.js +++ b/js/lib/ember.js @@ -1,5 +1,5 @@ -// Version: v1.0.0-rc.6.1 -// Last commit: 2a9ce26 (2013-07-25 18:13:33 -0400) +// Version: v1.0.0-rc.7-59-g4048275 +// Last commit: 4048275 (2013-08-25 00:57:38 -0400) (function() { @@ -49,7 +49,9 @@ if (!('MANDATORY_SETTER' in Ember.ENV)) { falsy, an exception will be thrown. */ Ember.assert = function(desc, test) { - Ember.Logger.assert(test, desc); + if (!test) { + Ember.Logger.assert(test, desc); + } if (Ember.testing && !test) { // when testing, ensure test failures when assertions fail @@ -156,8 +158,8 @@ Ember.deprecateFunc = function(message, func) { })(); -// Version: v1.0.0-rc.6.1 -// Last commit: 2a9ce26 (2013-07-25 18:13:33 -0400) +// Version: v1.0.0-rc.7-59-g4048275 +// Last commit: 4048275 (2013-08-25 00:57:38 -0400) (function() { @@ -185,7 +187,6 @@ var define, requireModule; deps = mod.deps; callback = mod.callback; reified = []; - exports; for (var i=0, l=deps.length; i= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - flags = actions[i][2], + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], actionIndex = indexOf(otherActions, target, method); if (actionIndex === -1) { - otherActions.push([target, method, flags]); + otherActions.push(target, method, flags); } } } @@ -1855,16 +1878,16 @@ function actionsDiff(obj, eventName, otherActions) { diffActions = []; if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - var target = actions[i][0], - method = actions[i][1], - flags = actions[i][2], + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i], + method = actions[i+1], + flags = actions[i+2], actionIndex = indexOf(otherActions, target, method); if (actionIndex !== -1) { continue; } - otherActions.push([target, method, flags]); - diffActions.push([target, method, flags]); + otherActions.push(target, method, flags); + diffActions.push(target, method, flags); } return diffActions; @@ -1897,7 +1920,7 @@ function addListener(obj, eventName, target, method, once) { if (actionIndex !== -1) { return; } - actions.push([target, method, flags]); + actions.push(target, method, flags); if ('function' === typeof obj.didAddListener) { obj.didAddListener(eventName, target, method); @@ -1931,7 +1954,7 @@ function removeListener(obj, eventName, target, method) { // action doesn't exist, give up silently if (actionIndex === -1) { return; } - actions.splice(actionIndex, 1); + actions.splice(actionIndex, 3); if ('function' === typeof obj.didRemoveListener) { obj.didRemoveListener(eventName, target, method); @@ -1945,8 +1968,8 @@ function removeListener(obj, eventName, target, method) { actions = meta && meta.listeners && meta.listeners[eventName]; if (!actions) { return; } - for (var i = actions.length - 1; i >= 0; i--) { - _removeListener(actions[i][0], actions[i][1]); + for (var i = actions.length - 3; i >= 0; i -= 3) { + _removeListener(actions[i], actions[i+1]); } } } @@ -1976,17 +1999,14 @@ function suspendListener(obj, eventName, target, method, callback) { } var actions = actionsFor(obj, eventName), - actionIndex = indexOf(actions, target, method), - action; + actionIndex = indexOf(actions, target, method); if (actionIndex !== -1) { - action = actions[actionIndex].slice(); // copy it, otherwise we're modifying a shared object - action[2] |= SUSPENDED; // mark the action as suspended - actions[actionIndex] = action; // replace the shared object with our copy + actions[actionIndex+2] |= SUSPENDED; // mark the action as suspended } function tryable() { return callback.call(target); } - function finalizer() { if (action) { action[2] &= ~SUSPENDED; } } + function finalizer() { if (actionIndex !== -1) { actions[actionIndex+2] &= ~SUSPENDED; } } return Ember.tryFinally(tryable, finalizer); } @@ -1994,14 +2014,10 @@ function suspendListener(obj, eventName, target, method, callback) { /** @private - Suspend listener during callback. + Suspends multiple listeners during a callback. - This should only be used by the target of the event listener - when it is taking an action that would cause the event, e.g. - an object might suspend its property change listener while it is - setting that property. - @method suspendListener + @method suspendListeners @for Ember @param obj @param {Array} eventName Array of event names @@ -2016,7 +2032,7 @@ function suspendListeners(obj, eventNames, target, method, callback) { } var suspendedActions = [], - eventName, actions, action, i, l; + eventName, actions, i, l; for (i=0, l=eventNames.length; i= 0; i--) { // looping in reverse for once listeners - var action = actions[i]; - if (!action) { continue; } - var target = action[0], method = action[1], flags = action[2]; + for (var i = actions.length - 3; i >= 0; i -= 3) { // looping in reverse for once listeners + var target = actions[i], method = actions[i+1], flags = actions[i+2]; + if (!method) { continue; } if (flags & SUSPENDED) { continue; } if (flags & ONCE) { removeListener(obj, eventName, target, method); } if (!target) { target = obj; } @@ -2129,15 +2148,40 @@ function listenersFor(obj, eventName) { if (!actions) { return ret; } - for (var i = 0, l = actions.length; i < l; i++) { - var target = actions[i][0], - method = actions[i][1]; + for (var i = 0, l = actions.length; i < l; i += 3) { + var target = actions[i], + method = actions[i+1]; ret.push([target, method]); } return ret; } +/** + Define a property as a function that should be executed when + a specified event or events are triggered. + + var Job = Ember.Object.extend({ + logCompleted: Ember.on('completed', function(){ + console.log('Job completed!'); + }) + }); + var job = Job.create(); + Ember.sendEvent(job, 'completed'); // Logs "Job completed!" + + @method on + @for Ember + @param {String} eventNames* + @param {Function} func + @return func +*/ +Ember.on = function(){ + var func = a_slice.call(arguments, -1)[0], + events = a_slice.call(arguments, 0, -1); + func.__ember_listens__ = events; + return func; +}; + Ember.addListener = addListener; Ember.removeListener = removeListener; Ember._suspendListener = suspendListener; @@ -2345,6 +2389,8 @@ var chainsWillChange = function(obj, keyName, m, arg) { nodes = nodes[keyName]; if (!nodes) { return; } + nodes = nodes.slice(); + for(var i = 0, l = nodes.length; i < l; i++) { nodes[i].willChange(arg); } @@ -2358,8 +2404,9 @@ var chainsDidChange = function(obj, keyName, m, arg) { nodes = nodes[keyName]; if (!nodes) { return; } - // looping in reverse because the chainWatchers array can be modified inside didChange - for (var i = nodes.length - 1; i >= 0; i--) { + nodes = nodes.slice(); + + for(var i = 0, l = nodes.length; i < l; i++) { nodes[i].didChange(arg); } }; @@ -2402,7 +2449,7 @@ var endPropertyChanges = Ember.endPropertyChanges = function() { @param {Function} callback @param [binding] */ -Ember.changeProperties = function(cb, binding){ +Ember.changeProperties = function(cb, binding) { beginPropertyChanges(); tryFinally(cb, endPropertyChanges, binding); }; @@ -2431,6 +2478,7 @@ var notifyObservers = function(obj, keyName) { sendEvent(obj, eventName, [obj, keyName]); } }; + })(); @@ -2607,8 +2655,7 @@ Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Map is mocked out to look like an Ember object, so you can do `Ember.Map.create()` for symmetry with other Ember classes. */ -var get = Ember.get, - set = Ember.set, +var set = Ember.set, guidFor = Ember.guidFor, indexOf = Ember.ArrayPolyfills.indexOf; @@ -2790,14 +2837,14 @@ Map.create = function() { Map.prototype = { /** This property will change as the number of objects in the map changes. - + @property length @type number @default 0 */ length: 0, - - + + /** Retrieve the value associated with a given key. @@ -3078,7 +3125,6 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { } else { obj[keyName] = undefined; // make enumerable } - desc.setup(obj, keyName); } else { descs[keyName] = undefined; // shadow descriptor in proto if (desc == null) { @@ -3134,13 +3180,14 @@ var changeProperties = Ember.changeProperties, @return self */ Ember.setProperties = function(self, hash) { - changeProperties(function(){ + changeProperties(function() { for(var prop in hash) { if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } } }); return self; }; + })(); @@ -3155,13 +3202,11 @@ Ember.watchKey = function(obj, keyName) { // can't watch length on Array - it is special... if (keyName === 'length' && typeOf(obj) === 'array') { return; } - var m = metaFor(obj), watching = m.watching, desc; + var m = metaFor(obj), watching = m.watching; // activate watching first time if (!watching[keyName]) { watching[keyName] = 1; - desc = m.descs[keyName]; - if (desc && desc.willWatch) { desc.willWatch(obj, keyName); } if ('function' === typeof obj.willWatchProperty) { obj.willWatchProperty(keyName); @@ -3183,13 +3228,10 @@ Ember.watchKey = function(obj, keyName) { Ember.unwatchKey = function(obj, keyName) { - var m = metaFor(obj), watching = m.watching, desc; + var m = metaFor(obj), watching = m.watching; if (watching[keyName] === 1) { watching[keyName] = 0; - desc = m.descs[keyName]; - - if (desc && desc.didUnwatch) { desc.didUnwatch(obj, keyName); } if ('function' === typeof obj.didUnwatchProperty) { obj.didUnwatchProperty(keyName); @@ -3208,6 +3250,7 @@ Ember.unwatchKey = function(obj, keyName) { watching[keyName]--; } }; + })(); @@ -3276,10 +3319,6 @@ var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) unwatchKey(obj, keyName); }; -function isProto(pvalue) { - return metaFor(pvalue, false).proto === pvalue; -} - // A ChainNode watches a single key on an object. If you provide a starting // value for the key then the node won't actually watch it. For a root node // pass null for parent and key and object for value. @@ -3314,10 +3353,32 @@ var ChainNode = Ember._ChainNode = function(parent, key, value) { var ChainNodePrototype = ChainNode.prototype; +function lazyGet(obj, key) { + if (!obj) return undefined; + + var meta = metaFor(obj, false); + // check if object meant only to be a prototype + if (meta.proto === obj) return undefined; + + if (key === "@each") return get(obj, key); + + // if a CP only return cached value + var desc = meta.descs[key]; + if (desc && desc._cacheable) { + if (key in meta.cache) { + return meta.cache[key]; + } else { + return undefined; + } + } + + return get(obj, key); +} + ChainNodePrototype.value = function() { if (this._value === undefined && this._watching) { var obj = this._parent.value(); - this._value = (obj && !isProto(obj)) ? get(obj, this._key) : undefined; + this._value = lazyGet(obj, this._key); } return this._value; }; @@ -3937,25 +3998,6 @@ ComputedPropertyPrototype.meta = function(meta) { } }; -/* impl descriptor API */ -ComputedPropertyPrototype.willWatch = function(obj, keyName) { - // watch already creates meta for this instance - var meta = obj[META_KEY]; - Ember.assert('watch should have setup meta to be writable', meta.source === obj); - if (!(keyName in meta.cache)) { - addDependentKeys(this, obj, keyName, meta); - } -}; - -ComputedPropertyPrototype.didUnwatch = function(obj, keyName) { - var meta = obj[META_KEY]; - Ember.assert('unwatch should have setup meta to be writable', meta.source === obj); - if (!(keyName in meta.cache)) { - // unwatch already creates meta for this instance - removeDependentKeys(this, obj, keyName, meta); - } -}; - /* impl descriptor API */ ComputedPropertyPrototype.didChange = function(obj, keyName) { // _suspended is set via a CP.set to ensure we don't clear @@ -3964,24 +4006,29 @@ ComputedPropertyPrototype.didChange = function(obj, keyName) { var meta = metaFor(obj); if (keyName in meta.cache) { delete meta.cache[keyName]; - if (!meta.watching[keyName]) { - removeDependentKeys(this, obj, keyName, meta); - } + removeDependentKeys(this, obj, keyName, meta); } } }; +function finishChains(chainNodes) +{ + for (var i=0, l=chainNodes.length; i 1) { set(this, dependentKey, value); return value; @@ -4381,7 +4438,7 @@ Ember.computed.alias = function(dependentKey) { @return {Ember.ComputedProperty} computed property which creates an one way computed property to the original value for property. - Where `computed.alias` aliases `get` and `set`, and allows for bidirectional + Where `computed.alias` aliases `get` and `set`, and allows for bidirectional data flow, `computed.oneWay` only provides an aliased `get`. The `set` will not mutate the upstream property, rather causes the current property to become the value set. This causes the downstream property to permentantly @@ -4541,119 +4598,335 @@ Ember.removeBeforeObserver = function(obj, path, target, method) { (function() { -define("backburner", - ["backburner/deferred_action_queues","exports"], - function(__dependency1__, __exports__) { +define("backburner/queue", + ["exports"], + function(__exports__) { "use strict"; - var DeferredActionQueues = __dependency1__.DeferredActionQueues; - - var slice = [].slice, - pop = [].pop, - debouncees = [], - timers = [], - autorun, laterTimer, laterTimerExpiresAt; - - function Backburner(queueNames, options) { - this.queueNames = queueNames; - this.options = options || {}; - if (!this.options.defaultQueue) { - this.options.defaultQueue = queueNames[0]; - } - this.instanceStack = []; + function Queue(daq, name, options) { + this.daq = daq; + this.name = name; + this.options = options; + this._queue = []; } - Backburner.prototype = { - queueNames: null, + Queue.prototype = { + daq: null, + name: null, options: null, - currentInstance: null, - instanceStack: null, + _queue: null, - begin: function() { - var onBegin = this.options && this.options.onBegin, - previousInstance = this.currentInstance; + push: function(target, method, args, stack) { + var queue = this._queue; + queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; + }, - if (previousInstance) { - this.instanceStack.push(previousInstance); - } + pushUnique: function(target, method, args, stack) { + var queue = this._queue, currentTarget, currentMethod, i, l; - this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); - if (onBegin) { - onBegin(this.currentInstance, previousInstance); + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === target && currentMethod === method) { + queue[i+2] = args; // replace args + queue[i+3] = stack; // replace stack + return {queue: this, target: target, method: method}; // TODO: test this code path + } } + + this._queue.push(target, method, args, stack); + return {queue: this, target: target, method: method}; }, - end: function() { - var onEnd = this.options && this.options.onEnd, - currentInstance = this.currentInstance, - nextInstance = null; + // TODO: remove me, only being used for Ember.run.sync + flush: function() { + var queue = this._queue, + options = this.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, i, l = queue.length; - try { - currentInstance.flush(); - } finally { - this.currentInstance = null; + if (l && before) { before(); } + for (i = 0; i < l; i += 4) { + target = queue[i]; + method = queue[i+1]; + args = queue[i+2]; + stack = queue[i+3]; // Debugging assistance - if (this.instanceStack.length) { - nextInstance = this.instanceStack.pop(); - this.currentInstance = nextInstance; + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); } + } + if (l && after) { after(); } - if (onEnd) { - onEnd(currentInstance, nextInstance); - } + // check if new items have been added + if (queue.length > l) { + this._queue = queue.slice(l); + this.flush(); + } else { + this._queue.length = 0; } }, - run: function(target, method /*, args */) { - var ret; - this.begin(); - - if (!method) { - method = target; - target = null; - } + cancel: function(actionToCancel) { + var queue = this._queue, currentTarget, currentMethod, i, l; - if (typeof method === 'string') { - method = target[method]; - } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - // Prevent Safari double-finally. - var finallyAlreadyCalled = false; - try { - if (arguments.length > 2) { - ret = method.apply(target, slice.call(arguments, 2)); - } else { - ret = method.call(target); - } - } finally { - if (!finallyAlreadyCalled) { - finallyAlreadyCalled = true; - this.end(); + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + queue.splice(i, 4); + return true; } } - return ret; - }, - defer: function(queueName, target, method /* , args */) { - if (!method) { - method = target; - target = null; + // if not found in current queue + // could be in the queue that is being flushed + queue = this._queueBeingFlushed; + if (!queue) { + return; } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; - if (typeof method === 'string') { - method = target[method]; + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + // don't mess with array during flush + // just nullify the method + queue[i+1] = null; + return true; + } } + } + }; - var stack = this.DEBUG ? new Error().stack : undefined, - args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; - if (!this.currentInstance) { createAutorun(this); } - return this.currentInstance.schedule(queueName, target, method, args, false, stack); - }, - deferOnce: function(queueName, target, method /* , args */) { - if (!method) { - method = target; - target = null; - } + __exports__.Queue = Queue; + }); + +define("backburner/deferred_action_queues", + ["backburner/queue","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var Queue = __dependency1__.Queue; + + function DeferredActionQueues(queueNames, options) { + var queues = this.queues = {}; + this.queueNames = queueNames = queueNames || []; + + var queueName; + for (var i = 0, l = queueNames.length; i < l; i++) { + queueName = queueNames[i]; + queues[queueName] = new Queue(this, queueName, options[queueName]); + } + } + + DeferredActionQueues.prototype = { + queueNames: null, + queues: null, + + schedule: function(queueName, target, method, args, onceFlag, stack) { + var queues = this.queues, + queue = queues[queueName]; + + if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } + + if (onceFlag) { + return queue.pushUnique(target, method, args, stack); + } else { + return queue.push(target, method, args, stack); + } + }, + + flush: function() { + var queues = this.queues, + queueNames = this.queueNames, + queueName, queue, queueItems, priorQueueNameIndex, + queueNameIndex = 0, numberOfQueues = queueNames.length; + + outerloop: + while (queueNameIndex < numberOfQueues) { + queueName = queueNames[queueNameIndex]; + queue = queues[queueName]; + queueItems = queue._queueBeingFlushed = queue._queue.slice(); + queue._queue = []; + + var options = queue.options, + before = options && options.before, + after = options && options.after, + target, method, args, stack, + queueIndex = 0, numberOfQueueItems = queueItems.length; + + if (numberOfQueueItems && before) { before(); } + while (queueIndex < numberOfQueueItems) { + target = queueItems[queueIndex]; + method = queueItems[queueIndex+1]; + args = queueItems[queueIndex+2]; + stack = queueItems[queueIndex+3]; // Debugging assistance + + if (typeof method === 'string') { method = target[method]; } + + // method could have been nullified / canceled during flush + if (method) { + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } + } + + queueIndex += 4; + } + queue._queueBeingFlushed = null; + if (numberOfQueueItems && after) { after(); } + + if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { + queueNameIndex = priorQueueNameIndex; + continue outerloop; + } + + queueNameIndex++; + } + } + }; + + function indexOfPriorQueueWithActions(daq, currentQueueIndex) { + var queueName, queue; + + for (var i = 0, l = currentQueueIndex; i <= l; i++) { + queueName = daq.queueNames[i]; + queue = daq.queues[queueName]; + if (queue._queue.length) { return i; } + } + + return -1; + } + + + __exports__.DeferredActionQueues = DeferredActionQueues; + }); + +define("backburner", + ["backburner/deferred_action_queues","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var DeferredActionQueues = __dependency1__.DeferredActionQueues; + + var slice = [].slice, + pop = [].pop, + throttlers = [], + debouncees = [], + timers = [], + autorun, laterTimer, laterTimerExpiresAt, + global = this; + + function Backburner(queueNames, options) { + this.queueNames = queueNames; + this.options = options || {}; + if (!this.options.defaultQueue) { + this.options.defaultQueue = queueNames[0]; + } + this.instanceStack = []; + } + + Backburner.prototype = { + queueNames: null, + options: null, + currentInstance: null, + instanceStack: null, + + begin: function() { + var onBegin = this.options && this.options.onBegin, + previousInstance = this.currentInstance; + + if (previousInstance) { + this.instanceStack.push(previousInstance); + } + + this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); + if (onBegin) { + onBegin(this.currentInstance, previousInstance); + } + }, + + end: function() { + var onEnd = this.options && this.options.onEnd, + currentInstance = this.currentInstance, + nextInstance = null; + + try { + currentInstance.flush(); + } finally { + this.currentInstance = null; + + if (this.instanceStack.length) { + nextInstance = this.instanceStack.pop(); + this.currentInstance = nextInstance; + } + + if (onEnd) { + onEnd(currentInstance, nextInstance); + } + } + }, + + run: function(target, method /*, args */) { + var ret; + this.begin(); + + if (!method) { + method = target; + target = null; + } + + if (typeof method === 'string') { + method = target[method]; + } + + // Prevent Safari double-finally. + var finallyAlreadyCalled = false; + try { + if (arguments.length > 2) { + ret = method.apply(target, slice.call(arguments, 2)); + } else { + ret = method.call(target); + } + } finally { + if (!finallyAlreadyCalled) { + finallyAlreadyCalled = true; + this.end(); + } + } + return ret; + }, + + defer: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } + + if (typeof method === 'string') { + method = target[method]; + } + + var stack = this.DEBUG ? new Error().stack : undefined, + args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; + if (!this.currentInstance) { createAutorun(this); } + return this.currentInstance.schedule(queueName, target, method, args, false, stack); + }, + + deferOnce: function(queueName, target, method /* , args */) { + if (!method) { + method = target; + target = null; + } if (typeof method === 'string') { method = target[method]; @@ -4708,7 +4981,7 @@ define("backburner", clearTimeout(laterTimer); laterTimer = null; } - laterTimer = window.setTimeout(function() { + laterTimer = global.setTimeout(function() { executeTimers(self); laterTimer = null; laterTimerExpiresAt = null; @@ -4718,38 +4991,86 @@ define("backburner", return fn; }, - debounce: function(target, method /* , args, wait */) { + throttle: function(target, method /* , args, wait */) { var self = this, args = arguments, wait = pop.call(args), - debouncee; + throttler; - for (var i = 0, l = debouncees.length; i < l; i++) { - debouncee = debouncees[i]; - if (debouncee[0] === target && debouncee[1] === method) { return; } // do nothing + for (var i = 0, l = throttlers.length; i < l; i++) { + throttler = throttlers[i]; + if (throttler[0] === target && throttler[1] === method) { return; } // do nothing } - var timer = window.setTimeout(function() { + var timer = global.setTimeout(function() { self.run.apply(self, args); - // remove debouncee + // remove throttler var index = -1; - for (var i = 0, l = debouncees.length; i < l; i++) { - debouncee = debouncees[i]; - if (debouncee[0] === target && debouncee[1] === method) { + for (var i = 0, l = throttlers.length; i < l; i++) { + throttler = throttlers[i]; + if (throttler[0] === target && throttler[1] === method) { index = i; break; } } - if (index > -1) { debouncees.splice(index, 1); } + if (index > -1) { throttlers.splice(index, 1); } + }, wait); + + throttlers.push([target, method, timer]); + }, + + debounce: function(target, method /* , args, wait, [immediate] */) { + var self = this, + args = arguments, + immediate = pop.call(args), + wait, + index, + debouncee; + + if (typeof immediate === "number") { + wait = immediate; + immediate = false; + } else { + wait = pop.call(args); + } + + // Remove debouncee + index = findDebouncee(target, method); + + if (index !== -1) { + debouncee = debouncees[index]; + debouncees.splice(index, 1); + clearTimeout(debouncee[2]); + } + + var timer = window.setTimeout(function() { + if (!immediate) { + self.run.apply(self, args); + } + index = findDebouncee(target, method); + if (index) { + debouncees.splice(index, 1); + } }, wait); + if (immediate && index === -1) { + self.run.apply(self, args); + } + debouncees.push([target, method, timer]); }, cancelTimers: function() { - for (var i = 0, l = debouncees.length; i < l; i++) { + var i, len; + + for (i = 0, len = throttlers.length; i < len; i++) { + clearTimeout(throttlers[i][2]); + } + throttlers = []; + + for (i = 0, len = debouncees.length; i < len; i++) { clearTimeout(debouncees[i][2]); } debouncees = []; @@ -4792,7 +5113,7 @@ define("backburner", function createAutorun(backburner) { backburner.begin(); - autorun = window.setTimeout(function() { + autorun = global.setTimeout(function() { backburner.end(); autorun = null; }); @@ -4817,7 +5138,7 @@ define("backburner", }); if (timers.length) { - laterTimer = window.setTimeout(function() { + laterTimer = global.setTimeout(function() { executeTimers(self); laterTimer = null; laterTimerExpiresAt = null; @@ -4826,224 +5147,25 @@ define("backburner", } } + function findDebouncee(target, method) { + var debouncee, + index = -1; + + for (var i = 0, l = debouncees.length; i < l; i++) { + debouncee = debouncees[i]; + if (debouncee[0] === target && debouncee[1] === method) { + index = i; + break; + } + } + + return index; + } + __exports__.Backburner = Backburner; }); - -define("backburner/deferred_action_queues", - ["backburner/queue","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var Queue = __dependency1__.Queue; - - function DeferredActionQueues(queueNames, options) { - var queues = this.queues = {}; - this.queueNames = queueNames = queueNames || []; - - var queueName; - for (var i = 0, l = queueNames.length; i < l; i++) { - queueName = queueNames[i]; - queues[queueName] = new Queue(this, queueName, options[queueName]); - } - } - - DeferredActionQueues.prototype = { - queueNames: null, - queues: null, - - schedule: function(queueName, target, method, args, onceFlag, stack) { - var queues = this.queues, - queue = queues[queueName]; - - if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } - - if (onceFlag) { - return queue.pushUnique(target, method, args, stack); - } else { - return queue.push(target, method, args, stack); - } - }, - - flush: function() { - var queues = this.queues, - queueNames = this.queueNames, - queueName, queue, queueItems, priorQueueNameIndex, - queueNameIndex = 0, numberOfQueues = queueNames.length; - - outerloop: - while (queueNameIndex < numberOfQueues) { - queueName = queueNames[queueNameIndex]; - queue = queues[queueName]; - queueItems = queue._queueBeingFlushed = queue._queue.slice(); - queue._queue = []; - - var options = queue.options, - before = options && options.before, - after = options && options.after, - target, method, args, stack, - queueIndex = 0, numberOfQueueItems = queueItems.length; - - if (numberOfQueueItems && before) { before(); } - while (queueIndex < numberOfQueueItems) { - target = queueItems[queueIndex]; - method = queueItems[queueIndex+1]; - args = queueItems[queueIndex+2]; - stack = queueItems[queueIndex+3]; // Debugging assistance - - if (typeof method === 'string') { method = target[method]; } - - // method could have been nullified / canceled during flush - if (method) { - // TODO: error handling - if (args && args.length > 0) { - method.apply(target, args); - } else { - method.call(target); - } - } - - queueIndex += 4; - } - queue._queueBeingFlushed = null; - if (numberOfQueueItems && after) { after(); } - - if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { - queueNameIndex = priorQueueNameIndex; - continue outerloop; - } - - queueNameIndex++; - } - } - }; - - function indexOfPriorQueueWithActions(daq, currentQueueIndex) { - var queueName, queue; - - for (var i = 0, l = currentQueueIndex; i <= l; i++) { - queueName = daq.queueNames[i]; - queue = daq.queues[queueName]; - if (queue._queue.length) { return i; } - } - - return -1; - } - - - __exports__.DeferredActionQueues = DeferredActionQueues; - }); - -define("backburner/queue", - ["exports"], - function(__exports__) { - "use strict"; - function Queue(daq, name, options) { - this.daq = daq; - this.name = name; - this.options = options; - this._queue = []; - } - - Queue.prototype = { - daq: null, - name: null, - options: null, - _queue: null, - - push: function(target, method, args, stack) { - var queue = this._queue; - queue.push(target, method, args, stack); - return {queue: this, target: target, method: method}; - }, - - pushUnique: function(target, method, args, stack) { - var queue = this._queue, currentTarget, currentMethod, i, l; - - for (i = 0, l = queue.length; i < l; i += 4) { - currentTarget = queue[i]; - currentMethod = queue[i+1]; - - if (currentTarget === target && currentMethod === method) { - queue[i+2] = args; // replace args - queue[i+3] = stack; // replace stack - return {queue: this, target: target, method: method}; // TODO: test this code path - } - } - - this._queue.push(target, method, args, stack); - return {queue: this, target: target, method: method}; - }, - - // TODO: remove me, only being used for Ember.run.sync - flush: function() { - var queue = this._queue, - options = this.options, - before = options && options.before, - after = options && options.after, - target, method, args, stack, i, l = queue.length; - - if (l && before) { before(); } - for (i = 0; i < l; i += 4) { - target = queue[i]; - method = queue[i+1]; - args = queue[i+2]; - stack = queue[i+3]; // Debugging assistance - - // TODO: error handling - if (args && args.length > 0) { - method.apply(target, args); - } else { - method.call(target); - } - } - if (l && after) { after(); } - - // check if new items have been added - if (queue.length > l) { - this._queue = queue.slice(l); - this.flush(); - } else { - this._queue.length = 0; - } - }, - - cancel: function(actionToCancel) { - var queue = this._queue, currentTarget, currentMethod, i, l; - - for (i = 0, l = queue.length; i < l; i += 4) { - currentTarget = queue[i]; - currentMethod = queue[i+1]; - - if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { - queue.splice(i, 4); - return true; - } - } - - // if not found in current queue - // could be in the queue that is being flushed - queue = this._queueBeingFlushed; - if (!queue) { - return; - } - for (i = 0, l = queue.length; i < l; i += 4) { - currentTarget = queue[i]; - currentMethod = queue[i+1]; - - if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { - // don't mess with array during flush - // just nullify the method - queue[i+1] = null; - return true; - } - } - } - }; - - - __exports__.Queue = Queue; - }); -})(); +})(); @@ -5083,7 +5205,7 @@ var Backburner = requireModule('backburner').Backburner, call. ```javascript - Ember.run(function(){ + Ember.run(function() { // code to be execute within a RunLoop }); ``` @@ -5126,7 +5248,7 @@ Ember.run = function(target, method) { If invoked when not within a run loop: ```javascript - Ember.run.join(function(){ + Ember.run.join(function() { // creates a new run-loop }); ``` @@ -5134,9 +5256,9 @@ Ember.run = function(target, method) { Alternatively, if called within an existing run loop: ```javascript - Ember.run(function(){ + Ember.run(function() { // creates a new run-loop - Ember.run.join(function(){ + Ember.run.join(function() { // joins with the existing run-loop, and queues for invocation on // the existing run-loops action queue. }); @@ -5229,12 +5351,12 @@ Ember.run.end = function() { the `Ember.run.queues` property. ```javascript - Ember.run.schedule('sync', this, function(){ + Ember.run.schedule('sync', this, function() { // this will be executed in the first RunLoop queue, when bindings are synced console.log("scheduled on sync queue"); }); - Ember.run.schedule('actions', this, function(){ + Ember.run.schedule('actions', this, function() { // this will be executed in the 'actions' queue, after bindings have synced. console.log("scheduled on actions queue"); }); @@ -5302,7 +5424,7 @@ Ember.run.sync = function() { together, which is often more efficient than using a real setTimeout. ```javascript - Ember.run.later(myContext, function(){ + Ember.run.later(myContext, function() { // code here will execute within a RunLoop in about 500ms with this == myContext }, 500); ``` @@ -5350,11 +5472,11 @@ Ember.run.once = function(target, method) { calls. ```javascript - Ember.run(function(){ + Ember.run(function() { var sayHi = function() { console.log('hi'); } Ember.run.scheduleOnce('afterRender', myContext, sayHi); Ember.run.scheduleOnce('afterRender', myContext, sayHi); - // doFoo will only be executed once, in the afterRender queue of the RunLoop + // sayHi will only be executed once, in the afterRender queue of the RunLoop }); ``` @@ -5395,7 +5517,7 @@ Ember.run.scheduleOnce = function(queue, target, method) { `Ember.run.later` with a wait time of 1ms. ```javascript - Ember.run.next(myContext, function(){ + Ember.run.next(myContext, function() { // code to be executed in the next run loop, which will be scheduled after the current one }); ``` @@ -5457,17 +5579,17 @@ Ember.run.next = function() { `Ember.run.once()`, or `Ember.run.next()`. ```javascript - var runNext = Ember.run.next(myContext, function(){ + var runNext = Ember.run.next(myContext, function() { // will not be executed }); Ember.run.cancel(runNext); - var runLater = Ember.run.later(myContext, function(){ + var runLater = Ember.run.later(myContext, function() { // will not be executed }, 500); Ember.run.cancel(runLater); - var runOnce = Ember.run.once(myContext, function(){ + var runOnce = Ember.run.once(myContext, function() { // will not be executed }); Ember.run.cancel(runOnce); @@ -5482,8 +5604,15 @@ Ember.run.cancel = function(timer) { }; /** - Execute the passed method in a specified amount of time, reset timer - upon additional calls. + Delay calling the target method until the debounce period has elapsed + with no additional debounce calls. If `debounce` is called again before + the specified time has elapsed, the timer is reset and the entire period + must pass again before the target method is called. + + This method should be used when an event may be called multiple times + but the action should only be called once when the event is done firing. + A common example is for scroll events where you only want updates to + happen once scrolling has ceased. ```javascript var myFunc = function() { console.log(this.name + ' ran.'); }; @@ -5507,12 +5636,50 @@ Ember.run.cancel = function(timer) { then it will be looked up on the passed target. @param {Object} [args*] Optional arguments to pass to the timeout. @param {Number} wait Number of milliseconds to wait. + @param {Boolean} immediate Trigger the function on the leading instead of the trailing edge of the wait interval. @return {void} */ Ember.run.debounce = function() { return backburner.debounce.apply(backburner, arguments); }; +/** + Ensure that the target method is never called more frequently than + the specified spacing period. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'throttle'}; + + Ember.run.throttle(myContext, myFunc, 150); + + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 50ms passes + Ember.run.throttle(myContext, myFunc, 150); + + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'throttle ran.' twice, 150ms apart. + ``` + + @method throttle + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} spacing Number of milliseconds to space out requests. + @return {void} +*/ +Ember.run.throttle = function() { + return backburner.throttle.apply(backburner, arguments); +}; + // Make sure it's not an autorun during testing function checkAutoRun() { if (!Ember.run.currentRunLoop) { @@ -6003,7 +6170,8 @@ Ember.oneWay = function(obj, to, from) { (function() { /** -@module ember-metal +@module ember +@submodule ember-metal */ var Mixin, REQUIRED, Alias, @@ -6062,13 +6230,13 @@ function mixinProperties(mixinsMeta, mixin) { } } -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; @@ -6135,7 +6303,28 @@ function applyConcatenatedProperties(obj, key, value, values) { } } -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; } @@ -6151,8 +6340,12 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { // 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; @@ -6161,7 +6354,7 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { } 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]; @@ -6177,12 +6370,13 @@ function mergeMixins(mixins, m, descs, values, base, keys) { 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 @@ -6252,26 +6446,30 @@ function followAlias(obj, desc, m, descs, values) { return { desc: desc, value: value }; } -function updateObservers(obj, key, observer, observerKey, method) { - if ('function' !== typeof observer) { return; } - - var paths = observer[observerKey]; +function updateObserversAndListeners(obj, key, observerOrListener, pathsKey, updateMethod) { + var paths = observerOrListener[pathsKey]; if (paths) { for (var i=0, l=paths.length; i this.changingFrom ? 'green' : 'red'; + if (this.get('state') === 'inDOM') { + 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') }); ``` @@ -6703,65 +6876,82 @@ Ember Metal (function() { define("rsvp/all", - ["rsvp/defer","exports"], + ["rsvp/promise","exports"], function(__dependency1__, __exports__) { "use strict"; - var defer = __dependency1__.defer; + var Promise = __dependency1__.Promise; + /* global toString */ - function all(promises) { - var results = [], deferred = defer(), remaining = promises.length; - if (remaining === 0) { - deferred.resolve([]); + function all(promises) { + if (Object.prototype.toString.call(promises) !== "[object Array]") { + throw new TypeError('You must pass an array to all.'); } - var resolver = function(index) { - return function(value) { - resolveAll(index, value); - }; - }; + return new Promise(function(resolve, reject) { + var results = [], remaining = promises.length, + promise; - var resolveAll = function(index, value) { - results[index] = value; - if (--remaining === 0) { - deferred.resolve(results); + if (remaining === 0) { + resolve([]); } - }; - var rejectAll = function(error) { - deferred.reject(error); - }; + function resolver(index) { + return function(value) { + resolveAll(index, value); + }; + } - for (var i = 0; i < promises.length; i++) { - if (promises[i] && typeof promises[i].then === 'function') { - promises[i].then(resolver(i), rejectAll); - } else { - resolveAll(i, promises[i]); + function resolveAll(index, value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } } - } - return deferred.promise; + + for (var i = 0; i < promises.length; i++) { + promise = promises[i]; + + if (promise && typeof promise.then === 'function') { + promise.then(resolver(i), reject); + } else { + resolveAll(i, promise); + } + } + }); } + __exports__.all = all; }); - define("rsvp/async", ["exports"], function(__exports__) { "use strict"; var browserGlobal = (typeof window !== 'undefined') ? window : {}; - var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var async; - if (typeof process !== 'undefined' && - {}.toString.call(process) === '[object process]') { - async = function(callback, binding) { + // old node + function useNextTick() { + return function(callback, arg) { process.nextTick(function() { - callback.call(binding); + callback(arg); }); }; - } else if (BrowserMutationObserver) { + } + + // node >= 0.10.x + function useSetImmediate() { + return function(callback, arg) { + /* global setImmediate */ + setImmediate(function(){ + callback(arg); + }); + }; + } + + function useMutationObserver() { var queue = []; var observer = new BrowserMutationObserver(function() { @@ -6769,8 +6959,8 @@ define("rsvp/async", queue = []; toProcess.forEach(function(tuple) { - var callback = tuple[0], binding = tuple[1]; - callback.call(binding); + var callback = tuple[0], arg= tuple[1]; + callback(arg); }); }); @@ -6781,24 +6971,35 @@ define("rsvp/async", window.addEventListener('unload', function(){ observer.disconnect(); observer = null; - }); + }, false); - async = function(callback, binding) { - queue.push([callback, binding]); + return function(callback, arg) { + queue.push([callback, arg]); element.setAttribute('drainQueue', 'drainQueue'); }; - } else { - async = function(callback, binding) { + } + + function useSetTimeout() { + return function(callback, arg) { setTimeout(function() { - callback.call(binding); + callback(arg); }, 1); }; } + if (typeof setImmediate === 'function') { + async = useSetImmediate(); + } else if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { + async = useNextTick(); + } else if (BrowserMutationObserver) { + async = useMutationObserver(); + } else { + async = useSetTimeout(); + } + __exports__.async = async; }); - define("rsvp/config", ["rsvp/async","exports"], function(__dependency1__, __exports__) { @@ -6808,9 +7009,9 @@ define("rsvp/config", var config = {}; config.async = async; + __exports__.config = config; }); - define("rsvp/defer", ["rsvp/promise","exports"], function(__dependency1__, __exports__) { @@ -6818,20 +7019,24 @@ define("rsvp/defer", var Promise = __dependency1__.Promise; function defer() { - var deferred = {}; + var deferred = { + // pre-allocate shape + resolve: undefined, + reject: undefined, + promise: undefined + }; - var promise = new Promise(function(resolve, reject) { + deferred.promise = new Promise(function(resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; }); - deferred.promise = promise; return deferred; } + __exports__.defer = defer; }); - define("rsvp/events", ["exports"], function(__exports__) { @@ -6933,7 +7138,6 @@ define("rsvp/events", __exports__.EventTarget = EventTarget; }); - define("rsvp/hash", ["rsvp/defer","exports"], function(__dependency1__, __exports__) { @@ -6941,13 +7145,13 @@ define("rsvp/hash", var defer = __dependency1__.defer; function size(object) { - var size = 0; + var s = 0; for (var prop in object) { - size++; + s++; } - return size; + return s; } function hash(promises) { @@ -6985,9 +7189,9 @@ define("rsvp/hash", return deferred.promise; } + __exports__.hash = hash; }); - define("rsvp/node", ["rsvp/promise","rsvp/all","exports"], function(__dependency1__, __dependency2__, __exports__) { @@ -7010,6 +7214,7 @@ define("rsvp/node", function denodeify(nodeFunc) { return function() { var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject; + var thisArg = this; var promise = new Promise(function(nodeResolve, nodeReject) { resolve = nodeResolve; @@ -7020,7 +7225,7 @@ define("rsvp/node", nodeArgs.push(makeNodeCallbackFor(resolve, reject)); try { - nodeFunc.apply(this, nodeArgs); + nodeFunc.apply(thisArg, nodeArgs); } catch(e) { reject(e); } @@ -7030,9 +7235,9 @@ define("rsvp/node", }; } + __exports__.denodeify = denodeify; }); - define("rsvp/promise", ["rsvp/config","rsvp/events","exports"], function(__dependency1__, __dependency2__, __exports__) { @@ -7080,6 +7285,8 @@ define("rsvp/promise", this.trigger('error', { detail: event.detail }); }, this); + this.on('error', onerror); + try { resolver(resolvePromise, rejectPromise); } catch(e) { @@ -7087,6 +7294,12 @@ define("rsvp/promise", } }; + function onerror(event) { + if (config.onerror) { + config.onerror(event.detail); + } + } + var invokeCallback = function(type, promise, callback, event) { var hasCallback = isFunction(callback), value, error, succeeded, failed; @@ -7120,18 +7333,25 @@ define("rsvp/promise", Promise.prototype = { constructor: Promise, + isRejected: undefined, + isFulfilled: undefined, + rejectedReason: undefined, + fulfillmentValue: undefined, + then: function(done, fail) { - var thenPromise = new Promise(function() {}); + this.off('error', onerror); + + var thenPromise = new this.constructor(function() {}); if (this.isFulfilled) { - config.async(function() { - invokeCallback('resolve', thenPromise, done, { detail: this.fulfillmentValue }); + config.async(function(promise) { + invokeCallback('resolve', thenPromise, done, { detail: promise.fulfillmentValue }); }, this); } if (this.isRejected) { - config.async(function() { - invokeCallback('reject', thenPromise, fail, { detail: this.rejectedReason }); + config.async(function(promise) { + invokeCallback('reject', thenPromise, fail, { detail: promise.rejectedReason }); }, this); } @@ -7158,32 +7378,40 @@ define("rsvp/promise", } function handleThenable(promise, value) { - var then = null; + var then = null, + resolved; - if (objectOrFunction(value)) { - try { - then = value.then; - } catch(e) { - reject(promise, e); - return true; + try { + if (promise === value) { + throw new TypeError("A promises callback cannot return that same promise."); } - if (isFunction(then)) { - try { + if (objectOrFunction(value)) { + then = value.then; + + if (isFunction(then)) { then.call(value, function(val) { + if (resolved) { return true; } + resolved = true; + if (value !== val) { resolve(promise, val); } else { fulfill(promise, val); } }, function(val) { + if (resolved) { return true; } + resolved = true; + reject(promise, val); }); - } catch (e) { - reject(promise, e); + + return true; } - return true; } + } catch (error) { + reject(promise, error); + return true; } return false; @@ -7208,19 +7436,12 @@ define("rsvp/promise", __exports__.Promise = Promise; }); - define("rsvp/reject", ["rsvp/promise","exports"], function(__dependency1__, __exports__) { "use strict"; var Promise = __dependency1__.Promise; - - function objectOrFunction(x) { - return typeof x === "function" || (typeof x === "object" && x !== null); - } - - function reject(reason) { return new Promise(function (resolve, reject) { reject(reason); @@ -7230,48 +7451,21 @@ define("rsvp/reject", __exports__.reject = reject; }); - define("rsvp/resolve", ["rsvp/promise","exports"], function(__dependency1__, __exports__) { "use strict"; var Promise = __dependency1__.Promise; - - function objectOrFunction(x) { - return typeof x === "function" || (typeof x === "object" && x !== null); - } - - function resolve(thenable){ - var promise = new Promise(function(resolve, reject){ - var then; - - try { - if ( objectOrFunction(thenable) ) { - then = thenable.then; - - if (typeof then === "function") { - then.call(thenable, resolve, reject); - } else { - resolve(thenable); - } - - } else { - resolve(thenable); - } - - } catch(error) { - reject(error); - } + function resolve(thenable) { + return new Promise(function(resolve, reject) { + resolve(thenable); }); - - return promise; } __exports__.resolve = resolve; }); - define("rsvp", ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { @@ -7302,8 +7496,6 @@ define("rsvp", __exports__.reject = reject; }); - - })(); (function() { @@ -7311,12 +7503,43 @@ define("container", [], function() { + /** + A safe and simple inheriting object. + + @class InheritingDict + */ function InheritingDict(parent) { this.parent = parent; this.dict = {}; } InheritingDict.prototype = { + + /** + @property parent + @type InheritingDict + @default null + */ + + parent: null, + + /** + Object used to store the current nodes data. + + @property dict + @type Object + @default Object + */ + dict: null, + + /** + Retrieve the value given a key, if the value is present at the current + level use it, otherwise walk up the parent hierarchy and try again. If + no matching key is found, return undefined. + + @method get + @return {any} + */ get: function(key) { var dict = this.dict; @@ -7329,10 +7552,36 @@ define("container", } }, + /** + Set the given value for the given key, at the current level. + + @method set + @param {String} key + @param {Any} value + */ set: function(key, value) { this.dict[key] = value; }, + /** + Delete the given key + + @method remove + @param {String} key + */ + remove: function(key) { + delete this.dict[key]; + }, + + /** + Check for the existence of given a key, if the key is present at the current + level return true, otherwise walk up the parent hierarchy and try again. If + no matching key is found, return false. + + @method has + @param {String} key + @returns {Boolean} + */ has: function(key) { var dict = this.dict; @@ -7347,6 +7596,13 @@ define("container", return false; }, + /** + Iterate and invoke a callback for each local key-value pair. + + @method eachLocal + @param {Function} callback + @param {Object} binding + */ eachLocal: function(callback, binding) { var dict = this.dict; @@ -7358,34 +7614,144 @@ define("container", } }; + /** + A lightweight container that helps to assemble and decouple components. + + @class Container + */ function Container(parent) { this.parent = parent; this.children = []; this.resolver = parent && parent.resolver || function() {}; + this.registry = new InheritingDict(parent && parent.registry); this.cache = new InheritingDict(parent && parent.cache); + this.factoryCache = new InheritingDict(parent && parent.cache); this.typeInjections = new InheritingDict(parent && parent.typeInjections); this.injections = {}; + + this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); + this.factoryInjections = {}; + this._options = new InheritingDict(parent && parent._options); this._typeOptions = new InheritingDict(parent && parent._typeOptions); } Container.prototype = { + + /** + @property parent + @type Container + @default null + */ + parent: null, + + /** + @property children + @type Array + @default [] + */ + children: null, + + /** + @property resolver + @type function + */ + resolver: null, + + /** + @property registry + @type InheritingDict + */ + registry: null, + + /** + @property cache + @type InheritingDict + */ + cache: null, + + /** + @property typeInjections + @type InheritingDict + */ + typeInjections: null, + + /** + @property injections + @type Object + @default {} + */ + injections: null, + + /** + @private + + @property _options + @type InheritingDict + @default null + */ + _options: null, + + /** + @private + + @property _typeOptions + @type InheritingDict + */ + _typeOptions: null, + + /** + Returns a new child of the current container. These children are configured + to correctly inherit from the current container. + + @method child + @returns {Container} + */ child: function() { var container = new Container(this); this.children.push(container); return container; }, + /** + Sets a key-value pair on the current container. If a parent container, + has the same key, once set on a child, the parent and child will diverge + as expected. + + @method set + @param {Object} object + @param {String} key + @param {any} value + */ set: function(object, key, value) { object[key] = value; }, + /** + Registers a factory for later injection. + + Example: + + ```javascript + var container = new Container(); + + container.register('model:user', Person, {singleton: false }); + container.register('fruit:favorite', Orange); + container.register('communication:main', Email, {singleton: false}); + ``` + + @method register + @param {String} type + @param {String} name + @param {Function} factory + @param {Object} options + */ register: function(type, name, factory, options) { var fullName; - if (type.indexOf(':') !== -1){ + if (type.indexOf(':') !== -1) { options = factory; factory = name; fullName = type; @@ -7400,14 +7766,129 @@ define("container", this._options.set(normalizedName, options || {}); }, + /** + Unregister a fullName + + ```javascript + var container = new Container(); + container.register('model:user', User); + + container.lookup('model:user') instanceof User //=> true + + container.unregister('model:user') + container.lookup('model:user') === undefined //=> true + + @method unregister + @param {String} fullName + */ + unregister: function(fullName) { + var normalizedName = this.normalize(fullName); + + this.registry.remove(normalizedName); + this.cache.remove(normalizedName); + this._options.remove(normalizedName); + }, + + /** + Given a fullName return the corresponding factory. + + By default `resolve` will retrieve the factory from + its container's registry. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + container.resolve('api:twitter') // => Twitter + ``` + + Optionally the container can be provided with a custom resolver. + If provided, `resolve` will first provide the custom resolver + the oppertunity to resolve the fullName, otherwise it will fallback + to the registry. + + ```javascript + var container = new Container(); + container.resolver = function(fullName) { + // lookup via the module system of choice + }; + + // the twitter factory is added to the module system + container.resolve('api:twitter') // => Twitter + ``` + + @method resolve + @param {String} fullName + @returns {Function} fullName's factory + */ resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); }, + /** + A hook that can be used to describe how the resolver will + attempt to find the factory. + + For example, the default Ember `.describe` returns the full + class name (including namespace) where Ember's resolver expects + to find the `fullName`. + + @method describe + */ + describe: function(fullName) { + return fullName; + }, + + /** + A hook to enable custom fullName normalization behaviour + + @method normalize + @param {String} fullName + @return {string} normalized fullName + */ normalize: function(fullName) { return fullName; }, + /** + Given a fullName return a corresponding instance. + + The default behaviour is for lookup to return a singleton instance. + The singleton is scoped to the container, allowing multiple containers + to all have their own locally scoped singletons. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + var twitter = container.lookup('api:twitter'); + + twitter instanceof Twitter; // => true + + // by default the container will return singletons + twitter2 = container.lookup('api:twitter'); + twitter instanceof Twitter; // => true + + twitter === twitter2; //=> true + ``` + + If singletons are not wanted an optional flag can be provided at lookup. + + ```javascript + var container = new Container(); + container.register('api:twitter', Twitter); + + var twitter = container.lookup('api:twitter', { singleton: false }); + var twitter2 = container.lookup('api:twitter', { singleton: false }); + + twitter === twitter2; //=> false + ``` + + @method lookup + @param {String} fullName + @param {Object} options + @return {any} + */ lookup: function(fullName, options) { fullName = this.normalize(fullName); @@ -7428,10 +7909,25 @@ define("container", return value; }, + /** + Given a fullName return the corresponding factory. + + @method lookupFactory + @param {String} fullName + @return {any} + */ lookupFactory: function(fullName) { return factoryFor(this, fullName); }, + /** + Given a fullName check if the container is aware of its factory + or singleton instance. + + @method has + @param {String} fullName + @return {Boolean} + */ has: function(fullName) { if (this.cache.has(fullName)) { return true; @@ -7440,27 +7936,134 @@ define("container", return !!factoryFor(this, fullName); }, + /** + Allow registering options for all factories of a type. + + ```javascript + var container = new Container(); + + // if all of type `connection` must not be singletons + container.optionsForType('connection', { singleton: false }); + + container.register('connection:twitter', TwitterConnection); + container.register('connection:facebook', FacebookConnection); + + var twitter = container.lookup('connection:twitter'); + var twitter2 = container.lookup('connection:twitter'); + + twitter === twitter2; // => false + + var facebook = container.lookup('connection:facebook'); + var facebook2 = container.lookup('connection:facebook'); + + facebook === facebook2; // => false + ``` + + @method optionsForType + @param {String} type + @param {Object} options + */ optionsForType: function(type, options) { if (this.parent) { illegalChildOperation('optionsForType'); } this._typeOptions.set(type, options); }, + /** + @method options + @param {String} type + @param {Object} options + */ options: function(type, options) { this.optionsForType(type, options); }, - typeInjection: function(type, property, fullName) { - if (this.parent) { illegalChildOperation('typeInjection'); } - - var injections = this.typeInjections.get(type); - if (!injections) { - injections = []; - this.typeInjections.set(type, injections); - } - injections.push({ property: property, fullName: fullName }); + /* + @private + + Used only via `injection`. + + Provides a specialized form of injection, specifically enabling + all objects of one type to be injected with a reference to another + object. + + For example, provided each object of type `controller` needed a `router`. + one would do the following: + + ```javascript + var container = new Container(); + + container.register('router:main', Router); + container.register('controller:user', UserController); + container.register('controller:post', PostController); + + container.typeInjection('controller', 'router', 'router:main'); + + var user = container.lookup('controller:user'); + var post = container.lookup('controller:post'); + + user.router instanceof Router; //=> true + post.router instanceof Router; //=> true + + // both controllers share the same router + user.router === post.router; //=> true + ``` + + @method typeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + typeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('typeInjection'); } + + addTypeInjection(this.typeInjections, type, property, fullName); }, + /* + Defines injection rules. + + These rules are used to inject dependencies onto objects when they + are instantiated. + + Two forms of injections are possible: + + * Injecting one fullName on another fullName + * Injecting one fullName on a type + + Example: + + ```javascript + var container = new Container(); + + container.register('source:main', Source); + container.register('model:user', User); + container.register('model:post', Post); + + // injecting one fullName on another fullName + // eg. each user model gets a post model + container.injection('model:user', 'post', 'model:post'); + + // injecting one fullName on another type + container.injection('model', 'source', 'source:main'); + + var user = container.lookup('model:user'); + var post = container.lookup('model:post'); + + user.source instanceof Source; //=> true + post.source instanceof Source; //=> true + + user.post instanceof Post; //=> true + + // and both models share the same source + user.source === post.source; //=> true + ``` + + @method injection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ injection: function(factoryName, property, injectionName) { if (this.parent) { illegalChildOperation('injection'); } @@ -7468,10 +8071,112 @@ define("container", return this.typeInjection(factoryName, property, injectionName); } - var injections = this.injections[factoryName] = this.injections[factoryName] || []; - injections.push({ property: property, fullName: injectionName }); + addInjection(this.injections, factoryName, property, injectionName); + }, + + + /* + @private + + Used only via `factoryInjection`. + + Provides a specialized form of injection, specifically enabling + all factory of one type to be injected with a reference to another + object. + + For example, provided each factory of type `model` needed a `store`. + one would do the following: + + ```javascript + var container = new Container(); + + container.registerFactory('model:user', User); + container.register('store:main', SomeStore); + + container.factoryTypeInjection('model', 'store', 'store:main'); + + var store = container.lookup('store:main'); + var UserFactory = container.lookupFactory('model:user'); + + UserFactory.store instanceof SomeStore; //=> true + ``` + + @method factoryTypeInjection + @param {String} type + @param {String} property + @param {String} fullName + */ + factoryTypeInjection: function(type, property, fullName) { + if (this.parent) { illegalChildOperation('factoryTypeInjection'); } + + addTypeInjection(this.factoryTypeInjections, type, property, fullName); + }, + + /* + Defines factory injection rules. + + Similar to regular injection rules, but are run against factories, via + `Container#lookupFactory`. + + These rules are used to inject objects onto factories when they + are looked up. + + Two forms of injections are possible: + + * Injecting one fullName on another fullName + * Injecting one fullName on a type + + Example: + + ```javascript + var container = new Container(); + + container.register('store:main', Store); + container.register('store:secondary', OtherStore); + container.register('model:user', User); + container.register('model:post', Post); + + // injecting one fullName on another type + container.factoryInjection('model', 'store', 'store:main'); + + // injecting one fullName on another fullName + container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); + + var UserFactory = container.lookupFactory('model:user'); + var PostFactory = container.lookupFactory('model:post'); + var store = container.lookup('store:main'); + + UserFactory.store instanceof Store; //=> true + UserFactory.secondaryStore instanceof OtherStore; //=> false + + PostFactory.store instanceof Store; //=> true + PostFactory.secondaryStore instanceof OtherStore; //=> true + + // and both models share the same source instance + UserFactory.store === PostFactory.store; //=> true + ``` + + @method factoryInjection + @param {String} factoryName + @param {String} property + @param {String} injectionName + */ + factoryInjection: function(factoryName, property, injectionName) { + if (this.parent) { illegalChildOperation('injection'); } + + if (factoryName.indexOf(':') === -1) { + return this.factoryTypeInjection(factoryName, property, injectionName); + } + + addInjection(this.factoryInjections, factoryName, property, injectionName); }, + /** + A depth first traversal, destroying the container, its descendant containers and all + their managed objects. + + @method destroy + */ destroy: function() { this.isDestroyed = true; @@ -7485,10 +8190,13 @@ define("container", item.destroy(); }); - delete this.parent; + this.parent = undefined; this.isDestroyed = true; }, + /** + @method reset + */ reset: function() { for (var i=0, l=this.children.length; i1) args = a_slice.call(arguments, 1); this.forEach(function(x, idx) { @@ -9066,7 +9982,7 @@ Ember.Enumerable = Ember.Mixin.create({ @return {Array} the enumerable as an array. */ toArray: function() { - var ret = Ember.A([]); + var ret = Ember.A(); this.forEach(function(o, idx) { ret[idx] = o; }); return ret ; }, @@ -9102,7 +10018,7 @@ Ember.Enumerable = Ember.Mixin.create({ */ without: function(value) { if (!this.contains(value)) return this; // nothing to do - var ret = Ember.A([]); + var ret = Ember.A(); this.forEach(function(k) { if (k !== value) ret[ret.length] = k; }) ; @@ -9122,8 +10038,8 @@ Ember.Enumerable = Ember.Mixin.create({ @return {Ember.Enumerable} */ uniq: function() { - var ret = Ember.A([]); - this.forEach(function(k){ + var ret = Ember.A(); + this.forEach(function(k) { if (a_indexOf(ret, k)<0) ret.push(k); }); return ret; @@ -9327,7 +10243,6 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.Enumera @class Array @namespace Ember - @extends Ember.Mixin @uses Ember.Enumerable @since Ember 0.9.0 */ @@ -9383,7 +10298,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot */ objectsAt: function(indexes) { var self = this; - return map(indexes, function(idx){ return self.objectAt(idx); }); + return map(indexes, function(idx) { return self.objectAt(idx); }); }, // overrides Ember.Enumerable version @@ -9415,7 +10330,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot }), // optimized version from Enumerable - contains: function(obj){ + contains: function(obj) { return this.indexOf(obj) >= 0; }, @@ -9438,7 +10353,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot @return {Array} New array with specified slice */ slice: function(beginIndex, endIndex) { - var ret = Ember.A([]); + var ret = Ember.A(); var length = get(this, 'length') ; if (isNone(beginIndex)) beginIndex = 0 ; if (isNone(endIndex) || (endIndex > length)) endIndex = length ; @@ -9480,7 +10395,7 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot if (startAt < 0) startAt += len; for(idx=startAt;idx0) { this._initMixins(arguments); } return new C(); }, + /** + Creates an instance of a class. Accepts either no arguments, or an object + containing values to initialize the newly instantiated object with. + + ```javascript + App.Person = Ember.Object.extend({ + helloWorld: function() { + alert("Hi, my name is " + this.get('name')); + } + }); + + var tom = App.Person.create({ + name: 'Tom Dale' + }); + + tom.helloWorld(); // alerts "Hi, my name is Tom Dale". + ``` + + `create` will call the `init` function if defined during + `Ember.AnyObject.extend` + + If no arguments are passed to `create`, it will not set values to the new + instance during initialization: + + ```javascript + var noName = App.Person.create(); + noName.helloWorld(); // alerts undefined + ``` + + NOTE: For performance reasons, you cannot declare methods or computed + properties during `create`. You should instead declare methods and computed + properties when using `extend` or use the `createWithMixins` shorthand. + + @method create + @static + @param [arguments]* + */ create: function() { var C = this; if (arguments.length>0) { this._initProperties(arguments); } @@ -11768,10 +12722,6 @@ if (Ember.config.overrideClassMixin) { CoreObject.ClassMixin = ClassMixin; ClassMixin.apply(CoreObject); -/** - @class CoreObject - @namespace Ember -*/ Ember.CoreObject = CoreObject; })(); @@ -12291,6 +13241,9 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array }, pushObjects: function(objects) { + if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { + throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); + } this._replace(get(this, 'length'), 0, objects); return this; }, @@ -12478,6 +13431,7 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); return set(content, key, value); } + }); Ember.ObjectProxy.reopenClass({ @@ -12543,6 +13497,7 @@ function addObserverForContentKey(content, keyName, proxy, idx, loc) { while(--loc>=idx) { var item = content.objectAt(loc); if (item) { + Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', Ember.typeOf(item) === 'instance' || Ember.typeOf(item) === 'object'); Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); @@ -12626,7 +13581,7 @@ Ember.EachProxy = Ember.Object.extend({ for(key in keys) { if (!keys.hasOwnProperty(key)) { continue; } - if (lim>0) removeObserverForContentKey(content, key, this, idx, lim); + if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } Ember.propertyWillChange(this, key); } @@ -12636,21 +13591,20 @@ Ember.EachProxy = Ember.Object.extend({ }, arrayDidChange: function(content, idx, removedCnt, addedCnt) { - var keys = this._keys, key, lim; + var keys = this._keys, lim; lim = addedCnt>0 ? idx+addedCnt : -1; - Ember.beginPropertyChanges(this); + Ember.changeProperties(function() { + for(var key in keys) { + if (!keys.hasOwnProperty(key)) { continue; } - for(key in keys) { - if (!keys.hasOwnProperty(key)) { continue; } + if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } - if (lim>0) addObserverForContentKey(content, key, this, idx, lim); - - Ember.propertyDidChange(this, key); - } + Ember.propertyDidChange(this, key); + } - Ember.propertyDidChange(this._content, '@each'); - Ember.endPropertyChanges(this); + Ember.propertyDidChange(this._content, '@each'); + }, this); }, // .......................................................... @@ -12799,7 +13753,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember copy: function(deep) { if (deep) { - return this.map(function(item){ return Ember.copy(item, true); }); + return this.map(function(item) { return Ember.copy(item, true); }); } return this.slice(); @@ -12825,7 +13779,6 @@ if (ignore.length>0) { @class NativeArray @namespace Ember - @extends Ember.Mixin @uses Ember.MutableArray @uses Ember.Observable @uses Ember.Copyable @@ -12840,7 +13793,7 @@ Ember.NativeArray = NativeArray; @for Ember @return {Ember.NativeArray} */ -Ember.A = function(arr){ +Ember.A = function(arr) { if (arr === undefined) { arr = []; } return Ember.Array.detect(arr) ? arr : Ember.NativeArray.apply(arr); }; @@ -13016,7 +13969,7 @@ Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Emb Ember.propertyWillChange(this, 'firstObject'); Ember.propertyWillChange(this, 'lastObject'); - for (var i=0; i < len; i++){ + for (var i=0; i < len; i++) { guid = guidFor(this[i]); delete this[guid]; delete this[i]; @@ -13436,7 +14389,6 @@ var get = Ember.get; @class ControllerMixin @namespace Ember - @extends Ember.Mixin */ Ember.ControllerMixin = Ember.Mixin.create({ /* ducktype as a controller */ @@ -13470,7 +14422,7 @@ Ember.ControllerMixin = Ember.Mixin.create({ if (this[actionName]) { Ember.assert("The controller " + this + " does not have the action " + actionName, typeof this[actionName] === 'function'); this[actionName].apply(this, args); - } else if(target = get(this, 'target')) { + } else if (target = get(this, 'target')) { Ember.assert("The target for controller " + this + " (" + target + ") did not define a `send` method", typeof target.send === 'function'); target.send.apply(target, arguments); } @@ -13524,7 +14476,6 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; @class SortableMixin @namespace Ember - @extends Ember.Mixin @uses Ember.MutableEnumerable */ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { @@ -13542,7 +14493,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { @property {Boolean} sortAscending */ sortAscending: true, - + /** The function used to compare two values. You can override this if you want to do custom comparisons.Functions must be of the type expected by @@ -13552,8 +14503,8 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { return a positive value otherwise: ```javascript - function(x,y){ // These are assumed to be integers - if(x === y) + function(x,y) { // These are assumed to be integers + if (x === y) return 0; return x < y ? -1 : 1; } @@ -13564,7 +14515,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { @default Ember.compare */ sortFunction: Ember.compare, - + orderBy: function(item1, item2) { var result = 0, sortProperties = get(this, 'sortProperties'), @@ -13927,20 +14878,24 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, controllerAt: function(idx, object, controllerClass) { var container = get(this, 'container'), subControllers = get(this, '_subControllers'), - subController = subControllers[idx]; + subController = subControllers[idx], + factory, fullName; - if (!subController) { - subController = container.lookup("controller:" + controllerClass, { singleton: false }); - subControllers[idx] = subController; - } + if (subController) { return subController; } - if (!subController) { + fullName = "controller:" + controllerClass; + + if (!container.has(fullName)) { throw new Error('Could not resolve itemController: "' + controllerClass + '"'); } - subController.set('target', this); - subController.set('parentController', get(this, 'parentController') || this); - subController.set('content', object); + subController = container.lookupFactory(fullName).create({ + target: this, + parentController: get(this, 'parentController') || this, + content: object + }); + + subControllers[idx] = subController; return subController; }, @@ -13970,10 +14925,7 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, */ /** - `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. @@ -14059,7 +15011,7 @@ if (Ember.$) { // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ -var needsShy = this.document && (function(){ +var needsShy = this.document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; @@ -14215,8 +15167,8 @@ ClassSet.prototype = { } }; -var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z\-]/; -var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z\-]/g; +var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z0-9\-]/; +var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z0-9\-]/g; function stripTagName(tagName) { if (!tagName) { @@ -14686,7 +15638,7 @@ Ember._RenderBuffer.prototype = if (this._hasElement && this._element) { // Firefox versions < 11 do not have support for element.outerHTML. var thisElement = this.element(), outerHTML = thisElement.outerHTML; - if (typeof outerHTML === 'undefined'){ + if (typeof outerHTML === 'undefined') { return Ember.$('
').append(thisElement).html(); } return outerHTML; @@ -14900,7 +15852,9 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro 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(); } @@ -15013,8 +15967,11 @@ var childViewsProperty = Ember.computed(function() { var childViews = this._childViews, ret = Ember.A(), view = this; a_forEach(childViews, function(view) { + var currentChildViews; if (view.isVirtual) { - ret.pushObjects(get(view, 'childViews')); + if (currentChildViews = get(view, 'childViews')) { + ret.pushObjects(currentChildViews); + } } else { ret.push(view); } @@ -15309,8 +16266,8 @@ class: MyView = Ember.View.extend({ classNameBindings: ['propertyA', 'propertyB'], propertyA: 'from-a', - propertyB: function(){ - if(someLogic){ return 'from-b'; } + propertyB: function() { + if (someLogic) { return 'from-b'; } }.property() }); ``` @@ -15395,7 +16352,7 @@ class: ```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 }); @@ -15418,7 +16375,7 @@ class: ```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 }); @@ -15491,7 +16448,7 @@ class: MyTextInput = Ember.View.extend({ tagName: 'input', attributeBindings: ['disabled'], - disabled: function(){ + disabled: function() { if (someLogic) { return true; } else { @@ -15600,7 +16557,7 @@ class: aController = Ember.Object.create({ firstName: 'Barry', - excitedGreeting: function(){ + excitedGreeting: function() { return this.get("content.firstName") + "!!!" }.property() }); @@ -15671,7 +16628,7 @@ class: ```javascript AView = Ember.View.extend({ - click: function(event){ + click: function(event) { // will be called when when an instance's // rendered element is clicked } @@ -15692,7 +16649,7 @@ class: ```javascript AView = Ember.View.extend({ eventManager: Ember.Object.create({ - doubleClick: function(event, view){ + doubleClick: function(event, view) { // will be called when when an instance's // rendered element or any rendering // of this views's descendent @@ -15707,12 +16664,12 @@ class: ```javascript AView = Ember.View.extend({ - mouseEnter: function(event){ + mouseEnter: function(event) { // will never trigger. }, eventManager: Ember.Object.create({ - mouseEnter: function(event, view){ - // takes presedence over AView#mouseEnter + mouseEnter: function(event, view) { + // takes precedence over AView#mouseEnter } }) }); @@ -15729,7 +16686,7 @@ class: OuterView = Ember.View.extend({ template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), eventManager: Ember.Object.create({ - mouseEnter: function(event, view){ + mouseEnter: function(event, view) { // view might be instance of either // OuterView or InnerView depending on // where on the page the user interaction occured @@ -15738,12 +16695,12 @@ class: }); InnerView = Ember.View.extend({ - click: function(event){ + click: function(event) { // will be called if rendered inside // an OuterView because OuterView's // eventManager doesn't handle click events }, - mouseEnter: function(event){ + mouseEnter: function(event) { // will never be called if rendered inside // an OuterView. } @@ -15923,6 +16880,11 @@ Ember.View = Ember.CoreView.extend( return layout || get(this, 'defaultLayout'); }).property('layoutName'), + _yield: function(context, options) { + var template = get(this, 'template'); + if (template) { template(context, options); } + }, + templateForName: function(name, type) { if (!name) { return; } Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); @@ -16054,7 +17016,7 @@ Ember.View = Ember.CoreView.extend( var view = get(this, 'parentView'); while (view) { - if(view instanceof klass) { return view; } + if (view instanceof klass) { return view; } view = get(view, 'parentView'); } }, @@ -16075,7 +17037,7 @@ Ember.View = Ember.CoreView.extend( function(view) { return klass.detect(view.constructor); }; while (view) { - if( isOfType(view) ) { return view; } + if (isOfType(view)) { return view; } view = get(view, 'parentView'); } }, @@ -16108,7 +17070,7 @@ Ember.View = Ember.CoreView.extend( var view = get(this, 'parentView'); while (view) { - if(get(view, 'parentView') instanceof klass) { return view; } + if (get(view, 'parentView') instanceof klass) { return view; } view = get(view, 'parentView'); } }, @@ -16457,6 +17419,7 @@ Ember.View = Ember.CoreView.extend( // 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); }); @@ -16478,6 +17441,7 @@ Ember.View = Ember.CoreView.extend( @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() { @@ -16631,7 +17595,7 @@ Ember.View = Ember.CoreView.extend( */ invokeRecursively: function(fn, includeSelf) { var childViews = (includeSelf === false) ? this._childViews : [this]; - var currentViews, view; + var currentViews, view, currentChildViews; while (childViews.length) { currentViews = childViews.slice(); @@ -16639,16 +17603,17 @@ Ember.View = Ember.CoreView.extend( for (var i=0, l=currentViews.length; i - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['type'], type: 'button' }); @@ -16919,7 +17886,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Renders something like
- Ember.View.create({ + Ember.View.extend({ attributeBindings: ['enabled'], enabled: true }); @@ -16956,14 +17923,6 @@ Ember.View = Ember.CoreView.extend( Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); this.classNames = Ember.A(this.classNames.slice()); - - var viewController = get(this, 'viewController'); - if (viewController) { - viewController = get(viewController); - if (viewController) { - set(viewController, 'view', this); - } - } }, appendChild: function(view, options) { @@ -17074,7 +18033,7 @@ Ember.View = Ember.CoreView.extend( act as a child of the parent. @method createChildView - @param {Class} viewClass + @param {Class|String} viewClass @param {Hash} [attrs] Attributes to add @return {Ember.View} new instance */ @@ -17085,11 +18044,11 @@ Ember.View = Ember.CoreView.extend( attrs = attrs || {}; attrs._parentView = this; - attrs.container = this.container; if (Ember.CoreView.detect(view)) { attrs.templateData = attrs.templateData || get(this, 'templateData'); + attrs.container = this.container; view = view.create(attrs); // don't set the property on a virtual view, as they are invisible to @@ -17097,14 +18056,24 @@ Ember.View = Ember.CoreView.extend( if (view.viewName) { set(get(this, 'concreteView'), view.viewName, view); } + } else if ('string' === typeof view) { + var fullName = 'view:' + view; + var View = this.container.lookupFactory(fullName); + + Ember.assert("Could not find view: '" + fullName + "'", !!View); + + attrs.templateData = get(this, 'templateData'); + view = View.create(attrs); } else { Ember.assert('You must pass instance or subclass of View', view.isView); - - Ember.setProperties(view, attrs); + attrs.container = this.container; if (!get(view, 'templateData')) { - set(view, 'templateData', get(this, 'templateData')); + attrs.templateData = get(this, 'templateData'); } + + Ember.setProperties(view, attrs); + } return view; @@ -17218,7 +18187,7 @@ Ember.View = Ember.CoreView.extend( } var view = this, - stateCheckedObserver = function(){ + stateCheckedObserver = function() { view.currentState.invokeObserver(this, observer); }, scheduledObserver = function() { @@ -17452,7 +18421,7 @@ Ember.View.applyAttributeBindings = function(elem, name, value) { } } else if (name === 'value' || type === 'boolean') { // We can't set properties to undefined or null - if (!value) { value = ''; } + if (Ember.isNone(value)) { value = ''; } if (value !== elem.prop(name)) { // value and booleans should always be properties @@ -18152,7 +19121,10 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { 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); @@ -18479,7 +19451,7 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie var content = get(this, 'content'); if (content) { - Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); + this._assertArrayLike(content); content.addArrayObserver(this); } @@ -18487,6 +19459,10 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie this.arrayDidChange(content, 0, null, len); }, 'content'), + _assertArrayLike: function(content) { + Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); + }, + destroy: function() { if (!this._super()) { return; } @@ -18545,17 +19521,20 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie @param {Number} added number of object added to content */ arrayDidChange: function(content, start, removed, added) { - var itemViewClass = get(this, 'itemViewClass'), - addedViews = [], view, item, idx, len; - - if ('string' === typeof itemViewClass) { - itemViewClass = get(itemViewClass); - } - - Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass)); + var addedViews = [], view, item, idx, len, itemViewClass, + emptyView; len = content ? get(content, 'length') : 0; + if (len) { + itemViewClass = get(this, 'itemViewClass'); + + if ('string' === typeof itemViewClass) { + itemViewClass = get(itemViewClass) || itemViewClass; + } + + Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), 'string' === typeof itemViewClass || Ember.View.detect(itemViewClass)); + for (idx = start; idx < start+added; idx++) { item = content.objectAt(idx); @@ -18567,17 +19546,23 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie addedViews.push(view); } } else { - var emptyView = get(this, 'emptyView'); + emptyView = get(this, 'emptyView'); + if (!emptyView) { return; } - var isClass = Ember.CoreView.detect(emptyView); + if ('string' === typeof emptyView) { + emptyView = get(emptyView) || emptyView; + } emptyView = this.createChildView(emptyView); addedViews.push(emptyView); set(this, 'emptyView', emptyView); - if (isClass) { this._createdEmptyView = emptyView; } + if (Ember.CoreView.detect(emptyView)) { + this._createdEmptyView = emptyView; + } } + this.replace(start, 0, addedViews); }, @@ -18585,9 +19570,11 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie view = this._super(view, attrs); var itemTagName = get(view, 'tagName'); - var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName; - set(view, 'tagName', tagName); + if (itemTagName === null || itemTagName === undefined) { + itemTagName = Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')]; + set(view, 'tagName', itemTagName); + } return view; } @@ -18619,6 +19606,8 @@ Ember.CollectionView.CONTAINER_MAP = { (function() { +var get = Ember.get, set = Ember.set, isNone = Ember.isNone; + /** @module ember @submodule ember-views @@ -18634,9 +19623,9 @@ Ember.CollectionView.CONTAINER_MAP = { The easiest way to create an `Ember.Component` is via a template. If you name a template - `controls/my-foo`, you will be able to use + `components/my-foo`, you will be able to use `{{my-foo}}` in other templates, which will make - an instance of the isolated control. + an instance of the isolated component. ```html {{app-profile person=currentUser}} @@ -18653,25 +19642,24 @@ Ember.CollectionView.CONTAINER_MAP = { include the **contents** of the custom tag: ```html - {{#my-profile person=currentUser}} -

Admin mode

- {{/my-profile}} + {{#app-profile person=currentUser}} +

Admin mode

+ {{/app-profile}} ``` ```html -

{{person.title}}

{{yield}} ``` - If you want to customize the control, in order to + If you want to customize the component, in order to handle events or actions, you implement a subclass of `Ember.Component` named after the name of the - control. + component. For example, you could implement the action - `hello` for the `app-profile` control: + `hello` for the `app-profile` component: ```js App.AppProfileComponent = Ember.Component.extend({ @@ -18681,7 +19669,7 @@ Ember.CollectionView.CONTAINER_MAP = { }); ``` - And then use it in the control's template: + And then use it in the component's template: ```html @@ -18703,11 +19691,117 @@ Ember.CollectionView.CONTAINER_MAP = { @namespace Ember @extends Ember.View */ -Ember.Component = Ember.View.extend({ +Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { init: function() { this._super(); - this.set('context', this); - this.set('controller', this); + set(this, 'context', this); + set(this, 'controller', this); + }, + + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _yield: function(context, options) { + var view = options.data.view, + parentView = this._parentView, + template = get(this, 'template'); + + if (template) { + Ember.assert("A Component must have a parent view in order to yield.", parentView); + + view.appendChild(Ember.View, { + isVirtual: true, + tagName: '', + template: get(this, 'template'), + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + targetObject: Ember.computed(function(key) { + var parentView = get(this, '_parentView'); + return parentView ? get(parentView, 'controller') : null; + }).property('_parentView'), + + /** + 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. + + 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 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. + + 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. + + 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, context) { + var actionName; + + // Send the default action + if (action === undefined) { + actionName = get(this, 'action'); + Ember.assert("The default action was triggered on the component " + this.toString() + ", but the action name (" + actionName + ") was not a string.", isNone(actionName) || typeof actionName === 'string'); + } 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, + actionContext: context + }); } }); @@ -18738,7 +19832,7 @@ For example: ```javascript App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { action: 'save', - click: function(){ + click: function() { this.triggerAction(); // Sends the `save` action, along with the current context // to the current controller } @@ -18750,7 +19844,7 @@ to `triggerAction` as well. ```javascript App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { - click: function(){ + click: function() { this.triggerAction({ action: 'save' }); // Sends the `save` action, along with the current context @@ -18806,17 +19900,18 @@ define("metamorph", // Copyright: ©2011 My Company Inc. All rights reserved. // ========================================================================== - var K = function(){}, + var K = function() {}, guid = 0, document = this.document, + disableRange = ('undefined' === typeof ENV ? {} : ENV).DISABLE_RANGE_API, // Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges - supportsRange = document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, + supportsRange = (!disableRange) && document && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, // Internet Explorer prior to 9 does not allow setting innerHTML if the first element // is a "zero-scope" element. This problem can be worked around by making // the first node an invisible text node. We, like Modernizr, use ­ - needsShy = document && (function(){ + needsShy = document && (function() { var testEl = document.createElement('div'); testEl.innerHTML = "
"; testEl.firstChild.innerHTML = ""; @@ -19272,12 +20367,12 @@ var objectCreate = Object.create || function(parent) { }; var Handlebars = this.Handlebars || (Ember.imports && Ember.imports.Handlebars); -if(!Handlebars && typeof require === 'function') { +if (!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars) -Ember.assert("Ember Handlebars requires Handlebars version 1.0.0-rc.4, COMPILER_REVISION expected: 3, got: " + Handlebars.COMPILER_REVISION + " – Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 3); +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.", Handlebars); +Ember.assert("Ember Handlebars requires Handlebars version 1.0.0, COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + " - Please note: Builds of master may have other COMPILER_REVISION values.", Handlebars.COMPILER_REVISION === 4); /** Prepares the Handlebars templating library for use inside Ember's view @@ -19334,7 +20429,7 @@ function makeBindings(options) { ## 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 @@ -19376,14 +20471,14 @@ Ember.Handlebars.helper = function(name, value) { if (Ember.View.detect(value)) { Ember.Handlebars.registerHelper(name, function(options) { - Ember.assert("You can only pass attributes as parameters (not values) to a application-defined helper", arguments.length < 2); + Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View", arguments.length < 2); makeBindings(options); return Ember.Handlebars.helpers.view.call(this, value, options); }); } else { Ember.Handlebars.registerBoundHelper.apply(null, arguments); } -} +}; /** @class helpers @@ -19464,12 +20559,12 @@ Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { } else if (mustache.params.length || mustache.hash) { // no changes required } else { - var id = new Handlebars.AST.IdNode(['_triageMustache']); + var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); // Update the mustache node to include a hash value indicating whether the original node // was escaped. This will allow us to properly escape values when the underlying value // changes and we need to re-render the value. - if(!mustache.escaped) { + if (!mustache.escaped) { mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); } @@ -19527,7 +20622,10 @@ if (Handlebars.compile) { 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; }; } @@ -19667,7 +20765,7 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { var error, view = ""; error = "%@ Handlebars error: Could not find property '%@' on object %@."; - if (options.data){ + if (options.data) { view = options.data.view; } throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); @@ -19704,7 +20802,7 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { Ember.Handlebars.registerBoundHelper('repeat', function(value, options) { var count = options.hash.count; var a = []; - while(a.length < count){ + while(a.length < count) { a.push(value); } return a.join(''); @@ -19748,7 +20846,7 @@ Ember.Handlebars.registerHelper('helperMissing', function(path, options) { ```javascript Ember.Handlebars.registerBoundHelper('concatenate', function() { - var values = arguments[arguments.length - 1]; + var values = Array.prototype.slice.call(arguments, 0, -1); return values.join('||'); }); ``` @@ -19795,7 +20893,7 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { view = data.view, currentContext = (options.contexts && options.contexts[0]) || this, normalized, - pathRoot, path, + pathRoot, path, prefixPathForDependentKeys = '', loc, hashOption; Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); @@ -19846,8 +20944,11 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { view.registerObserver(pathRoot, path, bindView, bindView.rerender); + if(!Ember.isEmpty(path)) { + prefixPathForDependentKeys = path + '.'; + } for (var i=0, l=dependentKeys.length; isomeString
') + * ``` + * + * @method htmlSafe + * @for Ember.String + * @static + * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars + */ Ember.String.htmlSafe = function(str) { return new Handlebars.SafeString(str); }; @@ -19979,11 +21089,18 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - See `Ember.String.htmlSafe`. - - @method htmlSafe - @for String - */ + * Mark a string as being safe for unescaped output with Handlebars. + * + * ```javascript + * '
someString
'.htmlSafe() + * ``` + * + * See `Ember.String.htmlSafe`. + * + * @method htmlSafe + * @for String + * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars + */ String.prototype.htmlSafe = function() { return htmlSafe(this); }; @@ -20088,7 +21205,6 @@ var DOMManager = { /** @class _Metamorph @namespace Ember - @extends Ember.Mixin @private */ Ember._Metamorph = Ember.Mixin.create({ @@ -20483,7 +21599,7 @@ var forEach = Ember.ArrayPolyfills.forEach; var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; -function exists(value){ +function exists(value) { return !Ember.isNone(value); } @@ -20508,7 +21624,7 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer var template, context, result = handlebarsGet(currentContext, property, options); - result = valueNormalizer(result); + result = valueNormalizer ? valueNormalizer(result) : result; context = preserveContext ? currentContext : result; if (shouldDisplay(result)) { @@ -20833,7 +21949,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ```javascript AView = Ember.View.extend({ - someProperty: function(){ + someProperty: function() { return "aValue"; }.property() }) @@ -20936,7 +22052,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { var path = attrs[attr], normalized; - Ember.assert(fmt("You must provide a String for a bound attribute, not %@", [path]), typeof path === 'string'); + Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); normalized = normalizePath(ctx, path, options.data); @@ -21122,6 +22238,8 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, var get = Ember.get, set = Ember.set; var EmberHandlebars = Ember.Handlebars; +var LOWERCASE_A_Z = /^[a-z]/; +var VIEW_PREFIX = /^view\./; EmberHandlebars.ViewHelper = Ember.Object.create({ @@ -21233,7 +22351,18 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ newView; if ('string' === typeof path) { - newView = EmberHandlebars.get(thisContext, path, options); + + // TODO: this is a lame conditional, this should likely change + // but something along these lines will likely need to be added + // as deprecation warnings + // + if (options.types[0] === 'STRING' && LOWERCASE_A_Z.test(path) && !VIEW_PREFIX.test(path)) { + Ember.assert("View requires a container", !!data.view.container); + newView = data.view.container.lookupFactory('view:' + path); + } else { + newView = EmberHandlebars.get(thisContext, path, options); + } + Ember.assert("Unable to find view at path '" + path + "'", !!newView); } else { newView = path; @@ -21602,11 +22731,25 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { var hash = options.hash, itemHash = {}, match; // Extract item view class if provided else default to the standard class - var itemViewClass, itemViewPath = hash.itemViewClass; - var collectionPrototype = collectionClass.proto(); + var collectionPrototype = collectionClass.proto(), + itemViewClass; + + if (hash.itemView) { + var controller = data.keywords.controller; + Ember.assert('You specified an itemView, but the current context has no container to look the itemView up in. This probably means that you created a view manually, instead of through the container. Instead, use container.lookup("view:viewName"), which will properly instantiate your view.', controller && controller.container); + var container = controller.container; + itemViewClass = container.resolve('view:' + Ember.String.camelize(hash.itemView)); + Ember.assert('You specified the itemView ' + hash.itemView + ", but it was not found at " + container.describe("view:" + hash.itemView) + " (and it was not registered in the container)", !!itemViewClass); + } else if (hash.itemViewClass) { + itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); + } else { + itemViewClass = collectionPrototype.itemViewClass; + } + + Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); + delete hash.itemViewClass; - itemViewClass = itemViewPath ? handlebarsGet(collectionPrototype, itemViewPath, options) : collectionPrototype.itemViewClass; - Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewPath]), !!itemViewClass); + delete hash.itemView; // Go through options passed to the {{collection}} helper and extract options // that configure item views instead of the collection itself. @@ -21614,7 +22757,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { if (hash.hasOwnProperty(prop)) { match = prop.match(/^item(.)(.*)$/); - if(match && prop !== 'itemController') { + if (match && prop !== 'itemController') { // Convert itemShouldFoo -> shouldFoo itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; // Delete from hash as this will end up getting passed to the @@ -21641,7 +22784,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { } if (emptyViewClass) { hash.emptyView = emptyViewClass; } - if(!hash.keyword){ + if (!hash.keyword) { itemHash._context = Ember.computed.alias('content'); } @@ -21688,7 +22831,7 @@ var handlebarsGet = Ember.Handlebars.get; Ember.Handlebars.registerHelper('unbound', function(property, fn) { var options = arguments[arguments.length - 1], helper, context, out; - if(arguments.length > 2) { + if (arguments.length > 2) { // Unbound helper call. options.data.isUnbound = true; helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helperMissing; @@ -21715,7 +22858,7 @@ Ember.Handlebars.registerHelper('unbound', function(property, fn) { 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 @@ -21746,7 +22889,7 @@ Ember.Handlebars.registerHelper('log', function(property, options) { @for Ember.Handlebars.helpers @param {String} property */ -Ember.Handlebars.registerHelper('debugger', function() { +Ember.Handlebars.registerHelper('debugger', function(options) { debugger; }); @@ -21768,12 +22911,12 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { var binding; if (itemController) { - var controller = Ember.ArrayController.create(); - set(controller, 'itemController', itemController); - set(controller, 'container', get(this, 'controller.container')); - set(controller, '_eachView', this); - set(controller, 'target', get(this, 'controller')); - set(controller, 'parentController', get(this, 'controller')); + var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ + parentController: get(this, 'controller'), + itemController: itemController, + target: get(this, 'controller'), + _eachView: this + }); this.disableContentObservers(function() { set(this, 'content', controller); @@ -21792,6 +22935,11 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { return this._super(); }, + _assertArrayLike: function(content) { + Ember.assert("The value that #each loops over must be an Array. You passed " + content.constructor + ", but it should have been an ArrayController", !Ember.ControllerMixin.detect(content) || (content && content.isGenerated) || content instanceof Ember.ArrayController); + Ember.assert("The value that #each loops over must be an Array. You passed " + ((Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? ("" + content.get('model') + " (wrapped in " + content + ")") : ("" + content)), Ember.Array.detect(content)); + }, + disableContentObservers: function(callback) { Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange'); Ember.removeObserver(this, 'content', null, '_contentDidChange'); @@ -22022,6 +23170,12 @@ GroupedEach.prototype = {
``` + If an `itemViewClass` is defined on the helper, and therefore the helper is not + being used as a block, an `emptyViewClass` can also be provided optionally. + The `emptyViewClass` will match the behavior of the `{{else}}` condition + described above. That is, the `emptyViewClass` will render if the collection + is empty. + ### Representing each item with a Controller. By default the controller lookup within an `{{#each}}` block will be the controller of the template where the `{{#each}}` was used. If each @@ -22035,7 +23189,7 @@ GroupedEach.prototype = { ```javascript App.DeveloperController = Ember.ObjectController.extend({ - isAvailableForHire: function(){ + isAvailableForHire: function() { return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); }.property('isEmployed', 'isSeekingWork') }) @@ -22135,18 +23289,15 @@ Ember.Handlebars.registerHelper('each', function(path, options) { Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('{{user}}'); ``` + @deprecated @method template @for Ember.Handlebars.helpers @param {String} templateName the template to render */ Ember.Handlebars.registerHelper('template', function(name, options) { - var view = options.data.view, - template = view.templateForName(name); - - Ember.assert("Unable to find template with name '"+name+"'.", !!template); - - template(this, { data: options.data }); + Ember.deprecate("The `template` helper has been deprecated in favor of the `partial` helper. Please use `partial` instead, which will work the same way."); + return Ember.Handlebars.helpers.partial.apply(this, arguments); }); })(); @@ -22196,7 +23347,6 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { template = view.templateForName(underscoredName), deprecatedTemplate = !template && view.templateForName(name); - Ember.deprecate("You tried to render the partial " + name + ", which should be at '" + underscoredName + "', but Ember found '" + name + "'. Please use a leading underscore in your partials", template); Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); template = template || deprecatedTemplate; @@ -22268,7 +23418,7 @@ var get = Ember.get, set = Ember.set; @return {String} HTML string */ Ember.Handlebars.registerHelper('yield', function(options) { - var view = options.data.view, template; + var view = options.data.view; while (view && !get(view, 'layout')) { view = get(view, 'parentView'); @@ -22276,9 +23426,39 @@ Ember.Handlebars.registerHelper('yield', function(options) { Ember.assert("You called yield in a template that was not a layout", !!view); - template = get(view, 'template'); + view._yield(this, options); +}); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-handlebars +*/ + +/** + `loc` looks up the string in the localized strings hash. + This is a convenient way to localize text. For example: + + ```html + + ``` + + Take note that `welcome` is a string and not an object + reference. + + @method loc + @for Ember.Handlebars.helpers + @param {String} str The string to format +*/ - if (template) { template(this, options); } +Ember.Handlebars.registerHelper('loc', function(str) { + return Ember.String.loc(str); }); })(); @@ -22348,17 +23528,23 @@ Ember.Checkbox = Ember.View.extend({ tagName: 'input', - attributeBindings: ['type', 'checked', 'disabled', 'tabindex', 'name'], + attributeBindings: ['type', 'checked', 'indeterminate', 'disabled', 'tabindex', 'name'], type: "checkbox", checked: false, disabled: false, + indeterminate: false, init: function() { this._super(); this.on("change", this, this._updateElementValue); }, + didInsertElement: function() { + this._super(); + this.get('element').indeterminate = !!this.get('indeterminate'); + }, + _updateElementValue: function() { set(this, 'checked', this.$().prop('checked')); } @@ -22381,7 +23567,6 @@ var get = Ember.get, set = Ember.set; @class TextSupport @namespace Ember - @extends Ember.Mixin @private */ Ember.TextSupport = Ember.Mixin.create({ @@ -22801,6 +23986,7 @@ var set = Ember.set, get = Ember.get, indexOf = Ember.EnumerableUtils.indexOf, indexesOf = Ember.EnumerableUtils.indexesOf, + forEach = Ember.EnumerableUtils.forEach, replace = Ember.EnumerableUtils.replace, isArray = Ember.isArray, precompileTemplate = Ember.Handlebars.compile; @@ -22854,6 +24040,18 @@ Ember.SelectOption = Ember.View.extend({ }, 'parentView.optionValuePath') }); +Ember.SelectOptgroup = Ember.CollectionView.extend({ + tagName: 'optgroup', + attributeBindings: ['label'], + + selectionBinding: 'parentView.selection', + multipleBinding: 'parentView.multiple', + optionLabelPathBinding: 'parentView.optionLabelPath', + optionValuePathBinding: 'parentView.optionValuePath', + + itemViewClassBinding: 'parentView.optionView' +}); + /** The `Ember.Select` view class renders a [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, @@ -23104,12 +24302,12 @@ Ember.Select = Ember.View.extend( tagName: 'select', classNames: ['ember-select'], defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { -this.compilerInfo = [3,'>= 1.0.0-rc.4']; -helpers = helpers || Ember.Handlebars.helpers; data = data || {}; +this.compilerInfo = [4,'>= 1.0.0']; +helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; var buffer = '', stack1, hashTypes, hashContexts, escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { - + var buffer = '', hashTypes, hashContexts; data.buffer.push("