diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js deleted file mode 100644 index ddcc17a81..000000000 --- a/app/assets/javascripts/admin.js +++ /dev/null @@ -1,2 +0,0 @@ -//= require list_view.js -//= require_tree ./admin diff --git a/app/assets/javascripts/admin.js.erb b/app/assets/javascripts/admin.js.erb new file mode 100644 index 000000000..aa336ccd8 --- /dev/null +++ b/app/assets/javascripts/admin.js.erb @@ -0,0 +1,10 @@ +<% +if Rails.env.development? + require_asset ("development/list-view.js") +else + require_asset ("production/list-view.js") +end + +require_asset("main_include_admin.js") + +%> diff --git a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js index fd3d2cfdf..43d4edeea 100644 --- a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js @@ -46,5 +46,12 @@ Discourse.AdminDashboardController = Ember.Controller.extend({ updatedTimestamp: function() { return moment(this.get('updated_at')).format('LLL'); - }.property('updated_at') + }.property('updated_at'), + + actions: { + refreshProblems: function() { + this.loadProblems(); + } + } + }); diff --git a/app/assets/javascripts/admin/controllers/admin_email_index_controller.js b/app/assets/javascripts/admin/controllers/admin_email_index_controller.js index 8da9fa2d4..3ad472713 100644 --- a/app/assets/javascripts/admin/controllers/admin_email_index_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_email_index_controller.js @@ -24,23 +24,24 @@ Discourse.AdminEmailIndexController = Discourse.Controller.extend({ this.set('sentTestEmail', false); }.observes('testEmailAddress'), + actions: { + /** + Sends a test email to the currently entered email address - /** - Sends a test email to the currently entered email address + @method sendTestEmail + **/ + sendTestEmail: function() { + this.set('sentTestEmail', false); - @method sendTestEmail - **/ - sendTestEmail: function() { - this.set('sentTestEmail', false); - - var adminEmailLogsController = this; - Discourse.ajax("/admin/email/test", { - type: 'POST', - data: { email_address: this.get('testEmailAddress') } - }).then(function () { - adminEmailLogsController.set('sentTestEmail', true); - }); + var adminEmailLogsController = this; + Discourse.ajax("/admin/email/test", { + type: 'POST', + data: { email_address: this.get('testEmailAddress') } + }).then(function () { + adminEmailLogsController.set('sentTestEmail', true); + }); + } } }); diff --git a/app/assets/javascripts/admin/controllers/admin_email_preview_digest_controller.js b/app/assets/javascripts/admin/controllers/admin_email_preview_digest_controller.js index eb54d21c6..171f0094d 100644 --- a/app/assets/javascripts/admin/controllers/admin_email_preview_digest_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_email_preview_digest_controller.js @@ -8,14 +8,21 @@ **/ Discourse.AdminEmailPreviewDigestController = Discourse.ObjectController.extend({ - refresh: function() { - var model = this.get('model'); - var controller = this; - controller.set('loading', true); - Discourse.EmailPreview.findDigest(this.get('lastSeen')).then(function (email) { - model.setProperties(email.getProperties('html_content', 'text_content')); - controller.set('loading', false); - }); + actions: { + refresh: function() { + var model = this.get('model'), + self = this; + + self.set('loading', true); + Discourse.EmailPreview.findDigest(this.get('lastSeen')).then(function (email) { + model.setProperties(email.getProperties('html_content', 'text_content')); + self.set('loading', false); + }); + }, + + toggleShowHtml: function() { + this.toggleProperty('showHtml'); + } } }); diff --git a/app/assets/javascripts/admin/controllers/admin_flags_controller.js b/app/assets/javascripts/admin/controllers/admin_flags_controller.js index c5a7e11d8..46485ffc3 100644 --- a/app/assets/javascripts/admin/controllers/admin_flags_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_flags_controller.js @@ -8,62 +8,64 @@ **/ Discourse.AdminFlagsController = Ember.ArrayController.extend({ - /** - Clear all flags on a post + actions: { + /** + Clear all flags on a post - @method clearFlags - @param {Discourse.FlaggedPost} item The post whose flags we want to clear - **/ - disagreeFlags: function(item) { - var adminFlagsController = this; - item.disagreeFlags().then((function() { - adminFlagsController.removeObject(item); - }), function() { - bootbox.alert(I18n.t("admin.flags.error")); - }); - }, + @method clearFlags + @param {Discourse.FlaggedPost} item The post whose flags we want to clear + **/ + disagreeFlags: function(item) { + var adminFlagsController = this; + item.disagreeFlags().then((function() { + adminFlagsController.removeObject(item); + }), function() { + bootbox.alert(I18n.t("admin.flags.error")); + }); + }, - agreeFlags: function(item) { - var adminFlagsController = this; - item.agreeFlags().then((function() { - adminFlagsController.removeObject(item); - }), function() { - bootbox.alert(I18n.t("admin.flags.error")); - }); - }, + agreeFlags: function(item) { + var adminFlagsController = this; + item.agreeFlags().then((function() { + adminFlagsController.removeObject(item); + }), function() { + bootbox.alert(I18n.t("admin.flags.error")); + }); + }, - deferFlags: function(item) { - var adminFlagsController = this; - item.deferFlags().then((function() { - adminFlagsController.removeObject(item); - }), function() { - bootbox.alert(I18n.t("admin.flags.error")); - }); - }, + deferFlags: function(item) { + var adminFlagsController = this; + item.deferFlags().then((function() { + adminFlagsController.removeObject(item); + }), function() { + bootbox.alert(I18n.t("admin.flags.error")); + }); + }, - /** - Deletes a post + /** + Deletes a post - @method deletePost - @param {Discourse.FlaggedPost} item The post to delete - **/ - deletePost: function(item) { - var adminFlagsController = this; - item.deletePost().then((function() { - adminFlagsController.removeObject(item); - }), function() { - bootbox.alert(I18n.t("admin.flags.error")); - }); - }, + @method deletePost + @param {Discourse.FlaggedPost} item The post to delete + **/ + deletePost: function(item) { + var adminFlagsController = this; + item.deletePost().then((function() { + adminFlagsController.removeObject(item); + }), function() { + bootbox.alert(I18n.t("admin.flags.error")); + }); + }, - /** - Deletes a user and all posts and topics created by that user. + /** + Deletes a user and all posts and topics created by that user. - @method deleteSpammer - @param {Discourse.FlaggedPost} item The post to delete - **/ - deleteSpammer: function(item) { - item.get('user').deleteAsSpammer(function() { window.location.reload(); }); + @method deleteSpammer + @param {Discourse.FlaggedPost} item The post to delete + **/ + deleteSpammer: function(item) { + item.get('user').deleteAsSpammer(function() { window.location.reload(); }); + } }, /** diff --git a/app/assets/javascripts/admin/controllers/admin_groups_controller.js b/app/assets/javascripts/admin/controllers/admin_groups_controller.js index 8fb3ae9a8..e54ae33d2 100644 --- a/app/assets/javascripts/admin/controllers/admin_groups_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_groups_controller.js @@ -1,49 +1,51 @@ Discourse.AdminGroupsController = Ember.Controller.extend({ itemController: 'adminGroup', - edit: function(group){ - this.get('model').select(group); - group.load(); - }, + actions: { + edit: function(group){ + this.get('model').select(group); + group.load(); + }, - refreshAutoGroups: function(){ - var controller = this; + refreshAutoGroups: function(){ + var self = this; - this.set('refreshingAutoGroups', true); - Discourse.ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}) - .then(function() { - controller.set('model', Discourse.Group.findAll()); - controller.set('refreshingAutoGroups', false); - }); - }, + self.set('refreshingAutoGroups', true); + Discourse.ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(function() { + self.set('model', Discourse.Group.findAll()); + self.set('refreshingAutoGroups', false); + }); + }, - newGroup: function(){ - var group = Discourse.Group.create(); - group.set("loaded", true); - var model = this.get("model"); - model.addObject(group); - model.select(group); - }, + newGroup: function(){ + var group = Discourse.Group.create(); + group.set("loaded", true); + var model = this.get("model"); + model.addObject(group); + model.select(group); + }, - save: function(group){ - if(!group.get("id")){ - group.create(); - } else { - group.save(); - } - }, - - destroy: function(group){ - var _this = this; - return bootbox.confirm(I18n.t("admin.groups.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { - if (result) { - group.destroy().then(function(deleted) { - if (deleted) { - _this.get("model").removeObject(group); - } - }); + save: function(group){ + if(!group.get("id")){ + group.create(); + } else { + group.save(); } - }); + }, + + destroy: function(group){ + var self = this; + return bootbox.confirm(I18n.t("admin.groups.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { + if (result) { + group.destroy().then(function(deleted) { + if (deleted) { + self.get("model").removeObject(group); + } + }); + } + }); + } } + }); diff --git a/app/assets/javascripts/admin/controllers/admin_reports_controller.js b/app/assets/javascripts/admin/controllers/admin_reports_controller.js index 14c15402c..35fb54b74 100644 --- a/app/assets/javascripts/admin/controllers/admin_reports_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_reports_controller.js @@ -4,14 +4,16 @@ Discourse.AdminReportsController = Ember.ObjectController.extend({ viewingTable: Em.computed.equal('viewMode', 'table'), viewingBarChart: Em.computed.equal('viewMode', 'barChart'), - // Changes the current view mode to 'table' - viewAsTable: function() { - this.set('viewMode', 'table'); - }, + actions: { + // Changes the current view mode to 'table' + viewAsTable: function() { + this.set('viewMode', 'table'); + }, - // Changes the current view mode to 'barChart' - viewAsBarChart: function() { - this.set('viewMode', 'barChart'); + // Changes the current view mode to 'barChart' + viewAsBarChart: function() { + this.set('viewMode', 'barChart'); + } } }); \ No newline at end of file diff --git a/app/assets/javascripts/admin/controllers/admin_site_content_edit_controller.js b/app/assets/javascripts/admin/controllers/admin_site_content_edit_controller.js index dfaebeb04..68e5b6d3a 100644 --- a/app/assets/javascripts/admin/controllers/admin_site_content_edit_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_site_content_edit_controller.js @@ -14,12 +14,15 @@ Discourse.AdminSiteContentEditController = Discourse.Controller.extend({ return false; }.property('saving', 'content.content'), - saveChanges: function() { - var controller = this; - controller.setProperties({saving: true, saved: false}); - this.get('content').save().then(function () { - controller.setProperties({saving: false, saved: true}); - }); + actions: { + saveChanges: function() { + var self = this; + self.setProperties({saving: true, saved: false}); + self.get('content').save().then(function () { + self.setProperties({saving: false, saved: true}); + }); + } } +}); -}); \ No newline at end of file +Discourse.AdminSiteContentsController = Ember.ArrayController.extend({}); \ No newline at end of file diff --git a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js index 075406e88..769865e13 100644 --- a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js @@ -39,35 +39,37 @@ Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.P }); }.property('filter', 'content.@each', 'onlyOverridden'), - /** - Reset a setting to its default value + actions: { + /** + Reset a setting to its default value - @method resetDefault - @param {Discourse.SiteSetting} setting The setting we want to revert - **/ - resetDefault: function(setting) { - setting.set('value', setting.get('default')); - setting.save(); - }, + @method resetDefault + @param {Discourse.SiteSetting} setting The setting we want to revert + **/ + resetDefault: function(setting) { + setting.set('value', setting.get('default')); + setting.save(); + }, - /** - Save changes to a site setting + /** + Save changes to a site setting - @method save - @param {Discourse.SiteSetting} setting The setting we've changed - **/ - save: function(setting) { - setting.save(); - }, + @method save + @param {Discourse.SiteSetting} setting The setting we've changed + **/ + save: function(setting) { + setting.save(); + }, - /** - Cancel changes to a site setting + /** + Cancel changes to a site setting - @method cancel - @param {Discourse.SiteSetting} setting The setting we've changed but want to revert - **/ - cancel: function(setting) { - setting.resetValue(); + @method cancel + @param {Discourse.SiteSetting} setting The setting we've changed but want to revert + **/ + cancel: function(setting) { + setting.resetValue(); + } } }); diff --git a/app/assets/javascripts/admin/controllers/admin_user_controller.js b/app/assets/javascripts/admin/controllers/admin_user_controller.js index d2c0e83c4..986e5129c 100644 --- a/app/assets/javascripts/admin/controllers/admin_user_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_user_controller.js @@ -9,22 +9,25 @@ Discourse.AdminUserController = Discourse.ObjectController.extend({ editingTitle: false, - toggleTitleEdit: function() { - this.set('editingTitle', !this.editingTitle); - }, - - saveTitle: function() { - Discourse.ajax("/users/" + this.get('username').toLowerCase(), { - data: {title: this.get('title')}, - type: 'PUT' - }).then(null, function(e){ - bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body})); - }); - - this.toggleTitleEdit(); - }, - showApproval: function() { return Discourse.SiteSettings.must_approve_users; - }.property() + }.property(), + + actions: { + toggleTitleEdit: function() { + this.toggleProperty('editingTitle'); + }, + + saveTitle: function() { + Discourse.ajax("/users/" + this.get('username').toLowerCase(), { + data: {title: this.get('title')}, + type: 'PUT' + }).then(null, function(e){ + bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body})); + }); + + this.send('toggleTitleEdit'); + } + } + }); diff --git a/app/assets/javascripts/admin/routes/admin_logs_routes.js b/app/assets/javascripts/admin/routes/admin_logs_routes.js index cceb94b3f..b989b0686 100644 --- a/app/assets/javascripts/admin/routes/admin_logs_routes.js +++ b/app/assets/javascripts/admin/routes/admin_logs_routes.js @@ -33,7 +33,7 @@ Discourse.AdminLogsStaffActionLogsRoute = Discourse.Route.extend({ return controller.show(); }, - events: { + actions: { showDetailsModal: function(logRecord) { Discourse.Route.showModal(this, 'admin_staff_action_log_details', logRecord); this.controllerFor('modal').set('modalClass', 'log-details-modal'); diff --git a/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js b/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js index 90465427d..b654c6383 100644 --- a/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js +++ b/app/assets/javascripts/admin/routes/admin_site_content_edit_route.js @@ -13,24 +13,11 @@ Discourse.AdminSiteContentEditRoute = Discourse.Route.extend({ }, model: function(params) { - var list = this.controllerFor('adminSiteContents').get('model'); + var list = Discourse.SiteContentType.findAll(); - // ember routing is fun ... this is what happens - // - // linkTo creates an Ember.LinkView , it marks an <a> with the class "active" - // if the "context" of this dynamic route is equal to the model in the linkTo - // the route "context" is set here, so we want to make sure we have the exact - // same object, from Ember we have: - // - // if (handlerInfo.context !== object) { return false; } - // - // we could avoid this hack if Ember just compared .serialize(model) with .serialize(context) - // - // alternatively we could use some sort of identity map - // - // see also: https://github.com/emberjs/ember.js/issues/3005 - - return list.findProperty("content_type", params.content_type); + return list.then(function(items) { + return items.findProperty("content_type", params.content_type); + }); }, renderTemplate: function() { diff --git a/app/assets/javascripts/admin/templates/admin.js.handlebars b/app/assets/javascripts/admin/templates/admin.js.handlebars index 105b66316..5a09dd52f 100644 --- a/app/assets/javascripts/admin/templates/admin.js.handlebars +++ b/app/assets/javascripts/admin/templates/admin.js.handlebars @@ -3,21 +3,21 @@ <div class="full-width"> <ul class="nav nav-pills"> - <li>{{#linkTo 'admin.dashboard'}}{{i18n admin.dashboard.title}}{{/linkTo}}</li> + <li>{{#link-to 'admin.dashboard'}}{{i18n admin.dashboard.title}}{{/link-to}}</li> {{#if currentUser.admin}} - <li>{{#linkTo 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/linkTo}}</li> - <li>{{#linkTo 'adminSiteContents'}}{{i18n admin.site_content.title}}{{/linkTo}}</li> + <li>{{#link-to 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/link-to}}</li> + <li>{{#link-to 'adminSiteContents'}}{{i18n admin.site_content.title}}{{/link-to}}</li> {{/if}} - <li>{{#linkTo 'adminUsersList'}}{{i18n admin.users.title}}{{/linkTo}}</li> + <li>{{#link-to 'adminUsersList'}}{{i18n admin.users.title}}{{/link-to}}</li> {{#if currentUser.admin}} - <li>{{#linkTo 'admin.groups'}}{{i18n admin.groups.title}}{{/linkTo}}</li> + <li>{{#link-to 'admin.groups'}}{{i18n admin.groups.title}}{{/link-to}}</li> {{/if}} - <li>{{#linkTo 'adminEmail'}}{{i18n admin.email.title}}{{/linkTo}}</li> - <li>{{#linkTo 'adminFlags'}}{{i18n admin.flags.title}}{{/linkTo}}</li> - <li>{{#linkTo 'adminLogs'}}{{i18n admin.logs.title}}{{/linkTo}}</li> + <li>{{#link-to 'adminEmail'}}{{i18n admin.email.title}}{{/link-to}}</li> + <li>{{#link-to 'adminFlags'}}{{i18n admin.flags.title}}{{/link-to}}</li> + <li>{{#link-to 'adminLogs'}}{{i18n admin.logs.title}}{{/link-to}}</li> {{#if currentUser.admin}} - <li>{{#linkTo 'admin.customize'}}{{i18n admin.customize.title}}{{/linkTo}}</li> - <li>{{#linkTo 'admin.api'}}{{i18n admin.api.title}}{{/linkTo}}</li> + <li>{{#link-to 'admin.customize'}}{{i18n admin.customize.title}}{{/link-to}}</li> + <li>{{#link-to 'admin.api'}}{{i18n admin.api.title}}{{/link-to}}</li> {{/if}} </ul> diff --git a/app/assets/javascripts/admin/templates/dashboard.js.handlebars b/app/assets/javascripts/admin/templates/dashboard.js.handlebars index e3e57f9e7..621d355ee 100644 --- a/app/assets/javascripts/admin/templates/dashboard.js.handlebars +++ b/app/assets/javascripts/admin/templates/dashboard.js.handlebars @@ -13,7 +13,7 @@ </p> <p class="actions"> <small>{{i18n admin.dashboard.last_checked}}: {{problemsTimestamp}}</small> - <button {{action loadProblems}} class="btn btn-small"><i class="icon icon-refresh"></i>{{i18n admin.dashboard.refresh_problems}}</button> + <button {{action refreshProblems}} class="btn btn-small"><i class="icon icon-refresh"></i>{{i18n admin.dashboard.refresh_problems}}</button> </p> </div> <div class="clearfix"></div> @@ -25,7 +25,7 @@ <div class="problem-messages"> <p> {{i18n admin.dashboard.no_problems}} - <button {{action loadProblems}} class="btn btn-small"><i class="icon icon-refresh"></i>{{i18n admin.dashboard.refresh_problems}}</button> + <button {{action refreshProblems}} class="btn btn-small"><i class="icon icon-refresh"></i>{{i18n admin.dashboard.refresh_problems}}</button> </p> </div> <div class="clearfix"></div> @@ -121,15 +121,15 @@ <table> <tr> <td class="title"><i class='icon icon-trophy'></i> {{i18n admin.dashboard.admins}}</td> - <td class="value">{{#linkTo 'adminUsersList.admins'}}{{admins}}{{/linkTo}}</td> + <td class="value">{{#link-to 'adminUsersList.admins'}}{{admins}}{{/link-to}}</td> <td class="title"><i class='icon icon-ban-circle'></i> {{i18n admin.dashboard.banned}}</td> - <td class="value">{{#linkTo 'adminUsersList.banned'}}{{banned}}{{/linkTo}}</td> + <td class="value">{{#link-to 'adminUsersList.banned'}}{{banned}}{{/link-to}}</td> </tr> <tr> <td class="title"><i class='icon icon-magic'></i> {{i18n admin.dashboard.moderators}}</td> - <td class="value">{{#linkTo 'adminUsersList.moderators'}}{{moderators}}{{/linkTo}}</td> + <td class="value">{{#link-to 'adminUsersList.moderators'}}{{moderators}}{{/link-to}}</td> <td class="title"><i class='icon icon-ban-circle'></i> {{i18n admin.dashboard.blocked}}</td> - <td class="value">{{#linkTo 'adminUsersList.blocked'}}{{blocked}}{{/linkTo}}</td> + <td class="value">{{#link-to 'adminUsersList.blocked'}}{{blocked}}{{/link-to}}</td> </tr> </table> </div> @@ -265,7 +265,7 @@ {{#each top_referrers.data}} <tbody> <tr> - <td class="title">{{#linkTo 'adminUser' this}}{{unbound username}}{{/linkTo}}</td> + <td class="title">{{#link-to 'adminUser' this}}{{unbound username}}{{/link-to}}</td> <td class="value">{{num_clicks}}</td> <td class="value">{{num_topics}}</td> </tr> diff --git a/app/assets/javascripts/admin/templates/email.js.handlebars b/app/assets/javascripts/admin/templates/email.js.handlebars index 4239864bd..8c6b4aa3b 100644 --- a/app/assets/javascripts/admin/templates/email.js.handlebars +++ b/app/assets/javascripts/admin/templates/email.js.handlebars @@ -1,9 +1,9 @@ <div class='admin-controls'> <div class='span15'> <ul class="nav nav-pills"> - <li>{{#linkTo 'adminEmail.index'}}{{i18n admin.email.settings}}{{/linkTo}}</li> - <li>{{#linkTo 'adminEmail.logs'}}{{i18n admin.email.logs}}{{/linkTo}}</li> - <li>{{#linkTo 'adminEmail.previewDigest'}}{{i18n admin.email.preview_digest}}{{/linkTo}}</li> + <li>{{#link-to 'adminEmail.index'}}{{i18n admin.email.settings}}{{/link-to}}</li> + <li>{{#link-to 'adminEmail.logs'}}{{i18n admin.email.logs}}{{/link-to}}</li> + <li>{{#link-to 'adminEmail.previewDigest'}}{{i18n admin.email.preview_digest}}{{/link-to}}</li> </ul> </div> </div> diff --git a/app/assets/javascripts/admin/templates/email_logs.js.handlebars b/app/assets/javascripts/admin/templates/email_logs.js.handlebars index f937ee6bc..508f72566 100644 --- a/app/assets/javascripts/admin/templates/email_logs.js.handlebars +++ b/app/assets/javascripts/admin/templates/email_logs.js.handlebars @@ -15,8 +15,8 @@ <td>{{date created_at}}</td> <td> {{#if user}} - {{#linkTo 'adminUser' user}}{{avatar user imageSize="tiny"}}{{/linkTo}} - {{#linkTo 'adminUser' user}}{{user.username}}{{/linkTo}} + {{#link-to 'adminUser' user}}{{avatar user imageSize="tiny"}}{{/link-to}} + {{#link-to 'adminUser' user}}{{user.username}}{{/link-to}} {{else}} — {{/if}} diff --git a/app/assets/javascripts/admin/templates/email_preview_digest.js.handlebars b/app/assets/javascripts/admin/templates/email_preview_digest.js.handlebars index aaa5bb3cc..c0dee55e0 100644 --- a/app/assets/javascripts/admin/templates/email_preview_digest.js.handlebars +++ b/app/assets/javascripts/admin/templates/email_preview_digest.js.handlebars @@ -11,9 +11,9 @@ <div class="span7 toggle"> <label>{{i18n admin.email.format}}</label> {{#if showHtml}} - <span>{{i18n admin.email.html}}</span> | <a href='#' {{action toggleProperty 'showHtml'}}>{{i18n admin.email.text}}</a> + <span>{{i18n admin.email.html}}</span> | <a href='#' {{action toggleShowHtml}}>{{i18n admin.email.text}}</a> {{else}} - <a href='#' {{action toggleProperty 'showHtml'}}>{{i18n admin.email.html}}</a> | <span>{{i18n admin.email.text}}</span> + <a href='#' {{action toggleShowHtml}}>{{i18n admin.email.html}}</a> | <span>{{i18n admin.email.text}}</span> {{/if}} </div> </div> diff --git a/app/assets/javascripts/admin/templates/flags.js.handlebars b/app/assets/javascripts/admin/templates/flags.js.handlebars index fc94328f0..2138a4ac3 100644 --- a/app/assets/javascripts/admin/templates/flags.js.handlebars +++ b/app/assets/javascripts/admin/templates/flags.js.handlebars @@ -1,8 +1,8 @@ <div class='admin-controls'> <div class='span15'> <ul class="nav nav-pills"> - <li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.active}}{{/linkTo}}</li> - <li>{{#linkTo 'adminFlags.old'}}{{i18n admin.flags.old}}{{/linkTo}}</li> + <li>{{#link-to 'adminFlags.active'}}{{i18n admin.flags.active}}{{/link-to}}</li> + <li>{{#link-to 'adminFlags.old'}}{{i18n admin.flags.old}}{{/link-to}}</li> </ul> </div> </div> @@ -25,7 +25,7 @@ {{#each flaggedPost in content}} <tr {{bindAttr class="flaggedPost.extraClasses"}}> - <td class='user'>{{#if flaggedPost.user}}{{#linkTo 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/linkTo}}{{/if}}</td> + <td class='user'>{{#if flaggedPost.user}}{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}}{{/if}}</td> <td class='excerpt'>{{#if flaggedPost.topicHidden}}<i title='{{i18n topic_statuses.invisible.help}}' class='icon icon-eye-close'></i> {{/if}}<h3><a href='{{unbound flaggedPost.url}}'>{{flaggedPost.title}}</a></h3><br>{{{flaggedPost.excerpt}}} </td> @@ -35,7 +35,7 @@ {{#each flaggedPost.flaggers}} <tr> <td> - {{#linkTo 'adminUser' this.user}}{{avatar this.user imageSize="small"}} {{/linkTo}} + {{#link-to 'adminUser' this.user}}{{avatar this.user imageSize="small"}} {{/link-to}} </td> <td> {{date this.flaggedAt}} @@ -54,7 +54,7 @@ <tr> <td></td> <td class='message'> - <div>{{#linkTo 'adminUser' user}}{{avatar user imageSize="small"}}{{/linkTo}} {{message}} <a href="{{unbound permalink}}"><button class='btn'><i class="icon-reply"></i> {{i18n admin.flags.view_message}}</button></a></div> + <div>{{#link-to 'adminUser' user}}{{avatar user imageSize="small"}}{{/link-to}} {{message}} <a href="{{unbound permalink}}"><button class='btn'><i class="icon-reply"></i> {{i18n admin.flags.view_message}}</button></a></div> </td> <td></td> <td></td> diff --git a/app/assets/javascripts/admin/templates/logs.js.handlebars b/app/assets/javascripts/admin/templates/logs.js.handlebars index f1faac2b6..5e6af379d 100644 --- a/app/assets/javascripts/admin/templates/logs.js.handlebars +++ b/app/assets/javascripts/admin/templates/logs.js.handlebars @@ -1,9 +1,9 @@ <div class='admin-controls'> <div class='span15'> <ul class="nav nav-pills"> - <li>{{#linkTo 'adminLogs.staffActionLogs'}}{{i18n admin.logs.staff_actions.title}}{{/linkTo}}</li> - <li>{{#linkTo 'adminLogs.screenedEmails'}}{{i18n admin.logs.screened_emails.title}}{{/linkTo}}</li> - <li>{{#linkTo 'adminLogs.screenedUrls'}}{{i18n admin.logs.screened_urls.title}}{{/linkTo}}</li> + <li>{{#link-to 'adminLogs.staffActionLogs'}}{{i18n admin.logs.staff_actions.title}}{{/link-to}}</li> + <li>{{#link-to 'adminLogs.screenedEmails'}}{{i18n admin.logs.screened_emails.title}}{{/link-to}}</li> + <li>{{#link-to 'adminLogs.screenedUrls'}}{{i18n admin.logs.screened_urls.title}}{{/link-to}}</li> </ul> </div> </div> diff --git a/app/assets/javascripts/admin/templates/logs/staff_action_logs_list_item.js.handlebars b/app/assets/javascripts/admin/templates/logs/staff_action_logs_list_item.js.handlebars index 749f31970..c807003e6 100644 --- a/app/assets/javascripts/admin/templates/logs/staff_action_logs_list_item.js.handlebars +++ b/app/assets/javascripts/admin/templates/logs/staff_action_logs_list_item.js.handlebars @@ -1,5 +1,5 @@ <div class="col value first staff_user"> - {{#linkTo 'adminUser' acting_user}}{{avatar acting_user imageSize="tiny"}}{{/linkTo}} + {{#link-to 'adminUser' acting_user}}{{avatar acting_user imageSize="tiny"}}{{/link-to}} <a {{action filterByStaffUser acting_user}}>{{acting_user.username}}</a> </div> <div class="col value action"> @@ -7,7 +7,7 @@ </div> <div class="col value subject"> {{#if target_user}} - {{#linkTo 'adminUser' target_user}}{{avatar target_user imageSize="tiny"}}{{/linkTo}} + {{#link-to 'adminUser' target_user}}{{avatar target_user imageSize="tiny"}}{{/link-to}} <a {{action filterByTargetUser target_user}}>{{target_user.username}}</a> {{/if}} {{#if subject}} diff --git a/app/assets/javascripts/admin/templates/reports/trust_levels_report.js.handlebars b/app/assets/javascripts/admin/templates/reports/trust_levels_report.js.handlebars index efd9dece0..a16c7883d 100644 --- a/app/assets/javascripts/admin/templates/reports/trust_levels_report.js.handlebars +++ b/app/assets/javascripts/admin/templates/reports/trust_levels_report.js.handlebars @@ -1,8 +1,8 @@ <tr> <td class="title">{{title}}</td> - <td class="value">{{#linkTo 'adminUsersList.newuser'}}{{valueAtTrustLevel data 0}}{{/linkTo}}</td> - <td class="value">{{#linkTo 'adminUsersList.basic'}}{{valueAtTrustLevel data 1}}{{/linkTo}}</td> - <td class="value">{{#linkTo 'adminUsersList.regular'}}{{valueAtTrustLevel data 2}}{{/linkTo}}</td> - <td class="value">{{#linkTo 'adminUsersList.leaders'}}{{valueAtTrustLevel data 3}}{{/linkTo}}</td> - <td class="value">{{#linkTo 'adminUsersList.elders'}}{{valueAtTrustLevel data 4}}{{/linkTo}}</td> + <td class="value">{{#link-to 'adminUsersList.newuser'}}{{valueAtTrustLevel data 0}}{{/link-to}}</td> + <td class="value">{{#link-to 'adminUsersList.basic'}}{{valueAtTrustLevel data 1}}{{/link-to}}</td> + <td class="value">{{#link-to 'adminUsersList.regular'}}{{valueAtTrustLevel data 2}}{{/link-to}}</td> + <td class="value">{{#link-to 'adminUsersList.leaders'}}{{valueAtTrustLevel data 3}}{{/link-to}}</td> + <td class="value">{{#link-to 'adminUsersList.elders'}}{{valueAtTrustLevel data 4}}{{/link-to}}</td> </tr> \ No newline at end of file diff --git a/app/assets/javascripts/admin/templates/site_contents.js.handlebars b/app/assets/javascripts/admin/templates/site_contents.js.handlebars index 970b88943..e6b86a54b 100644 --- a/app/assets/javascripts/admin/templates/site_contents.js.handlebars +++ b/app/assets/javascripts/admin/templates/site_contents.js.handlebars @@ -4,7 +4,7 @@ <ul> {{#each type in model}} <li> - {{#linkTo 'adminSiteContentEdit' type}}{{type.title}}{{/linkTo}} + {{#link-to 'adminSiteContentEdit' type}}{{type.title}}{{/link-to}} </li> {{/each}} </ul> diff --git a/app/assets/javascripts/admin/templates/user.js.handlebars b/app/assets/javascripts/admin/templates/user.js.handlebars index 08979e4f6..ab274137d 100644 --- a/app/assets/javascripts/admin/templates/user.js.handlebars +++ b/app/assets/javascripts/admin/templates/user.js.handlebars @@ -4,10 +4,10 @@ <div class='field'>{{i18n user.username.title}}</div> <div class='value'>{{username}}</div> <div class='controls'> - {{#linkTo 'userActivity' class="btn"}} + {{#link-to 'userActivity' class="btn"}} <i class='icon icon-user'></i> {{i18n admin.user.show_public_profile}} - {{/linkTo}} + {{/link-to}} {{#if can_impersonate}} <button class='btn' {{action impersonate target="content"}}> <i class='icon icon-screenshot'></i> @@ -71,8 +71,8 @@ {{#if approved}} {{i18n admin.user.approved_by}} - {{#linkTo 'adminUser' approved_by}}{{avatar approved_by imageSize="small"}}{{/linkTo}} - {{#linkTo 'adminUser' approved_by}}{{approved_by.username}}{{/linkTo}} + {{#link-to 'adminUser' approved_by}}{{avatar approved_by imageSize="small"}}{{/link-to}} + {{#link-to 'adminUser' approved_by}}{{approved_by.username}}{{/link-to}} {{else}} {{i18n no_value}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/users_list.js.handlebars b/app/assets/javascripts/admin/templates/users_list.js.handlebars index f08e54b60..5eed2a5f6 100644 --- a/app/assets/javascripts/admin/templates/users_list.js.handlebars +++ b/app/assets/javascripts/admin/templates/users_list.js.handlebars @@ -1,15 +1,15 @@ <div class='admin-controls'> <div class='span15'> <ul class="nav nav-pills"> - <li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.nav.active}}{{/linkTo}}</li> - <li>{{#linkTo 'adminUsersList.new'}}{{i18n admin.users.nav.new}}{{/linkTo}}</li> + <li>{{#link-to 'adminUsersList.active'}}{{i18n admin.users.nav.active}}{{/link-to}}</li> + <li>{{#link-to 'adminUsersList.new'}}{{i18n admin.users.nav.new}}{{/link-to}}</li> {{#if Discourse.SiteSettings.must_approve_users}} - <li>{{#linkTo 'adminUsersList.pending'}}{{i18n admin.users.nav.pending}}{{/linkTo}}</li> + <li>{{#link-to 'adminUsersList.pending'}}{{i18n admin.users.nav.pending}}{{/link-to}}</li> {{/if}} - <li>{{#linkTo 'adminUsersList.admins'}}{{i18n admin.users.nav.admins}}{{/linkTo}}</li> - <li>{{#linkTo 'adminUsersList.moderators'}}{{i18n admin.users.nav.moderators}}{{/linkTo}}</li> - <li>{{#linkTo 'adminUsersList.banned'}}{{i18n admin.users.nav.banned}}{{/linkTo}}</li> - <li>{{#linkTo 'adminUsersList.blocked'}}{{i18n admin.users.nav.blocked}}{{/linkTo}}</li> + <li>{{#link-to 'adminUsersList.admins'}}{{i18n admin.users.nav.admins}}{{/link-to}}</li> + <li>{{#link-to 'adminUsersList.moderators'}}{{i18n admin.users.nav.moderators}}{{/link-to}}</li> + <li>{{#link-to 'adminUsersList.banned'}}{{i18n admin.users.nav.banned}}{{/link-to}}</li> + <li>{{#link-to 'adminUsersList.blocked'}}{{i18n admin.users.nav.blocked}}{{/link-to}}</li> </ul> </div> <div class='span5 username controls'> @@ -61,8 +61,8 @@ {{/if}} </td> {{/if}} - <td>{{#linkTo 'adminUser' this}}{{avatar this imageSize="small"}}{{/linkTo}}</td> - <td>{{#linkTo 'adminUser' this}}{{unbound username}}{{/linkTo}}</td> + <td>{{#link-to 'adminUser' this}}{{avatar this imageSize="small"}}{{/link-to}}</td> + <td>{{#link-to 'adminUser' this}}{{unbound username}}{{/link-to}}</td> <td>{{shorten email}}</td> <td>{{{unbound last_emailed_age}}}</td> <td>{{{unbound last_seen_age}}}</td> diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 35acdbfee..d81c2f245 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -13,18 +13,18 @@ <% if Rails.env.development? - require_asset ("./external_development/jquery-2.0.3.js") + require_asset ("development/jquery-2.0.3.js") else - require_asset ("./external_production/jquery-2.0.3.min.js") + require_asset ("production/jquery-2.0.3.min.js") end -require_asset ("./external/jquery.ui.widget.js") -require_asset ("./external/handlebars.js") +require_asset ("jquery.ui.widget.js") +require_asset ("handlebars.js") if Rails.env.development? - require_asset ("./external_development/ember.js") + require_asset ("development/ember.js") else - require_asset ("./external_production/ember.js") + require_asset ("production/ember.js") end require_asset ("./main_include.js") diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 558a06710..8927da3ac 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -28,7 +28,7 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { return u + url; }, - resolver: Discourse.Resolver, + Resolver: Discourse.Resolver, titleChanged: function() { var title = ""; @@ -112,7 +112,7 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { if ($currentTarget.attr('target')) { return; } if ($currentTarget.data('auto-route')) { return; } - // If it's an ember #linkTo skip it + // If it's an ember #link-to skip it if ($currentTarget.hasClass('ember-view')) { return; } if ($currentTarget.hasClass('lightbox')) { return; } diff --git a/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js b/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js index 91ce9648b..c3564b15a 100644 --- a/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js +++ b/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js @@ -8,15 +8,19 @@ @module Discourse **/ Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, { - useUploadedAvatar: function() { - this.set("use_uploaded_avatar", true); - }, - useGravatar: function() { - this.set("use_uploaded_avatar", false); + actions: { + useUploadedAvatar: function() { + this.set("use_uploaded_avatar", true); + }, + + useGravatar: function() { + this.set("use_uploaded_avatar", false); + } }, avatarTemplate: function() { return this.get("use_uploaded_avatar") ? this.get("uploaded_avatar_template") : this.get("gravatar_template"); }.property("use_uploaded_avatar", "uploaded_avatar_template", "gravatar_template") + }); diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js b/app/assets/javascripts/discourse/controllers/composer_controller.js index e040f094d..25158965a 100644 --- a/app/assets/javascripts/discourse/controllers/composer_controller.js +++ b/app/assets/javascripts/discourse/controllers/composer_controller.js @@ -17,15 +17,6 @@ Discourse.ComposerController = Discourse.Controller.extend({ this.set('similarTopics', Em.A()); }, - togglePreview: function() { - this.get('model').togglePreview(); - }, - - // Import a quote from the post - importQuote: function() { - this.get('model').importQuote(); - }, - updateDraftStatus: function() { this.get('model').updateDraftStatus(); }, @@ -39,84 +30,122 @@ Discourse.ComposerController = Discourse.Controller.extend({ return Discourse.Category.list(); }.property(), - save: function(force) { - var composer = this.get('model'), - composerController = this; + actions: { - if( composer.get('cantSubmitPost') ) { - this.set('view.showTitleTip', Date.now()); - this.set('view.showCategoryTip', Date.now()); - this.set('view.showReplyTip', Date.now()); - return; - } + // Toggle the reply view + toggle: function() { + this.closeAutocomplete(); + switch (this.get('model.composeState')) { + case Discourse.Composer.OPEN: + if (this.blank('model.reply') && this.blank('model.title')) { + this.close(); + } else { + this.shrink(); + } + break; + case Discourse.Composer.DRAFT: + this.set('model.composeState', Discourse.Composer.OPEN); + break; + case Discourse.Composer.SAVING: + this.close(); + } + return false; + }, - composer.set('disableDrafts', true); - // for now handle a very narrow use case - // if we are replying to a topic AND not on the topic pop the window up - if(!force && composer.get('replyingToTopic')) { - var topic = this.get('topic'); - if (!topic || topic.get('id') !== composer.get('topic.id')) - { - var message = I18n.t("composer.posting_not_on_topic", {title: this.get('model.topic.title')}); + togglePreview: function() { + this.get('model').togglePreview(); + }, - var buttons = [{ - "label": I18n.t("composer.cancel"), - "class": "cancel", - "link": true - }]; + // Import a quote from the post + importQuote: function() { + this.get('model').importQuote(); + }, + + cancel: function() { + this.cancelComposer(); + }, + + save: function(force) { + var composer = this.get('model'), + composerController = this; + + if( composer.get('cantSubmitPost') ) { + this.set('view.showTitleTip', Date.now()); + this.set('view.showCategoryTip', Date.now()); + this.set('view.showReplyTip', Date.now()); + return; + } + + composer.set('disableDrafts', true); + + // for now handle a very narrow use case + // if we are replying to a topic AND not on the topic pop the window up + if(!force && composer.get('replyingToTopic')) { + var topic = this.get('topic'); + if (!topic || topic.get('id') !== composer.get('topic.id')) + { + var message = I18n.t("composer.posting_not_on_topic", {title: this.get('model.topic.title')}); + + var buttons = [{ + "label": I18n.t("composer.cancel"), + "class": "cancel", + "link": true + }]; + + if(topic) { + buttons.push({ + "label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + topic.get('title') + "</div>", + "class": "btn btn-reply-here", + "callback": function(){ + composer.set('topic', topic); + composer.set('post', null); + composerController.save(true); + } + }); + } - if(topic) { buttons.push({ - "label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + topic.get('title') + "</div>", - "class": "btn btn-reply-here", + "label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + this.get('model.topic.title') + "</div>", + "class": "btn-primary btn-reply-on-original", "callback": function(){ - composer.set('topic', topic); - composer.set('post', null); composerController.save(true); } }); + + bootbox.dialog(message, buttons, {"classes": "reply-where-modal"}); + return; + } + } + + return composer.save({ + imageSizes: this.get('view').imageSizes() + }).then(function(opts) { + + // If we replied as a new topic successfully, remove the draft. + if (composerController.get('replyAsNewTopicDraft')) { + composerController.destroyDraft(); } - buttons.push({ - "label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + this.get('model.topic.title') + "</div>", - "class": "btn-primary btn-reply-on-original", - "callback": function(){ - composerController.save(true); - } - }); + opts = opts || {}; + composerController.close(); - bootbox.dialog(message, buttons, {"classes": "reply-where-modal"}); - return; - } + var currentUser = Discourse.User.current(); + if (composer.get('creatingTopic')) { + currentUser.set('topic_count', currentUser.get('topic_count') + 1); + } else { + currentUser.set('reply_count', currentUser.get('reply_count') + 1); + } + Discourse.URL.routeTo(opts.post.get('url')); + + }, function(error) { + composer.set('disableDrafts', false); + bootbox.alert(error); + }); } - - return composer.save({ - imageSizes: this.get('view').imageSizes() - }).then(function(opts) { - - // If we replied as a new topic successfully, remove the draft. - if (composerController.get('replyAsNewTopicDraft')) { - composerController.destroyDraft(); - } - - opts = opts || {}; - composerController.close(); - - var currentUser = Discourse.User.current(); - if (composer.get('creatingTopic')) { - currentUser.set('topic_count', currentUser.get('topic_count') + 1); - } else { - currentUser.set('reply_count', currentUser.get('reply_count') + 1); - } - Discourse.URL.routeTo(opts.post.get('url')); - - }, function(error) { - composer.set('disableDrafts', false); - bootbox.alert(error); - }); }, + /** Checks to see if a reply has been typed. This is signaled by a keyUp event in a view. @@ -230,7 +259,7 @@ Discourse.ComposerController = Discourse.Controller.extend({ } else { opts.tested = true; if (!opts.ignoreIfChanged) { - this.cancel().then(function() { composerController.open(opts); }, + this.cancelComposer().then(function() { composerController.open(opts); }, function() { return promise.reject(); }); } return promise; @@ -278,7 +307,7 @@ Discourse.ComposerController = Discourse.Controller.extend({ } }, - cancel: function() { + cancelComposer: function() { var composerController = this; return Ember.Deferred.promise(function (promise) { @@ -332,26 +361,6 @@ Discourse.ComposerController = Discourse.Controller.extend({ $('#wmd-input').autocomplete({ cancel: true }); }, - // Toggle the reply view - toggle: function() { - this.closeAutocomplete(); - switch (this.get('model.composeState')) { - case Discourse.Composer.OPEN: - if (this.blank('model.reply') && this.blank('model.title')) { - this.close(); - } else { - this.shrink(); - } - break; - case Discourse.Composer.DRAFT: - this.set('model.composeState', Discourse.Composer.OPEN); - break; - case Discourse.Composer.SAVING: - this.close(); - } - return false; - }, - // ESC key hit hitEsc: function() { if (this.get('model.viewOpen')) { diff --git a/app/assets/javascripts/discourse/controllers/composer_messages_controller.js b/app/assets/javascripts/discourse/controllers/composer_messages_controller.js index 0b3e7fc3d..48371a4ae 100644 --- a/app/assets/javascripts/discourse/controllers/composer_messages_controller.js +++ b/app/assets/javascripts/discourse/controllers/composer_messages_controller.js @@ -20,6 +20,18 @@ Discourse.ComposerMessagesController = Ember.ArrayController.extend({ this.reset(); }, + actions: { + /** + Closes and hides a message. + + @method closeMessage + @params {Object} message The message to dismiss + **/ + closeMessage: function(message) { + this.removeObject(message); + } + }, + /** Displays a new message @@ -37,16 +49,6 @@ Discourse.ComposerMessagesController = Ember.ArrayController.extend({ } }, - /** - Closes and hides a message. - - @method closeMessage - @params {Object} message The message to dismiss - **/ - closeMessage: function(message) { - this.removeObject(message); - }, - /** Resets all active messages. For example if composing a new post. diff --git a/app/assets/javascripts/discourse/controllers/edit_category_controller.js b/app/assets/javascripts/discourse/controllers/edit_category_controller.js index cfa7f39e2..5e0ebf52f 100644 --- a/app/assets/javascripts/discourse/controllers/edit_category_controller.js +++ b/app/assets/javascripts/discourse/controllers/edit_category_controller.js @@ -39,18 +39,6 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M this.set('controllers.modal.title', this.get('title')); }.observes('title'), - selectGeneral: function() { - this.set('selectedTab', 'general'); - }, - - selectSecurity: function() { - this.set('selectedTab', 'security'); - }, - - selectSettings: function() { - this.set('selectedTab', 'settings'); - }, - disabled: function() { if (this.get('saving') || this.get('deleting')) return true; if (!this.get('name')) return true; @@ -103,83 +91,97 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M return I18n.t('category.delete'); }.property(), - showCategoryTopic: function() { - this.send('closeModal'); - Discourse.URL.routeTo(this.get('topic_url')); - return false; - }, + actions: { - editPermissions: function(){ - this.set('editingPermissions', true); - }, + selectGeneral: function() { + this.set('selectedTab', 'general'); + }, - addPermission: function(group, permission_id){ - this.get('model').addPermission({group_name: group + "", permission: Discourse.PermissionType.create({id: permission_id})}); - }, + selectSecurity: function() { + this.set('selectedTab', 'security'); + }, - removePermission: function(permission){ - this.get('model').removePermission(permission); - }, + selectSettings: function() { + this.set('selectedTab', 'settings'); + }, - saveCategory: function() { - var categoryController = this; - this.set('saving', true); + showCategoryTopic: function() { + this.send('closeModal'); + Discourse.URL.routeTo(this.get('topic_url')); + return false; + }, + + editPermissions: function(){ + this.set('editingPermissions', true); + }, + + addPermission: function(group, permission_id){ + this.get('model').addPermission({group_name: group + "", permission: Discourse.PermissionType.create({id: permission_id})}); + }, + + removePermission: function(permission){ + this.get('model').removePermission(permission); + }, + + saveCategory: function() { + var categoryController = this; + this.set('saving', true); - if( this.get('isUncategorized') ) { - $.when( - Discourse.SiteSetting.update('uncategorized_color', this.get('color')), - Discourse.SiteSetting.update('uncategorized_text_color', this.get('text_color')), - Discourse.SiteSetting.update('uncategorized_name', this.get('name')) - ).then(function(result) { - // success - categoryController.send('closeModal'); - // We can't redirect to the uncategorized category on save because the slug - // might have changed. - Discourse.URL.redirectTo("/categories"); - }, function(errors) { - // errors - if(errors.length === 0) errors.push(I18n.t("category.save_error")); - categoryController.displayErrors(errors); - categoryController.set('saving', false); - }); - } else { - this.get('model').save().then(function(result) { - // success - categoryController.send('closeModal'); - Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(result.category)); - }, function(errors) { - // errors - if(errors.length === 0) errors.push(I18n.t("category.creation_error")); - categoryController.displayErrors(errors); - categoryController.set('saving', false); - }); - } - }, - - deleteCategory: function() { - var categoryController = this; - this.set('deleting', true); - - $('#discourse-modal').modal('hide'); - bootbox.confirm(I18n.t("category.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { - if (result) { - categoryController.get('model').destroy().then(function(){ + if( this.get('isUncategorized') ) { + $.when( + Discourse.SiteSetting.update('uncategorized_color', this.get('color')), + Discourse.SiteSetting.update('uncategorized_text_color', this.get('text_color')), + Discourse.SiteSetting.update('uncategorized_name', this.get('name')) + ).then(function(result) { // success categoryController.send('closeModal'); + // We can't redirect to the uncategorized category on save because the slug + // might have changed. Discourse.URL.redirectTo("/categories"); - }, function(jqXHR){ - // error - $('#discourse-modal').modal('show'); - categoryController.displayErrors([I18n.t("category.delete_error")]); - categoryController.set('deleting', false); + }, function(errors) { + // errors + if(errors.length === 0) errors.push(I18n.t("category.save_error")); + categoryController.displayErrors(errors); + categoryController.set('saving', false); }); } else { - $('#discourse-modal').modal('show'); - categoryController.set('deleting', false); + this.get('model').save().then(function(result) { + // success + categoryController.send('closeModal'); + Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(result.category)); + }, function(errors) { + // errors + if(errors.length === 0) errors.push(I18n.t("category.creation_error")); + categoryController.displayErrors(errors); + categoryController.set('saving', false); + }); } - }); + }, + + deleteCategory: function() { + var categoryController = this; + this.set('deleting', true); + + $('#discourse-modal').modal('hide'); + bootbox.confirm(I18n.t("category.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { + if (result) { + categoryController.get('model').destroy().then(function(){ + // success + categoryController.send('closeModal'); + Discourse.URL.redirectTo("/categories"); + }, function(jqXHR){ + // error + $('#discourse-modal').modal('show'); + categoryController.displayErrors([I18n.t("category.delete_error")]); + categoryController.set('deleting', false); + }); + } else { + $('#discourse-modal').modal('show'); + categoryController.set('deleting', false); + } + }); + } } - }); diff --git a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js index 6301c2def..1dec95afa 100644 --- a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js +++ b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js @@ -20,12 +20,14 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco } }.observes('details.auto_close_at'), - saveAutoClose: function() { - this.setAutoClose( parseFloat(this.get('auto_close_days')) ); - }, + actions: { + saveAutoClose: function() { + this.setAutoClose( parseFloat(this.get('auto_close_days')) ); + }, - removeAutoClose: function() { - this.setAutoClose(null); + removeAutoClose: function() { + this.setAutoClose(null); + } }, setAutoClose: function(days) { diff --git a/app/assets/javascripts/discourse/controllers/flag_controller.js b/app/assets/javascripts/discourse/controllers/flag_controller.js index a573161bb..59c1a66e9 100644 --- a/app/assets/javascripts/discourse/controllers/flag_controller.js +++ b/app/assets/javascripts/discourse/controllers/flag_controller.js @@ -13,10 +13,6 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc this.set('selected', null); }, - changePostActionType: function(action) { - this.set('selected', action); - }, - submitEnabled: function() { var selected = this.get('selected'); if (!selected) return false; @@ -46,25 +42,31 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc } }.property('selected.is_custom_flag'), - takeAction: function() { - this.createFlag({takeAction: true}); - this.set('hidden', true); - }, + actions: { + takeAction: function() { + this.send('createFlag', {takeAction: true}); + this.set('hidden', true); + }, - createFlag: function(opts) { - var flagController = this; - var postAction = this.get('actionByName.' + this.get('selected.name_key')); - var params = this.get('selected.is_custom_flag') ? {message: this.get('message')} : {}; + createFlag: function(opts) { + var flagController = this; + var postAction = this.get('actionByName.' + this.get('selected.name_key')); + var params = this.get('selected.is_custom_flag') ? {message: this.get('message')} : {}; - if (opts) params = $.extend(params, opts); + if (opts) params = $.extend(params, opts); - $('#discourse-modal').modal('hide'); - postAction.act(params).then(function() { - flagController.send('closeModal'); - }, function(errors) { - $('#discourse-modal').modal('show'); - flagController.displayErrors(errors); - }); + $('#discourse-modal').modal('hide'); + postAction.act(params).then(function() { + flagController.send('closeModal'); + }, function(errors) { + $('#discourse-modal').modal('show'); + flagController.displayErrors(errors); + }); + }, + + changePostActionType: function(action) { + this.set('selected', action); + } }, canDeleteSpammer: function() { diff --git a/app/assets/javascripts/discourse/controllers/header_controller.js b/app/assets/javascripts/discourse/controllers/header_controller.js index 3d3699493..69413093b 100644 --- a/app/assets/javascripts/discourse/controllers/header_controller.js +++ b/app/assets/javascripts/discourse/controllers/header_controller.js @@ -10,12 +10,6 @@ Discourse.HeaderController = Discourse.Controller.extend({ topic: null, showExtraInfo: null, - toggleStar: function() { - var topic = this.get('topic'); - if (topic) topic.toggleStar(); - return false; - }, - categories: function() { return Discourse.Category.list(); }.property(), @@ -36,8 +30,16 @@ Discourse.HeaderController = Discourse.Controller.extend({ return Discourse.SiteSettings.enable_mobile_theme; }.property(), - toggleMobileView: function() { - Discourse.Mobile.toggleMobileView(); + actions: { + toggleStar: function() { + var topic = this.get('topic'); + if (topic) topic.toggleStar(); + return false; + }, + + toggleMobileView: function() { + Discourse.Mobile.toggleMobileView(); + } } }); diff --git a/app/assets/javascripts/discourse/controllers/invite_controller.js b/app/assets/javascripts/discourse/controllers/invite_controller.js index d8ad9bba3..e619914e3 100644 --- a/app/assets/javascripts/discourse/controllers/invite_controller.js +++ b/app/assets/javascripts/discourse/controllers/invite_controller.js @@ -25,22 +25,25 @@ Discourse.InviteController = Discourse.ObjectController.extend(Discourse.ModalFu return I18n.t('topic.invite_reply.success', { email: this.get('email') }); }.property('email'), - createInvite: function() { - if (this.get('disabled')) return; + actions: { + createInvite: function() { + if (this.get('disabled')) return; - var inviteController = this; - this.set('saving', true); - this.set('error', false); - this.get('model').inviteUser(this.get('email')).then(function() { - // Success - inviteController.set('saving', false); - return inviteController.set('finished', true); - }, function() { - // Failure - inviteController.set('error', true); - return inviteController.set('saving', false); - }); - return false; + var inviteController = this; + this.set('saving', true); + this.set('error', false); + this.get('model').inviteUser(this.get('email')).then(function() { + // Success + inviteController.set('saving', false); + return inviteController.set('finished', true); + }, function() { + // Failure + inviteController.set('error', true); + return inviteController.set('saving', false); + }); + return false; + } } + }); diff --git a/app/assets/javascripts/discourse/controllers/list_controller.js b/app/assets/javascripts/discourse/controllers/list_controller.js index 07bc684c4..edf9765fd 100644 --- a/app/assets/javascripts/discourse/controllers/list_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_controller.js @@ -113,14 +113,16 @@ Discourse.ListController = Discourse.Controller.extend({ }.observes('filterMode', 'category'), // Create topic button - createTopic: function() { - this.get('controllers.composer').open({ - categoryId: this.get('category.id'), - action: Discourse.Composer.CREATE_TOPIC, - draft: this.get('draft'), - draftKey: this.get('draft_key'), - draftSequence: this.get('draft_sequence') - }); + actions: { + createTopic: function() { + this.get('controllers.composer').open({ + categoryId: this.get('category.id'), + action: Discourse.Composer.CREATE_TOPIC, + draft: this.get('draft'), + draftKey: this.get('draft_key'), + draftSequence: this.get('draft_sequence') + }); + } }, canEditCategory: function() { diff --git a/app/assets/javascripts/discourse/controllers/list_topics_controller.js b/app/assets/javascripts/discourse/controllers/list_topics_controller.js index 511d39669..ec202ce11 100644 --- a/app/assets/javascripts/discourse/controllers/list_topics_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_topics_controller.js @@ -27,32 +27,34 @@ Discourse.ListTopicsController = Discourse.ObjectController.extend({ } }.observes('content.draft'), - // Star a topic - toggleStar: function(topic) { - topic.toggleStar(); - }, + actions: { + // Star a topic + toggleStar: function(topic) { + topic.toggleStar(); + }, - // clear a pinned topic - clearPin: function(topic) { - topic.clearPin(); - }, + // clear a pinned topic + clearPin: function(topic) { + topic.clearPin(); + }, - toggleRankDetails: function() { - this.toggleProperty('rankDetailsVisible'); - }, + toggleRankDetails: function() { + this.toggleProperty('rankDetailsVisible'); + }, - createTopic: function() { - this.get('controllers.list').createTopic(); - }, + createTopic: function() { + this.get('controllers.list').send('createTopic'); + }, - // Show newly inserted topics - showInserted: function(e) { - var tracker = Discourse.TopicTrackingState.current(); + // Show newly inserted topics + showInserted: function(e) { + var tracker = Discourse.TopicTrackingState.current(); - // Move inserted into topics - this.get('content').loadBefore(tracker.get('newIncoming')); - tracker.resetTracking(); - return false; + // Move inserted into topics + this.get('content').loadBefore(tracker.get('newIncoming')); + tracker.resetTracking(); + return false; + } }, allLoaded: function() { diff --git a/app/assets/javascripts/discourse/controllers/merge_topic_controller.js b/app/assets/javascripts/discourse/controllers/merge_topic_controller.js index 672654877..8483b1974 100644 --- a/app/assets/javascripts/discourse/controllers/merge_topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/merge_topic_controller.js @@ -46,7 +46,7 @@ Discourse.MergeTopicController = Discourse.ObjectController.extend(Discourse.Sel promise.then(function(result) { // Posts moved mergeTopicController.send('closeModal'); - mergeTopicController.get('topicController').toggleMultiSelect(); + mergeTopicController.get('topicController').send('toggleMultiSelect'); Em.run.next(function() { Discourse.URL.routeTo(result.url); }); }, function() { // Error moving posts diff --git a/app/assets/javascripts/discourse/controllers/preferences_controller.js b/app/assets/javascripts/discourse/controllers/preferences_controller.js index 278c232e3..3a4540356 100644 --- a/app/assets/javascripts/discourse/controllers/preferences_controller.js +++ b/app/assets/javascripts/discourse/controllers/preferences_controller.js @@ -37,53 +37,54 @@ Discourse.PreferencesController = Discourse.ObjectController.extend({ { name: I18n.t('user.new_topic_duration.after_n_weeks', { count: 1 }), value: 7 * 60 * 24 }, { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], - save: function() { - var preferencesController = this; - this.set('saving', true); - this.set('saved', false); - - // Cook the bio for preview - var model = this.get('model'); - return model.save().then(function() { - // model was saved - preferencesController.set('saving', false); - if (Discourse.User.currentProp('id') === model.get('id')) { - Discourse.User.currentProp('name', model.get('name')); - } - - preferencesController.set('bio_cooked', - Discourse.Markdown.cook(preferencesController.get('bio_raw'))); - preferencesController.set('saved', true); - }, function() { - // model failed to save - preferencesController.set('saving', false); - alert(I18n.t('generic_error')); - }); - }, - saveButtonText: function() { return this.get('saving') ? I18n.t('saving') : I18n.t('save'); }.property('saving'), - changePassword: function() { - var preferencesController = this; - if (!this.get('passwordProgress')) { - this.set('passwordProgress', I18n.t("user.change_password.in_progress")); - return this.get('model').changePassword().then(function() { - // password changed - preferencesController.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.success") - }); + actions: { + save: function() { + var self = this; + this.set('saving', true); + this.set('saved', false); + + // Cook the bio for preview + var model = this.get('model'); + return model.save().then(function() { + // model was saved + self.set('saving', false); + if (Discourse.User.currentProp('id') === model.get('id')) { + Discourse.User.currentProp('name', model.get('name')); + } + self.set('bio_cooked', Discourse.Markdown.cook(self.get('bio_raw'))); + self.set('saved', true); }, function() { - // password failed to change - preferencesController.setProperties({ - changePasswordProgress: false, - passwordProgress: I18n.t("user.change_password.error") - }); + // model failed to save + self.set('saving', false); + alert(I18n.t('generic_error')); }); + }, + + changePassword: function() { + var self = this; + if (!this.get('passwordProgress')) { + this.set('passwordProgress', I18n.t("user.change_password.in_progress")); + return this.get('model').changePassword().then(function() { + // password changed + self.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.success") + }); + }, function() { + // password failed to change + self.setProperties({ + changePasswordProgress: false, + passwordProgress: I18n.t("user.change_password.error") + }); + }); + } } } + }); diff --git a/app/assets/javascripts/discourse/controllers/preferences_email_controller.js b/app/assets/javascripts/discourse/controllers/preferences_email_controller.js index e07506f86..36426f848 100644 --- a/app/assets/javascripts/discourse/controllers/preferences_email_controller.js +++ b/app/assets/javascripts/discourse/controllers/preferences_email_controller.js @@ -22,17 +22,16 @@ Discourse.PreferencesEmailController = Discourse.ObjectController.extend({ return I18n.t("user.change"); }.property('saving'), - changeEmail: function() { - var preferencesEmailController = this; - this.set('saving', true); - return this.get('content').changeEmail(this.get('newEmail')).then(function() { - preferencesEmailController.set('success', true); - }, function() { - preferencesEmailController.setProperties({ - error: true, - saving: false + actions: { + changeEmail: function() { + var self = this; + this.set('saving', true); + return this.get('content').changeEmail(this.get('newEmail')).then(function() { + self.set('success', true); + }, function() { + self.setProperties({ error: true, saving: false }); }); - }); + } } }); diff --git a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js index 7fc6cebca..d9ae9d744 100644 --- a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js +++ b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js @@ -41,21 +41,24 @@ Discourse.PreferencesUsernameController = Discourse.ObjectController.extend({ return I18n.t("user.change"); }.property('saving'), - changeUsername: function() { - var preferencesUsernameController = this; - return bootbox.confirm(I18n.t("user.change_username.confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { - if (result) { - preferencesUsernameController.set('saving', true); - preferencesUsernameController.get('content').changeUsername(preferencesUsernameController.get('newUsername')).then(function() { - Discourse.URL.redirectTo("/users/" + preferencesUsernameController.get('newUsername').toLowerCase() + "/preferences"); - }, function() { - // error - preferencesUsernameController.set('error', true); - preferencesUsernameController.set('saving', false); - }); - } - }); + actions: { + changeUsername: function() { + var preferencesUsernameController = this; + return bootbox.confirm(I18n.t("user.change_username.confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { + if (result) { + preferencesUsernameController.set('saving', true); + preferencesUsernameController.get('content').changeUsername(preferencesUsernameController.get('newUsername')).then(function() { + Discourse.URL.redirectTo("/users/" + preferencesUsernameController.get('newUsername').toLowerCase() + "/preferences"); + }, function() { + // error + preferencesUsernameController.set('error', true); + preferencesUsernameController.set('saving', false); + }); + } + }); + } } + }); diff --git a/app/assets/javascripts/discourse/controllers/search_controller.js b/app/assets/javascripts/discourse/controllers/search_controller.js index 215622ff1..d2dde8267 100644 --- a/app/assets/javascripts/discourse/controllers/search_controller.js +++ b/app/assets/javascripts/discourse/controllers/search_controller.js @@ -62,14 +62,20 @@ Discourse.SearchController = Em.ArrayController.extend(Discourse.Presence, { }.property('typeFilter', 'loading'), termChanged: function() { - this.cancelType(); + this.cancelTypeFilter(); }.observes('term'), - moreOfType: function(type) { - this.set('typeFilter', type); + actions: { + moreOfType: function(type) { + this.set('typeFilter', type); + }, + + cancelType: function() { + this.cancelTypeFilter(); + } }, - cancelType: function() { + cancelTypeFilter: function() { this.set('typeFilter', null); }, diff --git a/app/assets/javascripts/discourse/controllers/share_controller.js b/app/assets/javascripts/discourse/controllers/share_controller.js index 4d513b3f0..344bb0343 100644 --- a/app/assets/javascripts/discourse/controllers/share_controller.js +++ b/app/assets/javascripts/discourse/controllers/share_controller.js @@ -11,10 +11,12 @@ Discourse.ShareController = Discourse.Controller.extend({ needs: ['topic'], // Close the share controller - close: function() { - this.set('link', ''); - this.set('postNumber', ''); - return false; + actions: { + close: function() { + this.set('link', ''); + this.set('postNumber', ''); + return false; + } }, shareLinks: function() { diff --git a/app/assets/javascripts/discourse/controllers/split_topic_controller.js b/app/assets/javascripts/discourse/controllers/split_topic_controller.js index 7d2dd5993..01ad6094d 100644 --- a/app/assets/javascripts/discourse/controllers/split_topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/split_topic_controller.js @@ -42,7 +42,7 @@ Discourse.SplitTopicController = Discourse.ObjectController.extend(Discourse.Sel }).then(function(result) { // Posts moved self.send('closeModal'); - self.get('topicController').toggleMultiSelect(); + self.get('topicController').send('toggleMultiSelect'); Em.run.next(function() { Discourse.URL.routeTo(result.url); }); }, function() { // Error moving posts diff --git a/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js b/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js index 43d308366..e100e5d6f 100644 --- a/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js @@ -10,12 +10,14 @@ Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({ menuVisible: false, needs: ['modal'], - show: function() { - this.set('menuVisible', true); - }, + actions: { + show: function() { + this.set('menuVisible', true); + }, - hide: function() { - this.set('menuVisible', false); + hide: function() { + this.set('menuVisible', false); + } }, showRecover: Em.computed.and('deleted', 'details.can_recover') diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index e6a01bbe4..09a8c88a2 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -21,6 +21,209 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected this.set('selectedReplies', new Em.Set()); }, + actions: { + jumpTop: function() { + Discourse.URL.routeTo(this.get('url')); + }, + + jumpBottom: function() { + Discourse.URL.routeTo(this.get('lastPostUrl')); + }, + + toggleSummary: function() { + this.toggleProperty('summaryCollapsed'); + }, + + selectAll: function() { + var posts = this.get('postStream.posts'); + var selectedPosts = this.get('selectedPosts'); + if (posts) { + selectedPosts.addObjects(posts); + } + this.set('allPostsSelected', true); + }, + + deselectAll: function() { + this.get('selectedPosts').clear(); + this.get('selectedReplies').clear(); + this.set('allPostsSelected', false); + }, + + /** + Toggle a participant for filtering + + @method toggleParticipant + **/ + toggleParticipant: function(user) { + this.get('postStream').toggleParticipant(Em.get(user, 'username')); + }, + + editTopic: function() { + if (!this.get('details.can_edit')) return false; + + this.setProperties({ + editingTopic: true, + newTitle: this.get('title'), + newCategoryId: this.get('category_id') + }); + return false; + }, + + // close editing mode + cancelEditingTopic: function() { + this.set('editingTopic', false); + }, + + toggleMultiSelect: function() { + this.toggleProperty('multiSelect'); + }, + + finishedEditingTopic: function() { + var topicController = this; + if (this.get('editingTopic')) { + + var topic = this.get('model'); + + // Topic title hasn't been sanitized yet, so the template shouldn't trust it. + this.set('topicSaving', true); + + // manually update the titles & category + topic.setProperties({ + title: this.get('newTitle'), + category_id: parseInt(this.get('newCategoryId'), 10), + fancy_title: this.get('newTitle') + }); + + // save the modifications + topic.save().then(function(result){ + // update the title if it has been changed (cleaned up) server-side + var title = result.basic_topic.title; + var fancy_title = result.basic_topic.fancy_title; + topic.setProperties({ + title: title, + fancy_title: fancy_title + }); + topicController.set('topicSaving', false); + }, function(error) { + topicController.set('editingTopic', true); + topicController.set('topicSaving', false); + if (error && error.responseText) { + bootbox.alert($.parseJSON(error.responseText).errors[0]); + } else { + bootbox.alert(I18n.t('generic_error')); + } + }); + + // close editing mode + topicController.set('editingTopic', false); + } + }, + + toggledSelectedPost: function(post) { + this.performTogglePost(post); + }, + + toggledSelectedPostReplies: function(post) { + var selectedReplies = this.get('selectedReplies'); + if (this.performTogglePost(post)) { + selectedReplies.addObject(post); + } else { + selectedReplies.removeObject(post); + } + }, + + deleteSelected: function() { + var self = this; + bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) { + if (result) { + + // If all posts are selected, it's the same thing as deleting the topic + if (self.get('allPostsSelected')) { + return self.deleteTopic(); + } + + var selectedPosts = self.get('selectedPosts'), + selectedReplies = self.get('selectedReplies'), + postStream = self.get('postStream'), + toRemove = new Ember.Set(); + + + Discourse.Post.deleteMany(selectedPosts, selectedReplies); + postStream.get('posts').forEach(function (p) { + if (self.postSelected(p)) { toRemove.addObject(p); } + }); + + postStream.removePosts(toRemove); + self.send('toggleMultiSelect'); + } + }); + }, + + toggleVisibility: function() { + this.get('content').toggleStatus('visible'); + }, + + toggleClosed: function() { + this.get('content').toggleStatus('closed'); + }, + + togglePinned: function() { + this.get('content').toggleStatus('pinned'); + }, + + toggleArchived: function() { + this.get('content').toggleStatus('archived'); + }, + + convertToRegular: function() { + this.get('content').convertArchetype('regular'); + }, + + // Toggle the star on the topic + toggleStar: function() { + this.get('content').toggleStar(); + }, + + + /** + Clears the pin from a topic for the currently logged in user + + @method clearPin + **/ + clearPin: function() { + this.get('content').clearPin(); + }, + + resetRead: function() { + Discourse.ScreenTrack.current().reset(); + this.unsubscribe(); + + var topicController = this; + this.get('model').resetRead().then(function() { + topicController.set('message', I18n.t("topic.read_position_reset")); + topicController.set('postStream.loaded', false); + }); + }, + + replyAsNewTopic: function(post) { + var composerController = this.get('controllers.composer'); + var promise = composerController.open({ + action: Discourse.Composer.CREATE_TOPIC, + draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY + }); + var postUrl = "" + location.protocol + "//" + location.host + (post.get('url')); + var postLink = "[" + (this.get('title')) + "](" + postUrl + ")"; + + promise.then(function() { + Discourse.Post.loadQuote(post.get('id')).then(function(q) { + composerController.appendText(I18n.t("post.continue_discussion", { + postLink: postLink + }) + "\n\n" + q); + }); + }); + } + }, + jumpTopDisabled: function() { return (this.get('progressPosition') === 1); }.property('postStream.filteredPostsCount', 'progressPosition'), @@ -78,7 +281,7 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected multiSelectChanged: function() { // Deselect all posts when multi select is turned off if (!this.get('multiSelect')) { - this.deselectAll(); + this.send('deselectAll'); } }.observes('multiSelect'), @@ -109,156 +312,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected return false; }, - toggledSelectedPost: function(post) { - var selectedPosts = this.get('selectedPosts'); - if (this.postSelected(post)) { - this.deselectPost(post); - return false; - } else { - selectedPosts.addObject(post); - - // If the user manually selects all posts, all posts are selected - if (selectedPosts.length === this.get('posts_count')) { - this.set('allPostsSelected', true); - } - return true; - } - }, - - toggledSelectedPostReplies: function(post) { - var selectedReplies = this.get('selectedReplies'); - if (this.toggledSelectedPost(post)) { - selectedReplies.addObject(post); - } else { - selectedReplies.removeObject(post); - } - }, - - selectAll: function() { - var posts = this.get('postStream.posts'); - var selectedPosts = this.get('selectedPosts'); - if (posts) { - selectedPosts.addObjects(posts); - } - this.set('allPostsSelected', true); - }, - - deselectAll: function() { - this.get('selectedPosts').clear(); - this.get('selectedReplies').clear(); - this.set('allPostsSelected', false); - }, - - toggleMultiSelect: function() { - this.toggleProperty('multiSelect'); - }, - - toggleSummary: function() { - this.toggleProperty('summaryCollapsed'); - }, - - editTopic: function() { - if (!this.get('details.can_edit')) return false; - - this.setProperties({ - editingTopic: true, - newTitle: this.get('title'), - newCategoryId: this.get('category_id') - }); - return false; - }, - - // close editing mode - cancelEditingTopic: function() { - this.set('editingTopic', false); - }, - - finishedEditingTopic: function() { - var topicController = this; - if (this.get('editingTopic')) { - - var topic = this.get('model'); - - // Topic title hasn't been sanitized yet, so the template shouldn't trust it. - this.set('topicSaving', true); - - // manually update the titles & category - topic.setProperties({ - title: this.get('newTitle'), - category_id: parseInt(this.get('newCategoryId'), 10), - fancy_title: this.get('newTitle') - }); - - // save the modifications - topic.save().then(function(result){ - // update the title if it has been changed (cleaned up) server-side - var title = result.basic_topic.title; - var fancy_title = result.basic_topic.fancy_title; - topic.setProperties({ - title: title, - fancy_title: fancy_title - }); - topicController.set('topicSaving', false); - }, function(error) { - topicController.set('editingTopic', true); - topicController.set('topicSaving', false); - if (error && error.responseText) { - bootbox.alert($.parseJSON(error.responseText).errors[0]); - } else { - bootbox.alert(I18n.t('generic_error')); - } - }); - - // close editing mode - topicController.set('editingTopic', false); - } - }, - - deleteSelected: function() { - var self = this; - bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) { - if (result) { - - // If all posts are selected, it's the same thing as deleting the topic - if (self.get('allPostsSelected')) { - return self.deleteTopic(); - } - - var selectedPosts = self.get('selectedPosts'), - selectedReplies = self.get('selectedReplies'), - postStream = self.get('postStream'), - toRemove = new Ember.Set(); - - - Discourse.Post.deleteMany(selectedPosts, selectedReplies); - postStream.get('posts').forEach(function (p) { - if (self.postSelected(p)) { toRemove.addObject(p); } - }); - - postStream.removePosts(toRemove); - self.toggleMultiSelect(); - } - }); - }, - - jumpTop: function() { - Discourse.URL.routeTo(this.get('url')); - }, - - jumpBottom: function() { - Discourse.URL.routeTo(this.get('lastPostUrl')); - }, - - - /** - Toggle a participant for filtering - - @method toggleParticipant - **/ - toggleParticipant: function(user) { - this.get('postStream').toggleParticipant(Em.get(user, 'username')); - }, - showFavoriteButton: function() { return Discourse.User.current() && !this.get('isPrivateMessage'); }.property('isPrivateMessage'), @@ -272,52 +325,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected this.get('content').destroy(Discourse.User.current()); }, - resetRead: function() { - Discourse.ScreenTrack.current().reset(); - this.unsubscribe(); - - var topicController = this; - this.get('model').resetRead().then(function() { - topicController.set('message', I18n.t("topic.read_position_reset")); - topicController.set('postStream.loaded', false); - }); - }, - - toggleVisibility: function() { - this.get('content').toggleStatus('visible'); - }, - - toggleClosed: function() { - this.get('content').toggleStatus('closed'); - }, - - togglePinned: function() { - this.get('content').toggleStatus('pinned'); - }, - - toggleArchived: function() { - this.get('content').toggleStatus('archived'); - }, - - convertToRegular: function() { - this.get('content').convertArchetype('regular'); - }, - - // Toggle the star on the topic - toggleStar: function() { - this.get('content').toggleStar(); - }, - - - /** - Clears the pin from a topic for the currently logged in user - - @method clearPin - **/ - clearPin: function() { - this.get('content').clearPin(); - }, - // Receive notifications for this topic subscribe: function() { @@ -383,24 +390,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected return false; }, - replyAsNewTopic: function(post) { - var composerController = this.get('controllers.composer'); - var promise = composerController.open({ - action: Discourse.Composer.CREATE_TOPIC, - draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY - }); - var postUrl = "" + location.protocol + "//" + location.host + (post.get('url')); - var postLink = "[" + (this.get('title')) + "](" + postUrl + ")"; - - promise.then(function() { - Discourse.Post.loadQuote(post.get('id')).then(function(q) { - composerController.appendText(I18n.t("post.continue_discussion", { - postLink: postLink - }) + "\n\n" + q); - }); - }); - }, - // Topic related reply: function() { this.replyToPost(); @@ -470,6 +459,22 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected } }, + performTogglePost: function(post) { + var selectedPosts = this.get('selectedPosts'); + if (this.postSelected(post)) { + this.deselectPost(post); + return false; + } else { + selectedPosts.addObject(post); + + // If the user manually selects all posts, all posts are selected + if (selectedPosts.length === this.get('posts_count')) { + this.set('allPostsSelected', true); + } + return true; + } + }, + removeAllowedUser: function(username) { this.get('details').removeAllowedUser(username); } diff --git a/app/assets/javascripts/discourse/controllers/upload_selector_controller.js b/app/assets/javascripts/discourse/controllers/upload_selector_controller.js index 6ffaff606..707f7f0cd 100644 --- a/app/assets/javascripts/discourse/controllers/upload_selector_controller.js +++ b/app/assets/javascripts/discourse/controllers/upload_selector_controller.js @@ -11,8 +11,10 @@ Discourse.UploadSelectorController = Discourse.Controller.extend(Discourse.Modal localSelected: true, remoteSelected: Em.computed.not('localSelected'), - selectLocal: function() { this.set('localSelected', true); }, - selectRemote: function() { this.set('localSelected', false); }, + actions: { + selectLocal: function() { this.set('localSelected', true); }, + selectRemote: function() { this.set('localSelected', false); } + }, localTitle: function() { return Discourse.UploadSelectorController.translate("local_title"); }.property(), remoteTitle: function() { return Discourse.UploadSelectorController.translate("remote_title"); }.property(), diff --git a/app/assets/javascripts/discourse/controllers/user_invited_controller.js b/app/assets/javascripts/discourse/controllers/user_invited_controller.js index 460b08d6a..4548bdb56 100644 --- a/app/assets/javascripts/discourse/controllers/user_invited_controller.js +++ b/app/assets/javascripts/discourse/controllers/user_invited_controller.js @@ -7,10 +7,14 @@ @module Discourse **/ Discourse.UserInvitedController = Discourse.ObjectController.extend({ - rescind: function(invite) { - invite.rescind(); - return false; + + actions: { + rescind: function(invite) { + invite.rescind(); + return false; + } } + }); diff --git a/app/assets/javascripts/discourse/routes/application_route.js b/app/assets/javascripts/discourse/routes/application_route.js index d28e6b09c..80fdd8687 100644 --- a/app/assets/javascripts/discourse/routes/application_route.js +++ b/app/assets/javascripts/discourse/routes/application_route.js @@ -8,7 +8,7 @@ **/ Discourse.ApplicationRoute = Em.Route.extend({ - events: { + actions: { showLogin: function() { Discourse.Route.showModal(this, 'login'); }, diff --git a/app/assets/javascripts/discourse/routes/list_categories_route.js b/app/assets/javascripts/discourse/routes/list_categories_route.js index bc05096ca..5ebd12d4d 100644 --- a/app/assets/javascripts/discourse/routes/list_categories_route.js +++ b/app/assets/javascripts/discourse/routes/list_categories_route.js @@ -10,7 +10,7 @@ Discourse.ListCategoriesRoute = Discourse.Route.extend({ redirect: function() { Discourse.redirectIfLoginRequired(this); }, - events: { + actions: { createCategory: function() { Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({ color: 'AB9364', text_color: 'FFFFFF', hotness: 5, group_permissions: [{group_name: "everyone", permission_type: 1}], diff --git a/app/assets/javascripts/discourse/routes/preferences_routes.js b/app/assets/javascripts/discourse/routes/preferences_routes.js index e337a43a8..ca7d3aadb 100644 --- a/app/assets/javascripts/discourse/routes/preferences_routes.js +++ b/app/assets/javascripts/discourse/routes/preferences_routes.js @@ -15,7 +15,7 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({ this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' }); }, - events: { + actions: { showAvatarSelector: function() { Discourse.Route.showModal(this, 'avatarSelector'); // all the properties needed for displaying the avatar selector modal diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js index fbe4253ba..6865e340d 100644 --- a/app/assets/javascripts/discourse/routes/topic_route.js +++ b/app/assets/javascripts/discourse/routes/topic_route.js @@ -10,7 +10,7 @@ Discourse.TopicRoute = Discourse.Route.extend({ redirect: function() { Discourse.redirectIfLoginRequired(this); }, - events: { + actions: { // Modals that can pop up within a topic showFlags: function(post) { diff --git a/app/assets/javascripts/discourse/templates/header.js.handlebars b/app/assets/javascripts/discourse/templates/header.js.handlebars index b0c499737..c3790dde1 100644 --- a/app/assets/javascripts/discourse/templates/header.js.handlebars +++ b/app/assets/javascripts/discourse/templates/header.js.handlebars @@ -85,7 +85,7 @@ </li> <li class='current-user'> {{#if currentUser}} - {{#linkTo 'userActivity.index' currentUser titleKey="current_user" class="icon"}}{{boundAvatar currentUser imageSize="medium" }}{{/linkTo}} + {{#link-to 'userActivity.index' currentUser titleKey="current_user" class="icon"}}{{boundAvatar currentUser imageSize="medium" }}{{/link-to}} {{else}} <div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div> {{/if}} @@ -139,7 +139,7 @@ {{#if categories}} <ul class="category-links"> <li class='heading' title="{{i18n filters.categories.help}}"> - {{#linkTo "list.categories"}}{{i18n filters.categories.title}}{{/linkTo}} + {{#link-to "list.categories"}}{{i18n filters.categories.title}}{{/link-to}} </li> {{#each categories}} diff --git a/app/assets/javascripts/discourse/templates/list/topics.js.handlebars b/app/assets/javascripts/discourse/templates/list/topics.js.handlebars index e9a256307..236787cd7 100644 --- a/app/assets/javascripts/discourse/templates/list/topics.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/topics.js.handlebars @@ -60,7 +60,7 @@ <a href='#' {{action createTopic}}>{{i18n topic.suggest_create_topic}}</a> {{/if}} {{else}} - {{#linkTo 'list.categories'}}{{i18n topic.browse_all_categories}}{{/linkTo}} {{i18n or}} {{#linkTo 'list.latest'}}{{i18n topic.view_latest_topics}}{{/linkTo}} + {{#link-to 'list.categories'}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}} {{/if}} {{/if}} </h3> diff --git a/app/assets/javascripts/discourse/templates/mobile/list/topics.js.handlebars b/app/assets/javascripts/discourse/templates/mobile/list/topics.js.handlebars index e6c731c9e..41a62dc2e 100644 --- a/app/assets/javascripts/discourse/templates/mobile/list/topics.js.handlebars +++ b/app/assets/javascripts/discourse/templates/mobile/list/topics.js.handlebars @@ -43,7 +43,7 @@ <a href='#' {{action createTopic}}>{{i18n topic.suggest_create_topic}}</a> {{/if}} {{else}} - {{#linkTo 'list.categories'}}{{i18n topic.browse_all_categories}}{{/linkTo}} {{i18n or}} {{#linkTo 'list.latest'}}{{i18n topic.view_latest_topics}}{{/linkTo}} + {{#link-to 'list.categories'}}{{i18n topic.browse_all_categories}}{{/link-to}} {{i18n or}} {{#link-to 'list.latest'}}{{i18n topic.view_latest_topics}}{{/link-to}} {{/if}} {{/if}} </h3> diff --git a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars index 03cd3c61d..c8d17d839 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars @@ -1,7 +1,7 @@ <div id='user-info'> <nav class='buttons'> {{#if can_edit}} - {{#linkTo "preferences" class="btn"}}{{i18n user.edit}}{{/linkTo}} + {{#link-to "preferences" class="btn"}}{{i18n user.edit}}{{/link-to}} {{/if}} <br/> {{#if can_send_private_message_to_user}} @@ -16,13 +16,13 @@ <ul class='action-list nav-stacked side-nav'> {{#if privateMessageView}} <li {{bindAttr class=":noGlyph privateMessagesActive:active"}}> - {{#linkTo 'userPrivateMessages.index' model}}{{i18n user.messages.all}}{{/linkTo}} + {{#link-to 'userPrivateMessages.index' model}}{{i18n user.messages.all}}{{/link-to}} </li> <li {{bindAttr class=":noGlyph privateMessagesMineActive:active"}}> - {{#linkTo 'userPrivateMessages.mine' model}}{{i18n user.messages.mine}}{{/linkTo}} + {{#link-to 'userPrivateMessages.mine' model}}{{i18n user.messages.mine}}{{/link-to}} </li> <li {{bindAttr class=":noGlyph privateMessagesUnreadActive:active"}}> - {{#linkTo 'userPrivateMessages.unread' model}}{{i18n user.messages.unread}}{{/linkTo}} + {{#link-to 'userPrivateMessages.unread' model}}{{i18n user.messages.unread}}{{/link-to}} </li> {{else}} @@ -48,7 +48,7 @@ <dt>{{i18n user.last_seen}}:</dt><dd>{{date last_seen_at}}</dd> {{/if}} {{#if invited_by}} - <dt>{{i18n user.invited_by}}:</dt><dd>{{#linkTo 'userActivity' invited_by}}{{invited_by.username}}{{/linkTo}}</dd> + <dt>{{i18n user.invited_by}}:</dt><dd>{{#link-to 'userActivity' invited_by}}{{invited_by.username}}{{/link-to}}</dd> {{/if}} {{#if email}} <dt>{{i18n user.email.title}}:</dt><dd {{bindAttr title="email"}}>{{email}}</dd> diff --git a/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars b/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars index e08e0aee5..1148bbd92 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars @@ -5,7 +5,7 @@ <div class="controls"> <span class='static'>{{username}}</span> {{#if can_edit_username}} - {{#linkTo "preferences.username" class="btn pad-left"}}<i class="icon-pencil"></i>{{/linkTo}} + {{#link-to "preferences.username" class="btn pad-left"}}<i class="icon-pencil"></i>{{/link-to}} {{/if}} </div> <div class='instructions'> @@ -28,7 +28,7 @@ <div class="controls"> <span class='static'>{{email}}</span> {{#if can_edit_email}} - {{#linkTo "preferences.email" class="btn pad-left"}}<i class="icon-pencil"></i>{{/linkTo}} + {{#link-to "preferences.email" class="btn pad-left"}}<i class="icon-pencil"></i>{{/link-to}} {{/if}} </div> <div class='instructions'> diff --git a/app/assets/javascripts/discourse/templates/user/user.js.handlebars b/app/assets/javascripts/discourse/templates/user/user.js.handlebars index bfa14de94..c49253364 100644 --- a/app/assets/javascripts/discourse/templates/user/user.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/user.js.handlebars @@ -12,19 +12,19 @@ {{/if}} <ul class="nav nav-pills"> <li> - {{#linkTo 'userActivity'}}{{i18n user.activity_stream}}{{/linkTo}} + {{#link-to 'userActivity'}}{{i18n user.activity_stream}}{{/link-to}} </li> {{#if canSeePrivateMessages}} <li> - {{#linkTo 'userPrivateMessages'}}{{i18n user.private_messages}}{{/linkTo}} + {{#link-to 'userPrivateMessages'}}{{i18n user.private_messages}}{{/link-to}} </li> {{/if}} <li> - {{#linkTo 'user.invited'}}{{i18n user.invited.title}}{{/linkTo}} + {{#link-to 'user.invited'}}{{i18n user.invited.title}}{{/link-to}} </li> {{#if can_edit}} <li> - {{#linkTo 'preferences'}}{{i18n user.preferences}}{{/linkTo}} + {{#link-to 'preferences'}}{{i18n user.preferences}}{{/link-to}} </li> {{/if}} </ul> diff --git a/app/assets/javascripts/discourse/views/buttons/favorite_button.js b/app/assets/javascripts/discourse/views/buttons/favorite_button.js index d04381cd2..535f39961 100644 --- a/app/assets/javascripts/discourse/views/buttons/favorite_button.js +++ b/app/assets/javascripts/discourse/views/buttons/favorite_button.js @@ -14,7 +14,7 @@ Discourse.FavoriteButton = Discourse.ButtonView.extend({ shouldRerender: Discourse.View.renderIfChanged('controller.starred'), click: function() { - this.get('controller').toggleStar(); + this.get('controller').send('toggleStar'); }, renderIcon: function(buffer) { diff --git a/app/assets/javascripts/discourse/views/share_view.js b/app/assets/javascripts/discourse/views/share_view.js index cb72a8a57..3c4f0f3ce 100644 --- a/app/assets/javascripts/discourse/views/share_view.js +++ b/app/assets/javascripts/discourse/views/share_view.js @@ -45,7 +45,7 @@ Discourse.ShareView = Discourse.View.extend({ // link is clicked (which is a click event) while the share dialog is showing. if (shareView.$().has(e.target).length !== 0) { return; } - shareView.get('controller').close(); + shareView.get('controller').send('close'); return true; }); @@ -76,7 +76,7 @@ Discourse.ShareView = Discourse.View.extend({ $('html').on('keydown.share-view', function(e){ if (e.keyCode === 27) { - shareView.get('controller').close(); + shareView.get('controller').send('close'); } }); }, diff --git a/app/assets/javascripts/discourse/views/topic_admin_menu_view.js b/app/assets/javascripts/discourse/views/topic_admin_menu_view.js index 39760f4a7..8864afbb4 100644 --- a/app/assets/javascripts/discourse/views/topic_admin_menu_view.js +++ b/app/assets/javascripts/discourse/views/topic_admin_menu_view.js @@ -13,11 +13,11 @@ Discourse.TopicAdminMenuView = Discourse.View.extend({ }, didInsertElement: function() { - var topicAdminMenuView = this; + var self = this; $('html').on('mouseup.discourse-topic-admin-menu', function(e) { var $target = $(e.target); - if ($target.is('button') || topicAdminMenuView.$().has($target).length === 0) { - topicAdminMenuView.get('controller').hide(); + if ($target.is('button') || self.$().has($target).length === 0) { + self.get('controller').send('hide'); } }); } diff --git a/app/assets/javascripts/docs/yuidoc.json b/app/assets/javascripts/docs/yuidoc.json index 39d6d36f3..292146820 100644 --- a/app/assets/javascripts/docs/yuidoc.json +++ b/app/assets/javascripts/docs/yuidoc.json @@ -3,7 +3,7 @@ "description": "This is the EmberJS client to access a Discourse Server", "url": "http://www.discourse.org/", "options": { - "exclude": "external,external_production,defer", + "exclude": "development,production,defer", "outdir": "./build" } } \ No newline at end of file diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 1ce1b7155..7aaea0fae 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -1,7 +1,37 @@ //= require_tree ./discourse/ember -// The rest of the externals -//= require_tree ./external +// The Vendored JS +//= require LAB.js +//= require Markdown.Converter.js +//= require Markdown.Editor.js +//= require Markdown.Sanitizer.js +//= require better_markdown.js +//= require bootbox.js +//= require bootstrap-alert.js +//= require bootstrap-button.js +//= require bootstrap-dropdown.js +//= require bootstrap-modal.js +//= require bootstrap-transition.js +//= require browser-update.js +//= require chosen.jquery.js +//= require ember-renderspeed.js +//= require favcount.js +//= require handlebars.js +//= require jquery.ba-replacetext.js +//= require jquery.ba-resize.min.js +//= require jquery.color.js +//= require jquery.cookie.js +//= require jquery.fileupload.js +//= require jquery.iframe-transport.js +//= require jquery.putcursoratend.js +//= require jquery.tagsinput.js +//= require jquery.ui.widget.js +//= require lodash.js +//= require md5.js +//= require modernizr.custom.95264.js +//= require mousetrap.js +//= require rsvp.js +//= require show-html.js //= require ./discourse/helpers/i18n_helpers //= require ./discourse/mixins/ajax @@ -39,4 +69,4 @@ //= require_tree ./discourse/templates //= require_tree ./discourse/routes -//= require ./external/browser-update.js +//= require browser-update.js diff --git a/app/assets/javascripts/main_include_admin.js b/app/assets/javascripts/main_include_admin.js new file mode 100644 index 000000000..b2c81ffd3 --- /dev/null +++ b/app/assets/javascripts/main_include_admin.js @@ -0,0 +1 @@ +//= require_tree ./admin \ No newline at end of file diff --git a/app/assets/javascripts/pagedown_custom.js b/app/assets/javascripts/pagedown_custom.js index 643fc8fb1..e7a0c885a 100644 --- a/app/assets/javascripts/pagedown_custom.js +++ b/app/assets/javascripts/pagedown_custom.js @@ -7,7 +7,7 @@ window.PagedownCustom = { description: I18n.t("composer.quote_post_title"), execute: function() { // AWFUL but I can't figure out how to call a controller method from outside our app - return Discourse.__container__.lookup('controller:composer').importQuote(); + return Discourse.__container__.lookup('controller:composer').send('importQuote'); } } ] diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 8dddd4b00..1872e1ea5 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -29,6 +29,7 @@ class TopicsController < ApplicationController return wordpress if params[:best].present? opts = params.slice(:username_filters, :filter, :page, :post_number) + begin @topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts) rescue Discourse::NotFound diff --git a/config/application.rb b/config/application.rb index 1a936197e..e069fa367 100644 --- a/config/application.rb +++ b/config/application.rb @@ -113,8 +113,8 @@ module Discourse # ember stuff only used for asset precompliation, production variant plays up config.ember.variant = :development - config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external_production/ember.js" - config.ember.handlebars_location = "#{Rails.root}/app/assets/javascripts/external/handlebars.js" + config.ember.ember_location = "#{Rails.root}/vendor/assets/javascripts/production/ember.js" + config.ember.handlebars_location = "#{Rails.root}/vendor/assets/javascripts/handlebars.js" # Since we are using strong_parameters, we can disable and remove # attr_accessible. diff --git a/config/jshint.yml b/config/jshint.yml index 588a527d8..e424b255e 100644 --- a/config/jshint.yml +++ b/config/jshint.yml @@ -13,9 +13,6 @@ paths: - test/javascripts/**/*.js exclude_paths: - - app/assets/javascripts/external/* - - app/assets/javascripts/external_production/* - - app/assets/javascripts/external_development/* - app/assets/javascripts/defer/* - app/assets/javascripts/locales/i18n.js diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 8a9603ae2..6631acd66 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -93,11 +93,11 @@ module PrettyText ctx["helpers"] = Helpers.new ctx_load(ctx, - "app/assets/javascripts/external/md5.js", - "app/assets/javascripts/external/lodash.js", - "app/assets/javascripts/external/Markdown.Converter.js", + "vendor/assets/javascripts/md5.js", + "vendor/assets/javascripts/lodash.js", + "vendor/assets/javascripts/Markdown.Converter.js", "lib/headless-ember.js", - "app/assets/javascripts/external/rsvp.js", + "vendor/assets/javascripts/rsvp.js", Rails.configuration.ember.handlebars_location) ctx.eval("var Discourse = {}; Discourse.SiteSettings = #{SiteSetting.client_settings_json};") @@ -107,7 +107,7 @@ module PrettyText decorate_context(ctx) ctx_load(ctx, - "app/assets/javascripts/external/better_markdown.js", + "vendor/assets/javascripts/better_markdown.js", "app/assets/javascripts/discourse/dialects/dialect.js", "app/assets/javascripts/discourse/components/utilities.js", "app/assets/javascripts/discourse/components/markdown.js") diff --git a/test/javascripts/controllers/avatar_selector_controller_test.js b/test/javascripts/controllers/avatar_selector_controller_test.js index 9bcc6ad17..680b8b622 100644 --- a/test/javascripts/controllers/avatar_selector_controller_test.js +++ b/test/javascripts/controllers/avatar_selector_controller_test.js @@ -14,13 +14,13 @@ test("avatarTemplate", function() { avatarSelector.get("gravatar_template"), "we are using gravatar by default"); - avatarSelectorController.useUploadedAvatar(); + avatarSelectorController.send('useUploadedAvatar'); equal(avatarSelectorController.get("avatarTemplate"), avatarSelector.get("uploaded_avatar_template"), "calling useUploadedAvatar switches to using the uploaded avatar"); - avatarSelectorController.useGravatar(); + avatarSelectorController.send('useGravatar'); equal(avatarSelectorController.get("avatarTemplate"), avatarSelector.get("gravatar_template"), diff --git a/test/javascripts/controllers/topic_controller_test.js b/test/javascripts/controllers/topic_controller_test.js index f45338c24..81370496c 100644 --- a/test/javascripts/controllers/topic_controller_test.js +++ b/test/javascripts/controllers/topic_controller_test.js @@ -18,16 +18,16 @@ test("editingMode", function() { ok(!topicController.get('editingTopic'), "we are not editing by default"); topicController.set('model.details.can_edit', false); - topicController.editTopic(); + topicController.send('editTopic'); ok(!topicController.get('editingTopic'), "calling editTopic doesn't enable editing unless the user can edit"); topicController.set('model.details.can_edit', true); - topicController.editTopic(); + topicController.send('editTopic'); ok(topicController.get('editingTopic'), "calling editTopic enables editing if the user can edit"); equal(topicController.get('newTitle'), topic.get('title')); equal(topicController.get('newCategoryId'), topic.get('category_id')); - topicController.cancelEditingTopic(); + topicController.send('cancelEditingTopic'); ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value"); }); @@ -43,12 +43,12 @@ test("toggledSelectedPost", function() { equal(tc.get('selectedPostsCount'), 0, "there is a selected post count of 0"); ok(!tc.postSelected(post), "the post is not selected by default"); - tc.toggledSelectedPost(post); + tc.send('toggledSelectedPost', post); present(tc.get('selectedPosts'), "there is a selectedPosts collection"); equal(tc.get('selectedPostsCount'), 1, "there is a selected post now"); ok(tc.postSelected(post), "the post is now selected"); - tc.toggledSelectedPost(post); + tc.send('toggledSelectedPost', post); ok(!tc.postSelected(post), "the post is no longer selected"); }); @@ -61,10 +61,10 @@ test("selectAll", function() { postStream.appendPost(post); ok(!tc.postSelected(post), "the post is not selected by default"); - tc.selectAll(); + tc.send('selectAll'); ok(tc.postSelected(post), "the post is now selected"); ok(tc.get('allPostsSelected'), "all posts are selected"); - tc.deselectAll(); + tc.send('deselectAll'); ok(!tc.postSelected(post), "the post is deselected again"); ok(!tc.get('allPostsSelected'), "all posts are not selected"); @@ -80,10 +80,10 @@ test("Automating setting of allPostsSelected", function() { postStream.appendPost(post); ok(!tc.get('allPostsSelected'), "all posts are not selected by default"); - tc.toggledSelectedPost(post); + tc.send('toggledSelectedPost', post); ok(tc.get('allPostsSelected'), "all posts are selected if we select the only post"); - tc.toggledSelectedPost(post); + tc.send('toggledSelectedPost', post); ok(!tc.get('allPostsSelected'), "the posts are no longer automatically selected"); }); @@ -96,20 +96,20 @@ test("Select Replies when present", function() { postStream = tc.get('postStream'); ok(!tc.postSelected(p3), "replies are not selected by default"); - tc.toggledSelectedPostReplies(p1); + tc.send('toggledSelectedPostReplies', p1); ok(tc.postSelected(p1), "it selects the post"); ok(!tc.postSelected(p2), "it doesn't select a post that's not a reply"); ok(tc.postSelected(p3), "it selects a post that is a reply"); equal(tc.get('selectedPostsCount'), 2, "it has a selected posts count of two"); // If we deselected the post whose replies are selected... - tc.toggledSelectedPost(p1); + tc.send('toggledSelectedPost', p1); ok(!tc.postSelected(p1), "it deselects the post"); ok(!tc.postSelected(p3), "it deselects the replies too"); // If we deselect a reply, it should deselect the parent's replies selected attribute. Weird but what else would make sense? - tc.toggledSelectedPostReplies(p1); - tc.toggledSelectedPost(p3); + tc.send('toggledSelectedPostReplies', p1); + tc.send('toggledSelectedPost', p3); ok(tc.postSelected(p1), "the post stays selected"); ok(!tc.postSelected(p3), "it deselects the replies too"); diff --git a/test/javascripts/jshint_all.js.erb b/test/javascripts/jshint_all.js.erb index 34b7eea68..05dbfaaa9 100644 --- a/test/javascripts/jshint_all.js.erb +++ b/test/javascripts/jshint_all.js.erb @@ -184,7 +184,5 @@ var jsHintOpts = { <%= jshint("#{Rails.root}/app/assets/javascripts/**/*.js", "/app/assets/javascripts/", [/external\//, - /external_development\//, - /external_production\//, /defer\//, /locales\//]) %> diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 3093140ca..94a3597f7 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -9,10 +9,10 @@ //= require ../../app/assets/javascripts/discourse/components/probes.js // Externals we need to load first -//= require ../../app/assets/javascripts/external_development/jquery-2.0.3.js -//= require ../../app/assets/javascripts/external/jquery.ui.widget.js -//= require ../../app/assets/javascripts/external/handlebars.js -//= require ../../app/assets/javascripts/external_development/ember.js +//= require development/jquery-2.0.3.js +//= require jquery.ui.widget.js +//= require handlebars.js +//= require development/ember.js //= require ../../app/assets/javascripts/locales/i18n //= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers @@ -21,8 +21,36 @@ // Pagedown customizations //= require ../../app/assets/javascripts/pagedown_custom.js -// The rest of the externals -//= require_tree ../../app/assets/javascripts/external +// The rest of the vendored JS +//= require LAB.js +//= require Markdown.Converter.js +//= require Markdown.Editor.js +//= require Markdown.Sanitizer.js +//= require better_markdown.js +//= require bootbox.js +//= require bootstrap-alert.js +//= require bootstrap-button.js +//= require bootstrap-dropdown.js +//= require bootstrap-modal.js +//= require bootstrap-transition.js +//= require browser-update.js +//= require chosen.jquery.js +//= require ember-renderspeed.js +//= require favcount.js +//= require jquery.ba-replacetext.js +//= require jquery.ba-resize.min.js +//= require jquery.color.js +//= require jquery.cookie.js +//= require jquery.fileupload.js +//= require jquery.iframe-transport.js +//= require jquery.putcursoratend.js +//= require jquery.tagsinput.js +//= require lodash.js +//= require md5.js +//= require modernizr.custom.95264.js +//= require mousetrap.js +//= require rsvp.js +//= require show-html.js // Stuff we need to load first //= require main_include diff --git a/app/assets/javascripts/external/LAB.js b/vendor/assets/javascripts/LAB.js similarity index 100% rename from app/assets/javascripts/external/LAB.js rename to vendor/assets/javascripts/LAB.js diff --git a/app/assets/javascripts/external/Markdown.Converter.js b/vendor/assets/javascripts/Markdown.Converter.js similarity index 100% rename from app/assets/javascripts/external/Markdown.Converter.js rename to vendor/assets/javascripts/Markdown.Converter.js diff --git a/app/assets/javascripts/external/Markdown.Editor.js b/vendor/assets/javascripts/Markdown.Editor.js similarity index 100% rename from app/assets/javascripts/external/Markdown.Editor.js rename to vendor/assets/javascripts/Markdown.Editor.js diff --git a/app/assets/javascripts/external/Markdown.Sanitizer.js b/vendor/assets/javascripts/Markdown.Sanitizer.js similarity index 100% rename from app/assets/javascripts/external/Markdown.Sanitizer.js rename to vendor/assets/javascripts/Markdown.Sanitizer.js diff --git a/app/assets/javascripts/external/better_markdown.js b/vendor/assets/javascripts/better_markdown.js similarity index 100% rename from app/assets/javascripts/external/better_markdown.js rename to vendor/assets/javascripts/better_markdown.js diff --git a/app/assets/javascripts/external/bootbox.js b/vendor/assets/javascripts/bootbox.js similarity index 100% rename from app/assets/javascripts/external/bootbox.js rename to vendor/assets/javascripts/bootbox.js diff --git a/app/assets/javascripts/external/bootstrap-alert.js b/vendor/assets/javascripts/bootstrap-alert.js similarity index 100% rename from app/assets/javascripts/external/bootstrap-alert.js rename to vendor/assets/javascripts/bootstrap-alert.js diff --git a/app/assets/javascripts/external/bootstrap-button.js b/vendor/assets/javascripts/bootstrap-button.js similarity index 100% rename from app/assets/javascripts/external/bootstrap-button.js rename to vendor/assets/javascripts/bootstrap-button.js diff --git a/app/assets/javascripts/external/bootstrap-dropdown.js b/vendor/assets/javascripts/bootstrap-dropdown.js similarity index 100% rename from app/assets/javascripts/external/bootstrap-dropdown.js rename to vendor/assets/javascripts/bootstrap-dropdown.js diff --git a/app/assets/javascripts/external/bootstrap-modal.js b/vendor/assets/javascripts/bootstrap-modal.js similarity index 100% rename from app/assets/javascripts/external/bootstrap-modal.js rename to vendor/assets/javascripts/bootstrap-modal.js diff --git a/app/assets/javascripts/external/bootstrap-transition.js b/vendor/assets/javascripts/bootstrap-transition.js similarity index 100% rename from app/assets/javascripts/external/bootstrap-transition.js rename to vendor/assets/javascripts/bootstrap-transition.js diff --git a/app/assets/javascripts/external/browser-update.js b/vendor/assets/javascripts/browser-update.js similarity index 100% rename from app/assets/javascripts/external/browser-update.js rename to vendor/assets/javascripts/browser-update.js diff --git a/app/assets/javascripts/external/chosen.jquery.js b/vendor/assets/javascripts/chosen.jquery.js similarity index 100% rename from app/assets/javascripts/external/chosen.jquery.js rename to vendor/assets/javascripts/chosen.jquery.js diff --git a/app/assets/javascripts/external_production/ember.js b/vendor/assets/javascripts/development/ember.js old mode 100644 new mode 100755 similarity index 74% rename from app/assets/javascripts/external_production/ember.js rename to vendor/assets/javascripts/development/ember.js index c7527f51c..6eca81224 --- a/app/assets/javascripts/external_production/ember.js +++ b/vendor/assets/javascripts/development/ember.js @@ -1,3 +1,199 @@ +// ========================================================================== +// Project: Ember - JavaScript Application Framework +// Copyright: ©2011-2013 Tilde Inc. and contributors +// Portions ©2006-2011 Strobe Inc. +// Portions ©2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license +// See https://raw.github.com/emberjs/ember.js/master/LICENSE +// ========================================================================== + + +// Version: v1.0.0-rc.6-733-gd034d11 +// Last commit: d034d11 (2013-09-16 00:44:21 -0700) + + +(function() { +/*global __fail__*/ + +/** +Ember Debug + +@module ember +@submodule ember-debug +*/ + +/** +@class Ember +*/ + +if ('undefined' === typeof Ember) { + Ember = {}; + + if ('undefined' !== typeof window) { + window.Em = window.Ember = Em = Ember; + } +} + +Ember.ENV = 'undefined' === typeof ENV ? {} : ENV; + +if (!('MANDATORY_SETTER' in Ember.ENV)) { + Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist +} + +/** + Define an assertion that will throw an exception if the condition is not + met. Ember build tools will remove any calls to `Ember.assert()` when + doing a production build. Example: + + ```javascript + // Test for truthiness + Ember.assert('Must pass a valid object', obj); + // Fail unconditionally + Ember.assert('This code path should never be run') + ``` + + @method assert + @param {String} desc A description of the assertion. This will become + the text of the Error thrown if the assertion fails. + @param {Boolean} test Must be truthy for the assertion to pass. If + falsy, an exception will be thrown. +*/ +Ember.assert = function(desc, test) { + if (!test) { + Ember.Logger.assert(test, desc); + } + + if (Ember.testing && !test) { + // when testing, ensure test failures when assertions fail + throw new Error("Assertion Failed: " + desc); + } +}; + + +/** + Display a warning with the provided message. Ember build tools will + remove any calls to `Ember.warn()` when doing a production build. + + @method warn + @param {String} message A warning to display. + @param {Boolean} test An optional boolean. If falsy, the warning + will be displayed. +*/ +Ember.warn = function(message, test) { + if (!test) { + Ember.Logger.warn("WARNING: "+message); + if ('trace' in Ember.Logger) Ember.Logger.trace(); + } +}; + +/** + Display a debug notice. Ember build tools will remove any calls to + `Ember.debug()` when doing a production build. + + ```javascript + Ember.debug("I'm a debug notice!"); + ``` + + @method debug + @param {String} message A debug message to display. +*/ +Ember.debug = function(message) { + Ember.Logger.debug("DEBUG: "+message); +}; + +/** + Display a deprecation warning with the provided message and a stack trace + (Chrome and Firefox only). Ember build tools will remove any calls to + `Ember.deprecate()` when doing a production build. + + @method deprecate + @param {String} message A description of the deprecation. + @param {Boolean} test An optional boolean. If falsy, the deprecation + will be displayed. +*/ +Ember.deprecate = function(message, test) { + if (Ember.TESTING_DEPRECATION) { return; } + + if (arguments.length === 1) { test = false; } + if (test) { return; } + + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + + var error; + + // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome + try { __fail__.fail(); } catch (e) { error = e; } + + if (Ember.LOG_STACKTRACE_ON_DEPRECATION && error.stack) { + var stack, stackStr = ''; + if (error['arguments']) { + // Chrome + stack = error.stack.replace(/^\s+at\s+/gm, ''). + replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2'). + replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); + stack.shift(); + } else { + // Firefox + stack = error.stack.replace(/(?:\n@:0)?\s+$/m, ''). + replace(/^\(/gm, '{anonymous}(').split('\n'); + } + + stackStr = "\n " + stack.slice(2).join("\n "); + message = message + stackStr; + } + + Ember.Logger.warn("DEPRECATION: "+message); +}; + + + +/** + Display a deprecation warning with the provided message and a stack trace + (Chrome and Firefox only) when the wrapped method is called. + + Ember build tools will not remove calls to `Ember.deprecateFunc()`, though + no warnings will be shown in production. + + @method deprecateFunc + @param {String} message A description of the deprecation. + @param {Function} func The function to be deprecated. + @return {Function} a new function that wrapped the original function with a deprecation warning +*/ +Ember.deprecateFunc = function(message, func) { + return function() { + Ember.deprecate(message); + return func.apply(this, arguments); + }; +}; + + +// Inform the developer about the Ember Inspector if not installed. +if (!Ember.testing) { + if (typeof window !== 'undefined' && window.chrome && window.addEventListener) { + window.addEventListener("load", function() { + if (document.body && document.body.dataset && !document.body.dataset.emberExtension) { + Ember.debug('For more advanced debugging, install the Ember Inspector from https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'); + } + }, false); + } +} + +})(); + +// ========================================================================== +// Project: Ember - JavaScript Application Framework +// Copyright: ©2011-2013 Tilde Inc. and contributors +// Portions ©2006-2011 Strobe Inc. +// Portions ©2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license +// See https://raw.github.com/emberjs/ember.js/master/LICENSE +// ========================================================================== + + +// Version: v1.0.0-rc.6-733-gd034d11 +// Last commit: d034d11 (2013-09-16 00:44:21 -0700) + + (function() { var define, requireModule; @@ -23,7 +219,6 @@ var define, requireModule; deps = mod.deps; callback = mod.callback; reified = []; - exports; for (var i=0, l=deps.length; i<l; i++) { if (deps[i] === 'exports') { @@ -62,7 +257,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.6 + @version 1.0.0 */ if ('undefined' === typeof Ember) { @@ -89,10 +284,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.6.1' + @default '1.0.0' @final */ -Ember.VERSION = '1.0.0-rc.6.1'; +Ember.VERSION = '1.0.0'; /** Standard environmental variables. You can define these in a global `ENV` @@ -102,10 +297,42 @@ Ember.VERSION = '1.0.0-rc.6.1'; @property ENV @type Hash */ -Ember.ENV = Ember.ENV || ('undefined' === typeof ENV ? {} : ENV); + +if ('undefined' === typeof ENV) { + exports.ENV = {}; +} + +// We disable the RANGE API by default for performance reasons +if ('undefined' === typeof ENV.DISABLE_RANGE_API) { + ENV.DISABLE_RANGE_API = true; +} + + +Ember.ENV = Ember.ENV || ENV; Ember.config = Ember.config || {}; +/** + Hash of enabled Canary features. Add to before creating your application. + + @property FEATURES + @type Hash +*/ + +Ember.FEATURES = {}; + +/** + Test that a feature is enabled. Parsed by Ember's build tools to leave + experimental features out of beta/stable builds. + + @method isEnabled + @param {string} feature +*/ + +Ember.FEATURES.isEnabled = function(feature) { + return Ember.FEATURES[feature]; +}; + // .......................................................... // BOOTSTRAP // @@ -158,7 +385,7 @@ Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPE Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; /** - Empty function. Useful for some operations. + Empty function. Useful for some operations. Always returns `this`. @method K @private @@ -192,14 +419,20 @@ Ember.uuid = 0; // function consoleMethod(name) { - var console = imports.console, - method = typeof console === 'object' ? console[name] : null; + var consoleObj; + if (imports.console) { + consoleObj = imports.console; + } else if (typeof console !== 'undefined') { + consoleObj = console; + } + + var method = typeof consoleObj === 'object' ? consoleObj[name] : null; if (method) { // Older IE doesn't support apply, but Chrome needs it if (method.apply) { return function() { - method.apply(console, arguments); + method.apply(consoleObj, arguments); }; } else { return function() { @@ -231,11 +464,87 @@ function assertPolyfill(test, message) { @namespace Ember */ Ember.Logger = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method log + @for Ember.Logger + @param {*} arguments + */ log: consoleMethod('log') || Ember.K, + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.warn('Something happened!'); // "Something happened!" will be printed to the console with a warning icon. + ``` + + @method warn + @for Ember.Logger + @param {*} arguments + */ warn: consoleMethod('warn') || Ember.K, + /** + Prints the arguments to the console with an error icon, red text and a stack race. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.error('Danger! Danger!'); // "Danger! Danger!" will be printed to the console in red text. + ``` + + @method error + @for Ember.Logger + @param {*} arguments + */ error: consoleMethod('error') || Ember.K, + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method info + @for Ember.Logger + @param {*} arguments + */ info: consoleMethod('info') || Ember.K, + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method debug + @for Ember.Logger + @param {*} arguments + */ debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + /** + + If the value passed into Ember.Logger.assert is not truthy it will throw an error with a stack trace. + + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ assert: consoleMethod('assert') || assertPolyfill }; @@ -249,6 +558,15 @@ Ember.Logger = { internals encounter an error. This is useful for specialized error handling and reporting code. + ```javascript + Ember.onerror = function(error) { + Em.$.ajax('/report-error', 'POST', { + stack: error.stack, + otherInformation: 'whatever app state you want to provide' + }); + }; + ``` + @event onerror @for Ember @param {Exception} error the error object @@ -279,6 +597,21 @@ Ember.handleErrors = function(func, context) { } }; +/** + Merge the contents of two objects together into the first object. + + ```javascript + Ember.merge({first: 'Tom'}, {last: 'Dale'}); // {first: 'Tom', last: 'Dale'} + var a = {first: 'Yehuda'}, b = {last: 'Katz'}; + Ember.merge(a, b); // a == {first: 'Yehuda', last: 'Katz'}, b == {last: 'Katz'} + ``` + + @method merge + @for Ember + @param {Object} original The object to merge into + @param {Object} updates The object to copy properties from + @return {Object} +*/ Ember.merge = function(original, updates) { for (var prop in updates) { if (!updates.hasOwnProperty(prop)) { continue; } @@ -581,6 +914,12 @@ var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.index return -1; }; +/** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills +*/ Ember.ArrayPolyfills = { map: arrayMap, forEach: arrayForEach, @@ -876,7 +1215,7 @@ Ember.setMeta = function setMeta(obj, property, value) { shared with its constructor */ Ember.metaPath = function metaPath(obj, path, writable) { - + Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); var meta = Ember.meta(obj, writable), keyName, value; for (var i=0, l=path.length; i<l; i++) { @@ -925,6 +1264,7 @@ Ember.wrap = function(func, superFunc) { superWrapper.wrappedFunction = func; superWrapper.__ember_observes__ = func.__ember_observes__; superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__; + superWrapper.__ember_listens__ = func.__ember_listens__; return superWrapper; }; @@ -993,10 +1333,18 @@ function canInvoke(obj, methodName) { /** Checks to see if the `methodName` exists on the `obj`. + ```javascript + var foo = {bar: Ember.K, baz: null}; + Ember.canInvoke(foo, 'bar'); // true + Ember.canInvoke(foo, 'baz'); // false + Ember.canInvoke(foo, 'bat'); // false + ``` + @method canInvoke @for Ember @param {Object} obj The object to check for the method @param {String} methodName The method name to check for + @return {Boolean} */ Ember.canInvoke = canInvoke; @@ -1004,6 +1352,13 @@ Ember.canInvoke = canInvoke; Checks to see if the `methodName` exists on the `obj`, and if it does, invokes it with the arguments passed. + ```javascript + var d = new Date('03/15/2013'); + Ember.tryInvoke(d, 'getTime'); // 1363320000000 + Ember.tryInvoke(d, 'setFullYear', [2014]); // 1394856000000 + Ember.tryInvoke(d, 'noSuchMethod', [2014]); // undefined + ``` + @method tryInvoke @for Ember @param {Object} obj The object to check for the method @@ -1035,6 +1390,17 @@ var needsFinallyFix = (function() { Provides try { } finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + someResource.lock(); + runCallback(); // May throw error. + }; + var finalizer = function() { + someResource.unlock(); + }; + Ember.tryFinally(tryable, finalizer); + ``` + @method tryFinally @for Ember @param {Function} tryable The function to run the try callback @@ -1085,6 +1451,30 @@ if (needsFinallyFix) { Provides try { } catch finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + for (i=0, l=listeners.length; i<l; i++) { + listener = listeners[i]; + beforeValues[i] = listener.before(name, time(), payload); + } + + return callback.call(binding); + }; + + var catchable = function(e) { + payload = payload || {}; + payload.exception = e; + }; + + var finalizer = function() { + for (i=0, l=listeners.length; i<l; i++) { + listener = listeners[i]; + listener.after(name, time(), payload, beforeValues[i]); + } + }; + Ember.tryCatchFinally(tryable, catchable, finalizer); + ``` + @method tryCatchFinally @for Ember @param {Function} tryable The function to run the try callback @@ -1280,13 +1670,23 @@ var populateListeners = function(name) { }; var time = (function() { - var perf = 'undefined' !== typeof window ? window.performance || {} : {}; - var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow; - // fn.bind will be available in all the browsers that support the advanced window.performance... ;-) - return fn ? fn.bind(perf) : function() { return +new Date(); }; + var perf = 'undefined' !== typeof window ? window.performance || {} : {}; + var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow; + // fn.bind will be available in all the browsers that support the advanced window.performance... ;-) + return fn ? fn.bind(perf) : function() { return +new Date(); }; })(); +/** + Notifies event's subscribers, calls `before` and `after` hooks. + @method instrument + @namespace Ember.Instrumentation + + @param {String} [name] Namespaced event name. + @param {Object} payload + @param {Function} callback Function that you're instrumenting. + @param {Object} binding Context that instrument function is called with. +*/ Ember.Instrumentation.instrument = function(name, payload, callback, binding) { var listeners = cache[name], timeName, ret; @@ -1335,6 +1735,15 @@ Ember.Instrumentation.instrument = function(name, payload, callback, binding) { return Ember.tryCatchFinally(tryable, catchable, finalizer); }; +/** + Subscribes to a particular event or instrumented block of code. + + @method subscribe + @namespace Ember.Instrumentation + + @param {String} [pattern] Namespaced event name. + @param {Object} [object] Before and After hooks. +*/ Ember.Instrumentation.subscribe = function(pattern, object) { var paths = pattern.split("."), path, regex = []; @@ -1362,6 +1771,14 @@ Ember.Instrumentation.subscribe = function(pattern, object) { return subscriber; }; +/** + Unsubscribes from a particular event or instrumented block of code. + + @method unsubscribe + @namespace Ember.Instrumentation + + @param {Object} [subscriber] +*/ Ember.Instrumentation.unsubscribe = function(subscriber) { var index; @@ -1375,6 +1792,12 @@ Ember.Instrumentation.unsubscribe = function(subscriber) { cache = {}; }; +/** + Resets `Ember.Instrumentation` by flushing list of subscribers. + + @method reset + @namespace Ember.Instrumentation +*/ Ember.Instrumentation.reset = function() { subscribers = []; cache = {}; @@ -1382,17 +1805,16 @@ Ember.Instrumentation.reset = function() { Ember.instrument = Ember.Instrumentation.instrument; Ember.subscribe = Ember.Instrumentation.subscribe; - })(); (function() { -var map, forEach, indexOf, concat; -concat = Array.prototype.concat; +var map, forEach, indexOf, splice; map = Array.prototype.map || Ember.ArrayPolyfills.map; forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; +splice = Array.prototype.splice; var utils = Ember.EnumerableUtils = { map: function(obj, callback, thisArg) { @@ -1423,12 +1845,31 @@ var utils = Ember.EnumerableUtils = { if (index !== -1) { array.splice(index, 1); } }, + _replace: function(array, idx, amt, objects) { + var args = [].concat(objects), chunk, ret = [], + // https://code.google.com/p/chromium/issues/detail?id=56588 + size = 60000, start = idx, ends = amt, count; + + while (args.length) { + count = ends > size ? size : ends; + if (count <= 0) { count = 0; } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + }, + replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = concat.apply([idx, amt], objects); - return array.splice.apply(array, args); + return utils._replace(array, idx, amt, objects); } }, @@ -1504,7 +1945,8 @@ get = function get(obj, keyName) { obj = null; } - + Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); + Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); if (obj === null || keyName.indexOf('.') !== -1) { return getPath(obj, keyName); @@ -1606,7 +2048,6 @@ Ember.getWithDefault = function(root, key, defaultValue) { Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); })(); @@ -1620,6 +2061,7 @@ Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now support var o_create = Ember.create, metaFor = Ember.meta, META_KEY = Ember.META_KEY, + a_slice = [].slice, /* listener flags */ ONCE = 1, SUSPENDED = 2; @@ -1634,7 +2076,7 @@ var o_create = Ember.create, { listeners: { // variable name: `listenerSet` "foo:changed": [ // variable name: `actions` - [target, method, flags] + target, method, flags ] } } @@ -1643,8 +2085,8 @@ var o_create = Ember.create, function indexOf(array, target, method) { var index = -1; - for (var i = 0, l = array.length; i < l; i++) { - if (target === array[i][0] && method === array[i][1]) { index = i; break; } + for (var i = 0, l = array.length; i < l; i += 3) { + if (target === array[i] && method === array[i+1]) { index = i; break; } } return index; } @@ -1677,14 +2119,14 @@ function actionsUnion(obj, eventName, otherActions) { actions = meta && meta.listeners && meta.listeners[eventName]; 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) { - otherActions.push([target, method, flags]); + otherActions.push(target, method, flags); } } } @@ -1695,16 +2137,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; @@ -1722,7 +2164,7 @@ function actionsDiff(obj, eventName, otherActions) { @param {Boolean} once A flag whether a function should only be called once */ function addListener(obj, eventName, target, method, once) { - + Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); if (!method && 'function' === typeof target) { method = target; @@ -1737,7 +2179,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); @@ -1757,7 +2199,7 @@ function addListener(obj, eventName, target, method, once) { @param {Function|String} method A function or the name of a function to be called on `target` */ function removeListener(obj, eventName, target, method) { - + Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); if (!method && 'function' === typeof target) { method = target; @@ -1771,7 +2213,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); @@ -1785,8 +2227,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]); } } } @@ -1816,17 +2258,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); } @@ -1836,7 +2275,7 @@ function suspendListener(obj, eventName, target, method, callback) { Suspends multiple listeners during a callback. - + @method suspendListeners @for Ember @param obj @@ -1852,7 +2291,8 @@ function suspendListeners(obj, eventNames, target, method, callback) { } var suspendedActions = [], - eventName, actions, action, i, l; + actionsList = [], + eventName, actions, i, l; for (i=0, l=eventNames.length; i<l; i++) { eventName = eventNames[i]; @@ -1860,18 +2300,18 @@ function suspendListeners(obj, eventNames, target, method, callback) { var actionIndex = indexOf(actions, target, method); if (actionIndex !== -1) { - action = actions[actionIndex].slice(); - action[2] |= SUSPENDED; - actions[actionIndex] = action; - suspendedActions.push(action); + actions[actionIndex+2] |= SUSPENDED; + suspendedActions.push(actionIndex); + actionsList.push(actions); } } function tryable() { return callback.call(target); } function finalizer() { - for (i = 0, l = suspendedActions.length; i < l; i++) { - suspendedActions[i][2] &= ~SUSPENDED; + for (var i = 0, l = suspendedActions.length; i < l; i++) { + var actionIndex = suspendedActions[i]; + actionsList[i][actionIndex+2] &= ~SUSPENDED; } } @@ -1903,7 +2343,7 @@ function watchedEvents(obj) { is skipped, and once listeners are removed. A listener without a target is executed on the passed object. If an array of actions is not passed, the actions stored on the passed object are invoked. - + @method sendEvent @for Ember @param obj @@ -1925,10 +2365,9 @@ function sendEvent(obj, eventName, params, actions) { if (!actions) { return; } - for (var i = actions.length - 1; 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; } @@ -1970,15 +2409,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; @@ -2093,7 +2557,7 @@ var metaFor = Ember.meta, @param {String} keyName The property key (or path) that will change. @return {void} */ -var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { +function propertyWillChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -2105,7 +2569,8 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { dependentKeysWillChange(obj, keyName, m); chainsWillChange(obj, keyName, m); notifyBeforeObservers(obj, keyName); -}; +} +Ember.propertyWillChange = propertyWillChange; /** This function is called just after an object property has changed. @@ -2113,7 +2578,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { Normally you will not need to call this method directly but if for some reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyWilLChange()` which you should call just + manually along with `Ember.propertyWillChange()` which you should call just before the property value changes. @method propertyDidChange @@ -2122,7 +2587,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { @param {String} keyName The property key (or path) that will change. @return {void} */ -var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { +function propertyDidChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -2135,9 +2600,10 @@ var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { if (!watching && keyName !== 'length') { return; } dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); notifyObservers(obj, keyName); -}; +} +Ember.propertyDidChange = propertyDidChange; var WILL_SEEN, DID_SEEN; @@ -2178,32 +2644,47 @@ function iterDeps(method, obj, depKey, seen, meta) { } } -var chainsWillChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); +function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; } -}; -var chainsDidChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var nodes = m.chainWatchers[keyName], + events = [], + i, l; - var nodes = m.chainWatchers; - - 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[i].didChange(arg); + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); } -}; + + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i+1]); + } +} + +function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = suppressEvents ? null : [], + i, l; + + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } + + if (suppressEvents) { + return; + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i+1]); + } +} Ember.overrideChains = function(obj, keyName, m) { chainsDidChange(obj, keyName, m, true); @@ -2213,20 +2694,24 @@ Ember.overrideChains = function(obj, keyName, m) { @method beginPropertyChanges @chainable */ -var beginPropertyChanges = Ember.beginPropertyChanges = function() { +function beginPropertyChanges() { deferred++; -}; +} + +Ember.beginPropertyChanges = beginPropertyChanges; /** @method endPropertyChanges */ -var endPropertyChanges = Ember.endPropertyChanges = function() { +function endPropertyChanges() { deferred--; if (deferred<=0) { beforeObserverSet.clear(); observerSet.flush(); } -}; +} + +Ember.endPropertyChanges = endPropertyChanges; /** Make a series of property changes together in an @@ -2248,7 +2733,7 @@ Ember.changeProperties = function(cb, binding) { tryFinally(cb, endPropertyChanges, binding); }; -var notifyBeforeObservers = function(obj, keyName) { +function notifyBeforeObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':before', listeners, diff; @@ -2259,9 +2744,9 @@ var notifyBeforeObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} -var notifyObservers = function(obj, keyName) { +function notifyObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':change', listeners; @@ -2271,7 +2756,7 @@ var notifyObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} })(); @@ -2290,7 +2775,7 @@ var META_KEY = Ember.META_KEY, /** Sets the value of a property on an object, respecting computed properties and notifying observers and other listeners of the change. If the - property is not defined but the object implements the `unknownProperty` + property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. If you plan to run on IE8 and older browsers then you should use this @@ -2300,7 +2785,7 @@ var META_KEY = Ember.META_KEY, On all newer browsers, you only need to use this method to set properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this + to respect the `setUnknownProperty` handler. Otherwise you can ignore this method. @method set @@ -2312,18 +2797,20 @@ var META_KEY = Ember.META_KEY, */ var set = function set(obj, keyName, value, tolerant) { if (typeof obj === 'string') { - + Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); value = keyName; keyName = obj; obj = null; } + Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); if (!obj || keyName.indexOf('.') !== -1) { return setPath(obj, keyName, value, tolerant); } - + Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember.assert('calling set on destroyed object', !obj.isDestroyed); var meta = obj[META_KEY], desc = meta && meta.descs[keyName], isUnknown, currentValue; @@ -2399,7 +2886,6 @@ function setPath(root, path, value, tolerant) { } Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); /** Error-tolerant form of `Ember.set`. Will not blow up if any part of the @@ -2417,7 +2903,6 @@ Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now support Ember.trySet = function(root, path, value) { return set(root, path, value, true); }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); })(); @@ -2629,14 +3114,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. @@ -2835,7 +3320,7 @@ Ember.Descriptor = function() {}; // var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) { - + Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false); }; var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { @@ -2917,7 +3402,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) { @@ -2958,6 +3442,47 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { +(function() { +var get = Ember.get; + +/** + To get multiple properties at once, call `Ember.getProperties` + with an object followed by a list of strings or an array: + + ```javascript + Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param obj + @param {String...|Array} list of keys to get + @return {Hash} +*/ +Ember.getProperties = function(obj) { + var ret = {}, + propertyNames = arguments, + i = 1; + + if (arguments.length === 2 && Ember.typeOf(arguments[1]) === 'array') { + i = 0; + propertyNames = arguments[1]; + } + for(var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = get(obj, propertyNames[i]); + } + return ret; +}; + +})(); + + + (function() { var changeProperties = Ember.changeProperties, set = Ember.set; @@ -2967,6 +3492,14 @@ var changeProperties = Ember.changeProperties, a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered. + ```javascript + anObject.setProperties({ + firstName: "Stanley", + lastName: "Stuart", + age: "21" + }) + ``` + @method setProperties @param self @param {Object} hash @@ -2995,13 +3528,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); @@ -3023,13 +3554,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); @@ -3048,6 +3576,7 @@ Ember.unwatchKey = function(obj, keyName) { watching[keyName]--; } }; + })(); @@ -3060,8 +3589,6 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange, FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { @@ -3116,10 +3643,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. @@ -3154,10 +3677,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; }; @@ -3277,42 +3822,50 @@ ChainNodePrototype.unchain = function(key, path) { }; -ChainNodePrototype.willChange = function() { +ChainNodePrototype.willChange = function(events) { var chains = this._chains; if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].willChange(); + chains[key].willChange(events); } } - if (this._parent) { this._parent.chainWillChange(this, this._key, 1); } + if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } }; -ChainNodePrototype.chainWillChange = function(chain, path, depth) { +ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainWillChange(this, path, depth+1); + this._parent.chainWillChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyWillChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.chainDidChange = function(chain, path, depth) { +ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainDidChange(this, path, depth+1); + this._parent.chainDidChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyDidChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.didChange = function(suppressEvent) { +ChainNodePrototype.didChange = function(events) { // invalidate my own value first. if (this._watching) { var obj = this._parent.value(); @@ -3334,14 +3887,25 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].didChange(suppressEvent); + chains[key].didChange(events); } } - if (suppressEvent) { return; } + // if no events are passed in then we only care about the above wiring update + if (events === null) { return; } // and finally tell parent about my path changing... - if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } + if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } +}; + +Ember.finishChains = function(obj) { + var m = metaFor(obj, false), chains = m.chains; + if (chains) { + if (chains.value() !== obj) { + m.chains = chains = chains.copy(obj); + } + chains.didChange(null); + } }; })(); @@ -3536,6 +4100,7 @@ Ember.destroy = function (obj) { @module ember-metal */ +Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false); var get = Ember.get, @@ -3623,6 +4188,81 @@ function removeDependentKeys(desc, obj, keyName, meta) { // /** + A computed property transforms an objects function into a property. + + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property is dependent on. This will force the cached + result to be recomputed if the dependencies are modified. + + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the properties + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as it's dependencies have not been changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. + + ```javascript + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); + + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); + + tom.get('fullName') // "Tom Dale" + ``` + + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. + + ```javascript + + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(" "); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + person.set('fullName', "Peter Wagenet"); + person.get('firstName') // Peter + person.get('lastName') // Wagenet + ``` + @class ComputedProperty @namespace Ember @extends Ember.Descriptor @@ -3641,7 +4281,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/* +/** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -3767,25 +4407,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]; - - if (!(keyName in meta.cache)) { - addDependentKeys(this, obj, keyName, meta); - } -}; - -ComputedPropertyPrototype.didUnwatch = function(obj, keyName) { - var meta = obj[META_KEY]; - - 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 @@ -3794,31 +4415,76 @@ 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); } } }; -/* impl descriptor API */ +function finishChains(chainNodes) +{ + for (var i=0, l=chainNodes.length; i<l; i++) { + chainNodes[i].didChange(null); + } +} + +/** + Access the value of the function backing the computed property. + If this property has already been cached, return the cached result. + Otherwise, call the function passing the property name as an argument. + + ```javascript + Person = Ember.Object.extend({ + fullName: function(keyName) { + // the keyName parameter is 'fullName' in this case. + + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + }); + + + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); + + tom.get('fullName') // "Tom Dale" + ``` + + @method get + @param {String} keyName The key being accessed. + @return {Object} The return value of the function backing the CP. +*/ ComputedPropertyPrototype.get = function(obj, keyName) { - var ret, cache, meta; + var ret, cache, meta, chainNodes; if (this._cacheable) { meta = metaFor(obj); cache = meta.cache; if (keyName in cache) { return cache[keyName]; } ret = cache[keyName] = this.func.call(obj, keyName); - if (!meta.watching[keyName]) { - addDependentKeys(this, obj, keyName, meta); - } + chainNodes = meta.chainWatchers && meta.chainWatchers[keyName]; + if (chainNodes) { finishChains(chainNodes); } + addDependentKeys(this, obj, keyName, meta); } else { ret = this.func.call(obj, keyName); } return ret; }; -/* impl descriptor API */ +/** + Set the value of a computed property. If the function that backs your + computed property does not accept arguments then the default action for + setting would be to define the property on the current object, and set + the value of the property to the value being set. + + Generally speaking if you intend for your computed property to be set + your backing function should accept either two or three arguments. + + @method set + @param {String} keyName The key being accessed. + @param {Object} newValue The new value being assigned. + @param {String} oldValue The old value being replaced. + @return {Object} The return value of the function backing the CP. +*/ ComputedPropertyPrototype.set = function(obj, keyName, value) { var cacheable = this._cacheable, func = this.func, @@ -3870,7 +4536,7 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { } if (cacheable) { - if (!watched && !hadCachedValue) { + if (!hadCachedValue) { addDependentKeys(this, obj, keyName, meta); } cache[keyName] = ret; @@ -3883,19 +4549,11 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { return ret; }; -/* called when property is defined */ -ComputedPropertyPrototype.setup = function(obj, keyName) { - var meta = obj[META_KEY]; - if (meta && meta.watching[keyName]) { - addDependentKeys(this, obj, keyName, metaFor(obj)); - } -}; - /* called before property is overridden */ ComputedPropertyPrototype.teardown = function(obj, keyName) { var meta = metaFor(obj); - if (meta.watching[keyName] || keyName in meta.cache) { + if (keyName in meta.cache) { removeDependentKeys(this, obj, keyName, meta); } @@ -3991,6 +4649,25 @@ function registerComputedWithProperties(name, macro) { } /** + A computed property that returns true if the value of the dependent + property is null, an empty string, empty array, or empty function. + + Note: When using `Ember.computed.empty` to watch an array make sure to + use the `array.length` syntax so the computed can subscribe to transitions + from empty to non-empty states. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + done: Ember.computed.empty('todos.length') + }); + var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); + todoList.get('done'); // false + todoList.get('todos').clear(); // [] + todoList.get('done'); // true + ``` + @method computed.empty @for Ember @param {String} dependentKey @@ -4002,6 +4679,21 @@ registerComputed('empty', function(dependentKey) { }); /** + A computed property that returns true if the value of the dependent + property is NOT null, an empty string, empty array, or empty function. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasStuff: Ember.computed.notEmpty('backpack') + }); + var hampster = Hampster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); + hampster.get('hasStuff'); // true + hampster.get('backpack').clear(); // [] + hampster.get('hasStuff'); // false + ``` + @method computed.notEmpty @for Ember @param {String} dependentKey @@ -4013,6 +4705,24 @@ registerComputed('notEmpty', function(dependentKey) { }); /** + A computed property that returns true if the value of the dependent + property is null or undefined. This avoids errors from JSLint complaining + about use of ==, which can be technically confusing. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + isHungry: Ember.computed.none('food') + }); + var hampster = Hampster.create(); + hampster.get('isHungry'); // true + hampster.set('food', 'Banana'); + hampster.get('isHungry'); // false + hampster.set('food', null); + hampster.get('isHungry'); // true + ``` + @method computed.none @for Ember @param {String} dependentKey @@ -4024,6 +4734,21 @@ registerComputed('none', function(dependentKey) { }); /** + A computed property that returns the inverse boolean value + of the original value for the dependent property. + + Example + + ```javascript + var User = Ember.Object.extend({ + isAnonymous: Ember.computed.not('loggedIn') + }); + var user = User.create({loggedIn: false}); + user.get('isAnonymous'); // true + user.set('loggedIn', true); + user.get('isAnonymous'); // false + ``` + @method computed.not @for Ember @param {String} dependentKey @@ -4035,6 +4760,23 @@ registerComputed('not', function(dependentKey) { }); /** + A computed property that converts the provided dependent property + into a boolean value. + + ```javascript + var Hampster = Ember.Object.extend({ + hasBananas: Ember.computed.bool('numBananas') + }); + var hampster = Hampster.create(); + hampster.get('hasBananas'); // false + hampster.set('numBananas', 0); + hampster.get('hasBananas'); // false + hampster.set('numBananas', 1); + hampster.get('hasBananas'); // true + hampster.set('numBananas', null); + hampster.get('hasBananas'); // false + ``` + @method computed.bool @for Ember @param {String} dependentKey @@ -4046,6 +4788,24 @@ registerComputed('bool', function(dependentKey) { }); /** + A computed property which matches the original value for the + dependent property against a given RegExp, returning `true` + if they values matches the RegExp and `false` if it does not. + + Example + + ```javascript + var User = Ember.Object.extend({ + hasValidEmail: Ember.computed.match('email', /^.+@.+\..+$/) + }); + var user = User.create({loggedIn: false}); + user.get('hasValidEmail'); // false + user.set('email', ''); + user.get('hasValidEmail'); // false + user.set('email', 'ember_hampster@example.com'); + user.get('hasValidEmail'); // true + ``` + @method computed.match @for Ember @param {String} dependentKey @@ -4059,6 +4819,23 @@ registerComputed('match', function(dependentKey, regexp) { }); /** + A computed property that returns true if the provided dependent property + is equal to the given value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + napTime: Ember.computed.equal('state', 'sleepy') + }); + var hampster = Hampster.create(); + hampster.get('napTime'); // false + hampster.set('state', 'sleepy'); + hampster.get('napTime'); // true + hampster.set('state', 'hungry'); + hampster.get('napTime'); // false + ``` + @method computed.equal @for Ember @param {String} dependentKey @@ -4071,6 +4848,23 @@ registerComputed('equal', function(dependentKey, value) { }); /** + A computed property that returns true if the provied dependent property + is greater than the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gt('numBananas', 10) + }); + var hampster = Hampster.create(); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 3); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 11); + hampster.get('hasTooManyBananas'); // true + ``` + @method computed.gt @for Ember @param {String} dependentKey @@ -4083,6 +4877,23 @@ registerComputed('gt', function(dependentKey, value) { }); /** + A computed property that returns true if the provided dependent property + is greater than or equal to the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gte('numBananas', 10) + }); + var hampster = Hampster.create(); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 3); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 10); + hampster.get('hasTooManyBananas'); // true + ``` + @method computed.gte @for Ember @param {String} dependentKey @@ -4095,6 +4906,23 @@ registerComputed('gte', function(dependentKey, value) { }); /** + A computed property that returns true if the provided dependent property + is less than the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lt('numBananas', 3) + }); + var hampster = Hampster.create(); + hampster.get('needsMoreBananas'); // true + hampster.set('numBananas', 3); + hampster.get('needsMoreBananas'); // false + hampster.set('numBananas', 2); + hampster.get('needsMoreBananas'); // true + ``` + @method computed.lt @for Ember @param {String} dependentKey @@ -4107,6 +4935,23 @@ registerComputed('lt', function(dependentKey, value) { }); /** + A computed property that returns true if the provided dependent property + is less than or equal to the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lte('numBananas', 3) + }); + var hampster = Hampster.create(); + hampster.get('needsMoreBananas'); // true + hampster.set('numBananas', 5); + hampster.get('needsMoreBananas'); // false + hampster.set('numBananas', 3); + hampster.get('needsMoreBananas'); // true + ``` + @method computed.lte @for Ember @param {String} dependentKey @@ -4119,10 +4964,28 @@ registerComputed('lte', function(dependentKey, value) { }); /** + A computed property that performs a logical `and` on the + original values for the provided dependent properties. + + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') + }); + var hampster = Hampster.create(); + hampster.get('readyForCamp'); // false + hampster.set('hasTent', true); + hampster.get('readyForCamp'); // false + hampster.set('hasBackpack', true); + hampster.get('readyForCamp'); // true + ``` + @method computed.and @for Ember @param {String} dependentKey, [dependentKey...] - @return {Ember.ComputedProperty} computed property which peforms + @return {Ember.ComputedProperty} computed property which performs a logical `and` on the values of all the original values for properties. */ registerComputedWithProperties('and', function(properties) { @@ -4135,10 +4998,25 @@ registerComputedWithProperties('and', function(properties) { }); /** + A computed property that which performs a logical `or` on the + original values for the provided dependent properties. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + readyForRain: Ember.computed.or('hasJacket', 'hasUmbrella') + }); + var hampster = Hampster.create(); + hampster.get('readyForRain'); // false + hampster.set('hasJacket', true); + hampster.get('readyForRain'); // true + ``` + @method computed.or @for Ember @param {String} dependentKey, [dependentKey...] - @return {Ember.ComputedProperty} computed property which peforms + @return {Ember.ComputedProperty} computed property which performs a logical `or` on the values of all the original values for properties. */ registerComputedWithProperties('or', function(properties) { @@ -4151,6 +5029,21 @@ registerComputedWithProperties('or', function(properties) { }); /** + A computed property that returns the first truthy value + from a list of dependent properties. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasClothes: Ember.computed.any('hat', 'shirt') + }); + var hampster = Hampster.create(); + hampster.get('hasClothes'); // null + hampster.set('shirt', 'Hawaiian Shirt'); + hampster.get('hasClothes'); // 'Hawaiian Shirt' + ``` + @method computed.any @for Ember @param {String} dependentKey, [dependentKey...] @@ -4167,13 +5060,29 @@ registerComputedWithProperties('any', function(properties) { }); /** + A computed property that returns the array of values + for the provided dependent properties. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + clothes: Ember.computed.map('hat', 'shirt') + }); + var hampster = Hampster.create(); + hampster.get('clothes'); // [null, null] + hampster.set('hat', 'Camp Hat'); + hampster.set('shirt', 'Camp Shirt'); + hampster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] + ``` + @method computed.map @for Ember @param {String} dependentKey, [dependentKey...] @return {Ember.ComputedProperty} computed property which maps values of all passed properties in to an array. */ -registerComputedWithProperties('map', function(properties) { +registerComputedWithProperties('collect', function(properties) { var res = []; for (var key in properties) { if (properties.hasOwnProperty(key)) { @@ -4223,18 +5132,14 @@ Ember.computed.alias = function(dependentKey) { }; /** - @method computed.oneWay - @for Ember - @param {String} 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 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 diverge from the upstream property. + Example + ```javascript User = Ember.Object.extend({ firstName: null, @@ -4256,6 +5161,12 @@ Ember.computed.alias = function(dependentKey) { user.get('firstName'); # 'Teddy' ``` + + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + one way computed property to the original value for property. */ Ember.computed.oneWay = function(dependentKey) { return Ember.computed(dependentKey, function() { @@ -4265,6 +5176,23 @@ Ember.computed.oneWay = function(dependentKey) { /** + A computed property that acts like a standard getter and setter, + but retruns the value at the provided `defaultPath` if the + property itself has not been set to a value + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + wishList: Ember.computed.defaultTo('favoriteFood') + }); + var hampster = Hampster.create({favoriteFood: 'Banana'}); + hampster.get('wishList'); // 'Banana' + hampster.set('wishList', 'More Unit Tests'); + hampster.get('wishList'); // 'More Unit Tests' + hampster.get('favoriteFood'); // 'Banana' + ``` + @method computed.defaultTo @for Ember @param {String} defaultPath @@ -4280,6 +5208,8 @@ Ember.computed.defaultTo = function(defaultPath) { }); }; + + })(); @@ -4669,9 +5599,6 @@ define("backburner", run: function(target, method /*, args */) { var ret; - - var t2 = new Date().getTime(); - this.begin(); if (!method) { @@ -4697,13 +5624,6 @@ define("backburner", this.end(); } } - - var diff = new Date().getTime() - t2; - - if ((typeof console !== 'undefined') && console.log && diff > 10) { - console.log("Backburner: " + (new Date() - t2) + "ms"); - } - return ret; }, @@ -4846,7 +5766,7 @@ define("backburner", clearTimeout(debouncee[2]); } - var timer = window.setTimeout(function() { + var timer = global.setTimeout(function() { if (!immediate) { self.run.apply(self, args); } @@ -4915,8 +5835,8 @@ define("backburner", function createAutorun(backburner) { backburner.begin(); autorun = global.setTimeout(function() { - backburner.end(); autorun = null; + backburner.end(); }); } @@ -5484,7 +6404,7 @@ Ember.run.throttle = function() { // Make sure it's not an autorun during testing function checkAutoRun() { if (!Ember.run.currentRunLoop) { - + Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); } } @@ -5563,7 +6483,7 @@ Binding.prototype = { This copies the Binding so it can be connected to another object. @method copy - @return {Ember.Binding} + @return {Ember.Binding} `this` */ copy: function () { var copy = new Binding(this._to, this._from); @@ -5648,7 +6568,7 @@ Binding.prototype = { @return {Ember.Binding} `this` */ connect: function(obj) { - + Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj); var fromPath = this._from, toPath = this._to; Ember.trySet(obj, toPath, getWithGlobals(obj, fromPath)); @@ -5673,7 +6593,7 @@ Binding.prototype = { @return {Ember.Binding} `this` */ disconnect: function(obj) { - + Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); var twoWay = !this._oneWay; @@ -5771,7 +6691,7 @@ function mixinProperties(to, from) { mixinProperties(Binding, { - /** + /* See `Ember.Binding.from`. @method from @@ -5782,7 +6702,7 @@ mixinProperties(Binding, { return binding.from.apply(binding, arguments); }, - /** + /* See `Ember.Binding.to`. @method to @@ -5807,6 +6727,7 @@ mixinProperties(Binding, { @param {Boolean} [flag] (Optional) passing nothing here will make the binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the binding two way again. + @return {Ember.Binding} `this` */ oneWay: function(from, flag) { var C = this, binding = new C(null, from); @@ -6031,13 +6952,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; @@ -6104,7 +7025,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; } @@ -6117,11 +7059,14 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { descs[key] = value; values[key] = undefined; } else { - // impl super if needed... - if (isMethod(value)) { - value = giveMethodSuper(base, key, value, values, descs); - } else if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties') { + 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); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); } descs[key] = undefined; @@ -6130,7 +7075,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]; @@ -6139,19 +7084,21 @@ function mergeMixins(mixins, m, descs, values, base, keys) { for(var i=0, l=mixins.length; i<l; i++) { mixin = mixins[i]; - + Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); props = mixinProperties(m, mixin); if (props === CONTINUE) { continue; } if (props) { meta = Ember.meta(base); - concats = concatenatedProperties(props, values, base); + if (base.willMergeMixin) { base.willMergeMixin(props); } + 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 @@ -6221,26 +7168,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<l; i++) { - Ember[method](obj, paths[i], null, key); + Ember[updateMethod](obj, paths[i], null, key); } } } -function replaceObservers(obj, key, observer) { - var prevObserver = obj[key]; +function replaceObserversAndListeners(obj, key, observerOrListener) { + var prev = obj[key]; - updateObservers(obj, key, prevObserver, '__ember_observesBefore__', 'removeBeforeObserver'); - updateObservers(obj, key, prevObserver, '__ember_observes__', 'removeObserver'); + if ('function' === typeof prev) { + updateObserversAndListeners(obj, key, prev, '__ember_observesBefore__', 'removeBeforeObserver'); + updateObserversAndListeners(obj, key, prev, '__ember_observes__', 'removeObserver'); + updateObserversAndListeners(obj, key, prev, '__ember_listens__', 'removeListener'); + } - updateObservers(obj, key, observer, '__ember_observesBefore__', 'addBeforeObserver'); - updateObservers(obj, key, observer, '__ember_observes__', 'addObserver'); + if ('function' === typeof observerOrListener) { + updateObserversAndListeners(obj, key, observerOrListener, '__ember_observesBefore__', 'addBeforeObserver'); + updateObserversAndListeners(obj, key, observerOrListener, '__ember_observes__', 'addObserver'); + updateObserversAndListeners(obj, key, observerOrListener, '__ember_listens__', 'addListener'); + } } function applyMixin(obj, mixins, partial) { @@ -6250,6 +7201,7 @@ function applyMixin(obj, mixins, partial) { // Go through all mixins and hashes passed in, and: // // * Handle concatenated properties + // * Handle merged properties // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers @@ -6272,7 +7224,7 @@ function applyMixin(obj, mixins, partial) { if (desc === undefined && value === undefined) { continue; } - replaceObservers(obj, key, value); + replaceObserversAndListeners(obj, key, value); detectBinding(obj, key, value, m); defineProperty(obj, key, desc, value, m); } @@ -6313,7 +7265,7 @@ Ember.mixin = function(obj) { // Mix mixins into classes by passing them as the first arguments to // .extend. App.CommentView = Ember.View.extend(App.Editable, { - template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') + template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') }); commentView = App.CommentView.create(); @@ -6323,6 +7275,31 @@ Ember.mixin = function(obj) { Note that Mixins are created with `Ember.Mixin.create`, not `Ember.Mixin.extend`. + Note that mixins extend a constructor's prototype so arrays and object literals + defined as properties will be shared amongst objects that implement the mixin. + If you want to define an property in a mixin that is not shared, you can define + it either as a computed property or have it be created on initialization of the object. + + ```javascript + //filters array will be shared amongst any object implementing mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.A() + }); + + //filters will be a separate array for every object implementing the mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.computed(function(){return Ember.A();}) + }); + + //filters will be created as a separate array during the object's initialization + App.Filterable = Ember.Mixin.create({ + init: function() { + this._super(); + this.set("filters", Ember.A()); + } + }); + ``` + @class Mixin @namespace Ember */ @@ -6380,7 +7357,7 @@ MixinPrototype.reopen = function() { for(idx=0; idx < len; idx++) { mixin = arguments[idx]; - + Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); if (mixin instanceof Mixin) { mixins.push(mixin); @@ -6558,6 +7535,22 @@ Ember.aliasMethod = function(methodName) { // /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. + + Also available as `Function.prototype.observes` if prototype extensions are + enabled. + @method observer @for Ember @param {Function} func @@ -6570,9 +7563,23 @@ Ember.observer = function(func) { return func; }; -// If observers ever become asynchronous, Ember.immediateObserver -// must remain synchronous. /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` + + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. + + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. + @method immediateObserver @for Ember @param {Function} func @@ -6582,36 +7589,49 @@ Ember.observer = function(func) { Ember.immediateObserver = function() { for (var i=0, l=arguments.length; i<l; i++) { var arg = arguments[i]; - + Ember.assert("Immediate observers must observe internal properties only, not properties on other objects.", typeof arg !== "string" || arg.indexOf('.') === -1); } return Ember.observer.apply(this, arguments); }; /** - When observers fire, they are called with the arguments `obj`, `keyName` - and `value`. In a typical observer, value is the new, post-change value. + When observers fire, they are called with the arguments `obj`, `keyName`. - A `beforeObserver` fires before a property changes. The `value` argument contains - the pre-change value. + Note, `@each.property` observer is called per each add or replace of an element + and it's not called with a specific enumeration item. + + A `beforeObserver` fires before a property changes. A `beforeObserver` is an alternative form of `.observesBefore()`. ```javascript App.PersonView = Ember.View.extend({ - valueWillChange: function (obj, keyName, value) { - this.changingFrom = value; - }.observesBefore('content.value'), - valueDidChange: function(obj, keyName, value) { + + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], + + valueWillChange: Ember.beforeObserver(function(obj, keyName) { + this.changingFrom = obj.get(keyName); + }, 'content.value'), + + valueDidChange: Ember.observer(function(obj, keyName) { // only run if updating a value already in the DOM if (this.get('state') === 'inDOM') { - var color = value > this.changingFrom ? 'green' : 'red'; - // logic + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; + // logic } - }.observes('content.value') + }, 'content.value'), + + friendsDidChange: Ember.observer(function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }, 'friends.@each.name') }); ``` + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. + @method beforeObserver @for Ember @param {Function} func @@ -6628,6 +7648,52 @@ Ember.beforeObserver = function(func) { +(function() { +// Provides a way to register library versions with ember. + +Ember.libraries = function() { + var libraries = []; + var coreLibIndex = 0; + + var getLibrary = function(name) { + for (var i = 0; i < libraries.length; i++) { + if (libraries[i].name === name) { + return libraries[i]; + } + } + }; + + libraries.register = function(name, version) { + if (!getLibrary(name)) { + libraries.push({name: name, version: version}); + } + }; + + libraries.registerCoreLibrary = function(name, version) { + if (!getLibrary(name)) { + libraries.splice(coreLibIndex++, 0, {name: name, version: version}); + } + }; + + libraries.deRegister = function(name) { + var lib = getLibrary(name); + if (lib) libraries.splice(libraries.indexOf(lib), 1); + }; + + libraries.each = function (callback) { + libraries.forEach(function(lib) { + callback(lib.name, lib.version); + }); + }; + return libraries; +}(); + +Ember.libraries.registerCoreLibrary('Ember', Ember.VERSION); + +})(); + + + (function() { /** Ember Metal @@ -6695,6 +7761,7 @@ define("rsvp/async", var browserGlobal = (typeof window !== 'undefined') ? window : {}; var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var async; + var local = (typeof global !== 'undefined') ? global : this; // old node function useNextTick() { @@ -6745,7 +7812,7 @@ define("rsvp/async", function useSetTimeout() { return function(callback, arg) { - setTimeout(function() { + local.setTimeout(function() { callback(arg); }, 1); }; @@ -7128,6 +8195,10 @@ define("rsvp/promise", }); return thenPromise; + }, + + fail: function(fail) { + return this.then(null, fail); } }; @@ -7230,19 +8301,36 @@ define("rsvp/resolve", __exports__.resolve = resolve; }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + var local = (typeof global === "undefined") ? this : global; + + function rethrow(reason) { + local.setTimeout(function() { + throw reason; + }); + throw reason; + } + + + __exports__.rethrow = rethrow; + }); 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__) { + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { "use strict"; var EventTarget = __dependency1__.EventTarget; var Promise = __dependency2__.Promise; var denodeify = __dependency3__.denodeify; var all = __dependency4__.all; var hash = __dependency5__.hash; - var defer = __dependency6__.defer; - var config = __dependency7__.config; - var resolve = __dependency8__.resolve; - var reject = __dependency9__.reject; + var rethrow = __dependency6__.rethrow; + var defer = __dependency7__.defer; + var config = __dependency8__.config; + var resolve = __dependency9__.resolve; + var reject = __dependency10__.reject; function configure(name, value) { config[name] = value; @@ -7253,25 +8341,35 @@ define("rsvp", __exports__.EventTarget = EventTarget; __exports__.all = all; __exports__.hash = hash; + __exports__.rethrow = rethrow; __exports__.defer = defer; __exports__.denodeify = denodeify; __exports__.configure = configure; __exports__.resolve = resolve; __exports__.reject = reject; }); - })(); (function() { +/** +@private +Public api for the container is still in flux. +The public api, specified on the application namespace should be considered the stable api. +// @module container +*/ + +/* + Flag to enable/disable model factory injections (disabled by default) + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); +*/ +Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; + define("container", [], function() { - /** - A safe and simple inheriting object. - - @class InheritingDict - */ + // A safe and simple inheriting object. function InheritingDict(parent) { this.parent = parent; this.dict = {}; @@ -7344,7 +8442,7 @@ define("container", @method has @param {String} key - @returns {Boolean} + @return {Boolean} */ has: function(key) { var dict = this.dict; @@ -7378,20 +8476,25 @@ define("container", } }; - /** - A lightweight container that helps to assemble and decouple components. - @class Container - */ + // A lightweight container that helps to assemble and decouple components. + // Public api for the container is still in flux. + // The public api, specified on the application namespace should be considered the stable api. 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); } @@ -7465,7 +8568,7 @@ define("container", to correctly inherit from the current container. @method child - @returns {Container} + @return {Container} */ child: function() { var container = new Container(this); @@ -7479,7 +8582,7 @@ define("container", as expected. @method set - @param {Object} obkect + @param {Object} object @param {String} key @param {any} value */ @@ -7514,7 +8617,7 @@ define("container", factory = name; fullName = type; } else { - + Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false); fullName = type + ":" + name; } @@ -7535,6 +8638,7 @@ define("container", container.unregister('model:user') container.lookup('model:user') === undefined //=> true + ``` @method unregister @param {String} fullName @@ -7544,6 +8648,7 @@ define("container", this.registry.remove(normalizedName); this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); this._options.remove(normalizedName); }, @@ -7577,7 +8682,7 @@ define("container", @method resolve @param {String} fullName - @returns {Function} fullName's factory + @return {Function} fullName's factory */ resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); @@ -7608,12 +8713,23 @@ define("container", return fullName; }, + /** + @method makeToString + + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, + /** 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 there own locally scoped singletons. + to all have their own locally scoped singletons. ```javascript var container = new Container(); @@ -7658,7 +8774,7 @@ define("container", var value = instantiate(this, fullName); - if (!value) { return; } + if (value === undefined) { return; } if (isSingleton(this, fullName) && options.singleton !== false) { this.cache.set(fullName, value); @@ -7695,7 +8811,7 @@ define("container", }, /** - Allow registerying options for all factories of a type. + Allow registering options for all factories of a type. ```javascript var container = new Container(); @@ -7736,7 +8852,7 @@ define("container", this.optionsForType(type, options); }, - /* + /** @private Used only via `injection`. @@ -7775,20 +8891,10 @@ define("container", 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 - }); + addTypeInjection(this.typeInjections, type, property, fullName); }, - /* + /** Defines injection rules. These rules are used to inject dependencies onto objects when they @@ -7796,8 +8902,8 @@ define("container", Two forms of injections are possible: - * Injecting one fullName on another fullName - * Injecting one fullName on a type + * Injecting one fullName on another fullName + * Injecting one fullName on a type Example: @@ -7806,7 +8912,7 @@ define("container", container.register('source:main', Source); container.register('model:user', User); - container.register('model:post', PostController); + container.register('model:post', Post); // injecting one fullName on another fullName // eg. each user model gets a post model @@ -7839,8 +8945,104 @@ 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); }, /** @@ -7862,7 +9064,7 @@ define("container", item.destroy(); }); - delete this.parent; + this.parent = undefined; this.isDestroyed = true; }, @@ -7898,7 +9100,7 @@ define("container", injection = injections[i]; lookup = container.lookup(injection.fullName); - if (lookup) { + if (lookup !== undefined) { hash[injection.property] = lookup; } else { throw new Error('Attempting to inject an unknown injection: `' + injection.fullName + '`'); @@ -7925,32 +9127,83 @@ define("container", function factoryFor(container, fullName) { var name = container.normalize(fullName); - return container.resolve(name); + var factory = container.resolve(name); + var injectedFactory; + var cache = container.factoryCache; + var type = fullName.split(":")[0]; + + if (factory === undefined) { return; } + + if (cache.has(fullName)) { + return cache.get(fullName); + } + + if (!factory || typeof factory.extend !== 'function' || (!Ember.MODEL_FACTORY_INJECTIONS && type === 'model')) { + // TODO: think about a 'safe' merge style extension + // for now just fallback to create time injection + return factory; + } else { + + var injections = injectionsFor(container, fullName); + var factoryInjections = factoryInjectionsFor(container, fullName); + + factoryInjections._toString = container.makeToString(factory, fullName); + + injectedFactory = factory.extend(injections); + injectedFactory.reopenClass(factoryInjections); + + cache.set(fullName, injectedFactory); + + return injectedFactory; + } + } + + function injectionsFor(container ,fullName) { + var splitName = fullName.split(":"), + type = splitName[0], + injections = []; + + injections = injections.concat(container.typeInjections.get(type) || []); + injections = injections.concat(container.injections[fullName] || []); + + injections = buildInjections(container, injections); + injections._debugContainerKey = fullName; + injections.container = container; + + return injections; + } + + function factoryInjectionsFor(container, fullName) { + var splitName = fullName.split(":"), + type = splitName[0], + factoryInjections = []; + + factoryInjections = factoryInjections.concat(container.factoryTypeInjections.get(type) || []); + factoryInjections = factoryInjections.concat(container.factoryInjections[fullName] || []); + + factoryInjections = buildInjections(container, factoryInjections); + factoryInjections._debugContainerKey = fullName; + + return factoryInjections; } function instantiate(container, fullName) { var factory = factoryFor(container, fullName); - var splitName = fullName.split(":"), - type = splitName[0], - value; - if (option(container, fullName, 'instantiate') === false) { return factory; } if (factory) { - var injections = []; - injections = injections.concat(container.typeInjections.get(type) || []); - injections = injections.concat(container.injections[fullName] || []); - - var hash = buildInjections(container, injections); - hash.container = container; - hash._debugContainerKey = fullName; - - value = factory.create(hash); - - return value; + if (typeof factory.extend === 'function') { + // assume the factory was extendable and is already injected + return factory.create(); + } else { + // assume the factory was extendable + // to create time injections + // TODO: support new'ing for instantiation and merge injections for pure JS Functions + return factory.create(injectionsFor(container, fullName)); + } } } @@ -7969,6 +9222,25 @@ define("container", container.cache.dict = {}; } + function addTypeInjection(rules, type, property, fullName) { + var injections = rules.get(type); + + if (!injections) { + injections = []; + rules.set(type, injections); + } + + injections.push({ + property: property, + fullName: fullName + }); + } + + function addInjection(rules, factoryName, property, injectionName) { + var injections = rules[factoryName] = rules[factoryName] || []; + injections.push({ property: property, fullName: injectionName }); + } + return Container; }); @@ -8104,6 +9376,7 @@ function _copy(obj, deep, seen, copies) { // avoid cyclical loops if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; + Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); // IMPORTANT: this specific test will detect a native array only. Any other // object will need to implement Copyable. @@ -8284,529 +9557,6 @@ Ember.Error.prototype = Ember.create(Error.prototype); -(function() { -/** - Expose RSVP implementation - - @class RSVP - @namespace Ember - @constructor -*/ -Ember.RSVP = requireModule('rsvp'); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var STRING_DASHERIZE_REGEXP = (/[ _]/g); -var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); -var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); -var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); -var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); - -/** - Defines the hash of localized strings for the current language. Used by - the `Ember.String.loc()` helper. To localize, add string values to this - hash. - - @property STRINGS - @for Ember - @type Hash -*/ -Ember.STRINGS = {}; - -/** - Defines string helper methods including string formatting and localization. - Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be - added to the `String.prototype` as well. - - @class String - @namespace Ember - @static -*/ -Ember.String = { - - /** - Apply formatting options to the string. This will look for occurrences - of "%@" in your string and substitute them with the arguments you pass into - this method. If you want to control the specific order of replacement, - you can add a number after the key as well to indicate which argument - you want to insert. - - Ordered insertions are most useful when building loc strings where values - you need to insert may appear in different orders. - - ```javascript - "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" - "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" - ``` - - @method fmt - @param {String} str The string to format - @param {Array} formats An array of parameters to interpolate into string. - @return {String} formatted string - */ - fmt: function(str, formats) { - // first, replace any ORDERED replacements. - var idx = 0; // the current index for non-numerical replacements - return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { - argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; - s = formats[argIndex]; - return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); - }) ; - }, - - /** - Formats the passed string, but first looks up the string in the localized - strings hash. This is a convenient way to localize text. See - `Ember.String.fmt()` for more information on formatting. - - Note that it is traditional but not required to prefix localized string - keys with an underscore or other character so you can easily identify - localized strings. - - ```javascript - Ember.STRINGS = { - '_Hello World': 'Bonjour le monde', - '_Hello %@ %@': 'Bonjour %@ %@' - }; - - Ember.String.loc("_Hello World"); // 'Bonjour le monde'; - Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; - ``` - - @method loc - @param {String} str The string to format - @param {Array} formats Optional array of parameters to interpolate into string. - @return {String} formatted string - */ - loc: function(str, formats) { - str = Ember.STRINGS[str] || str; - return Ember.String.fmt(str, formats) ; - }, - - /** - Splits a string into separate units separated by spaces, eliminating any - empty strings in the process. This is a convenience method for split that - is mostly useful when applied to the `String.prototype`. - - ```javascript - Ember.String.w("alpha beta gamma").forEach(function(key) { - console.log(key); - }); - - // > alpha - // > beta - // > gamma - ``` - - @method w - @param {String} str The string to split - @return {String} split string - */ - w: function(str) { return str.split(/\s+/); }, - - /** - Converts a camelized string into all lower case separated by underscores. - - ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' - ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. - */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); - }, - - /** - Replaces underscores or spaces with dashes. - - ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' - ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - hit = cache.hasOwnProperty(str), - ret; - - if (hit) { - return cache[str]; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } - - return ret; - }, - - /** - Returns the lowerCamelCase form of a string. - - ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - 'My Favorite Items'.camelize(); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }).replace(/^([A-Z])/, function(match, separator, chr) { - return match.toLowerCase(); - }); - }, - - /** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ - classify: function(str) { - var parts = str.split("."), - out = []; - - for (var i=0, l=parts.length; i<l; i++) { - var camelized = Ember.String.camelize(parts[i]); - out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1)); - } - - return out.join("."); - }, - - /** - More general than decamelize. Returns the lower\_case\_and\_underscored - form of a string. - - ```javascript - 'innerHTML'.underscore(); // 'inner_html' - 'action_name'.underscore(); // 'action_name' - 'css-class-name'.underscore(); // 'css_class_name' - 'my favorite items'.underscore(); // 'my_favorite_items' - ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. - */ - underscore: function(str) { - return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). - replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); - }, - - /** - Returns the Capitalized form of a string - - ```javascript - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' - ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. - */ - capitalize: function(str) { - return str.charAt(0).toUpperCase() + str.substr(1); - } - -}; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - - -var fmt = Ember.String.fmt, - w = Ember.String.w, - loc = Ember.String.loc, - camelize = Ember.String.camelize, - decamelize = Ember.String.decamelize, - dasherize = Ember.String.dasherize, - underscore = Ember.String.underscore, - capitalize = Ember.String.capitalize, - classify = Ember.String.classify; - -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { - - /** - See `Ember.String.fmt`. - - @method fmt - @for String - */ - String.prototype.fmt = function() { - return fmt(this, arguments); - }; - - /** - See `Ember.String.w`. - - @method w - @for String - */ - String.prototype.w = function() { - return w(this); - }; - - /** - See `Ember.String.loc`. - - @method loc - @for String - */ - String.prototype.loc = function() { - return loc(this, arguments); - }; - - /** - See `Ember.String.camelize`. - - @method camelize - @for String - */ - String.prototype.camelize = function() { - return camelize(this); - }; - - /** - See `Ember.String.decamelize`. - - @method decamelize - @for String - */ - String.prototype.decamelize = function() { - return decamelize(this); - }; - - /** - See `Ember.String.dasherize`. - - @method dasherize - @for String - */ - String.prototype.dasherize = function() { - return dasherize(this); - }; - - /** - See `Ember.String.underscore`. - - @method underscore - @for String - */ - String.prototype.underscore = function() { - return underscore(this); - }; - - /** - See `Ember.String.classify`. - - @method classify - @for String - */ - String.prototype.classify = function() { - return classify(this); - }; - - /** - See `Ember.String.capitalize`. - - @method capitalize - @for String - */ - String.prototype.capitalize = function() { - return capitalize(this); - }; - -} - - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var a_slice = Array.prototype.slice; - -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { - - /** - The `property` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - `true`, which is the default. - - Computed properties allow you to treat a function like a property: - - ```javascript - MyApp.president = Ember.Object.create({ - firstName: "Barack", - lastName: "Obama", - - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Call this flag to mark the function as a property - }.property() - }); - - MyApp.president.get('fullName'); // "Barack Obama" - ``` - - Treating a function like a property is useful because they can work with - bindings, just like any other property. - - Many computed properties have dependencies on other properties. For - example, in the above example, the `fullName` property depends on - `firstName` and `lastName` to determine its value. You can tell Ember - about these dependencies like this: - - ```javascript - MyApp.president = Ember.Object.create({ - firstName: "Barack", - lastName: "Obama", - - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Tell Ember.js that this computed property depends on firstName - // and lastName - }.property('firstName', 'lastName') - }); - ``` - - Make sure you list these dependencies so Ember knows when to update - bindings that connect to a computed property. Changing a dependency - will not immediately trigger an update of the computed property, but - will instead clear the cache so that it is updated when the next `get` - is called on the property. - - See `Ember.ComputedProperty`, `Ember.computed`. - - @method property - @for Function - */ - Function.prototype.property = function() { - var ret = Ember.computed(this); - return ret.property.apply(ret, arguments); - }; - - /** - The `observes` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - true, which is the default. - - You can observe property changes simply by adding the `observes` - call to the end of your method declarations in classes that you write. - For example: - - ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` - - See `Ember.Observable.observes`. - - @method observes - @for Function - */ - Function.prototype.observes = function() { - this.__ember_observes__ = a_slice.call(arguments); - return this; - }; - - /** - The `observesBefore` extension of Javascript's Function prototype is - available when `Ember.EXTEND_PROTOTYPES` or - `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. - - You can get notified when a property changes is about to happen by - by adding the `observesBefore` call to the end of your method - declarations in classes that you write. For example: - - ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property is about to change - }.observesBefore('value') - }); - ``` - - See `Ember.Observable.observesBefore`. - - @method observesBefore - @for Function - */ - Function.prototype.observesBefore = function() { - this.__ember_observesBefore__ = a_slice.call(arguments); - return this; - }; - -} - - -})(); - - - -(function() { - -})(); - - - (function() { /** @module ember @@ -9036,14 +9786,14 @@ Ember.Enumerable = Ember.Mixin.create({ }, /** - Alias for `mapProperty` + Alias for `mapBy` @method getEach @param {String} key name of the property @return {Array} The mapped array. */ getEach: function(key) { - return this.mapProperty(key); + return this.mapBy(key); }, /** @@ -9101,16 +9851,28 @@ Ember.Enumerable = Ember.Mixin.create({ Similar to map, this specialized function returns the value of the named property on all items in the enumeration. - @method mapProperty + @method mapBy @param {String} key name of the property @return {Array} The mapped array. */ - mapProperty: function(key) { + mapBy: function(key) { return this.map(function(next) { return get(next, key); }); }, + /** + Similar to map, this specialized function returns the value of the named + property on all items in the enumeration. + + @method mapProperty + @param {String} key name of the property + @return {Array} The mapped array. + @deprecated Use `mapBy` instead + */ + + mapProperty: Ember.aliasMethod('mapBy'), + /** Returns an array with all of the items in the enumeration that the passed function returns true for. This method corresponds to `filter()` defined in @@ -9182,15 +9944,46 @@ Ember.Enumerable = Ember.Mixin.create({ can pass an optional second argument with the target value. Otherwise this will match any property that evaluates to `true`. - @method filterProperty + @method filterBy @param {String} key the property to test @param {String} [value] optional value to test against. @return {Array} filtered array */ - filterProperty: function(key, value) { + filterBy: function(key, value) { return this.filter(iter.apply(this, arguments)); }, + /** + Returns an array with just the items with the matched property. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to `true`. + + @method filterProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} filtered array + @deprecated Use `filterBy` instead + */ + filterProperty: Ember.aliasMethod('filterBy'), + + /** + Returns an array with the items that do not have truthy values for + key. You can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to false. + + @method rejectBy + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} rejected array + */ + rejectBy: function(key, value) { + var exactValue = function(item) { return get(item, key) === value; }, + hasValue = function(item) { return !!get(item, key); }, + use = (arguments.length === 2 ? exactValue : hasValue); + + return this.reject(use); + }, + /** Returns an array with the items that do not have truthy values for key. You can pass an optional second argument with the target value. Otherwise @@ -9200,14 +9993,9 @@ Ember.Enumerable = Ember.Mixin.create({ @param {String} key the property to test @param {String} [value] optional value to test against. @return {Array} rejected array + @deprecated Use `rejectBy` instead */ - rejectProperty: function(key, value) { - var exactValue = function(item) { return get(item, key) === value; }, - hasValue = function(item) { return !!get(item, key); }, - use = (arguments.length === 2 ? exactValue : hasValue); - - return this.reject(use); - }, + rejectProperty: Ember.aliasMethod('rejectBy'), /** Returns the first item in the array for which the callback returns true. @@ -9260,15 +10048,30 @@ Ember.Enumerable = Ember.Mixin.create({ This method works much like the more generic `find()` method. - @method findProperty + @method findBy @param {String} key the property to test @param {String} [value] optional value to test against. @return {Object} found item or `undefined` */ - findProperty: function(key, value) { + findBy: function(key, value) { return this.find(iter.apply(this, arguments)); }, + /** + Returns the first item with a property matching the passed value. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to `true`. + + This method works much like the more generic `find()` method. + + @method findProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Object} found item or `undefined` + @deprecated Use `findBy` instead + */ + findProperty: Ember.aliasMethod('findBy'), + /** Returns `true` if the passed function returns true for every item in the enumeration. This corresponds with the `every()` method in JavaScript 1.6. @@ -9311,15 +10114,65 @@ Ember.Enumerable = Ember.Mixin.create({ Returns `true` if the passed property resolves to `true` for all items in the enumerable. This method is often simpler/faster than using a callback. - @method everyProperty + @method everyBy @param {String} key the property to test @param {String} [value] optional value to test against. @return {Boolean} */ - everyProperty: function(key, value) { + everyBy: function(key, value) { return this.every(iter.apply(this, arguments)); }, + /** + Returns `true` if the passed property resolves to `true` for all items in + the enumerable. This method is often simpler/faster than using a callback. + + @method everyProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Boolean} + @deprecated Use `everyBy` instead + */ + everyProperty: Ember.aliasMethod('everyBy'), + + /** + Returns `true` if the passed function returns true for any item in the + enumeration. This corresponds with the `some()` method in JavaScript 1.6. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(item, index, enumerable); + ``` + + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. + + It should return the `true` to include the item in the results, `false` + otherwise. + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. This is a good way + to give your iterator function access to the current object. + + Usage Example: + + ```javascript + if (people.any(isManager)) { Paychecks.addBiggerBonus(); } + ``` + + @method any + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Boolean} `true` if the passed function returns `true` for any item + */ + any: function(callback, target) { + return !!this.find(function(x, idx, i) { + return !!callback.call(target, x, idx, i); + }); + }, /** Returns `true` if the passed function returns true for any item in the @@ -9353,11 +10206,21 @@ Ember.Enumerable = Ember.Mixin.create({ @param {Function} callback The callback to execute @param {Object} [target] The target object to use @return {Boolean} `true` if the passed function returns `true` for any item + @deprecated Use `any` instead */ - some: function(callback, target) { - return !!this.find(function(x, idx, i) { - return !!callback.call(target, x, idx, i); - }); + some: Ember.aliasMethod('any'), + + /** + Returns `true` if the passed property resolves to `true` for any item in + the enumerable. This method is often simpler/faster than using a callback. + + @method anyBy + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Boolean} `true` if the passed function returns `true` for any item + */ + anyBy: function(key, value) { + return this.any(iter.apply(this, arguments)); }, /** @@ -9368,10 +10231,9 @@ Ember.Enumerable = Ember.Mixin.create({ @param {String} key the property to test @param {String} [value] optional value to test against. @return {Boolean} `true` if the passed function returns `true` for any item + @deprecated Use `anyBy` instead */ - someProperty: function(key, value) { - return this.some(iter.apply(this, arguments)); - }, + someProperty: Ember.aliasMethod('anyBy'), /** This will combine the values of the enumerator into a single value. It @@ -10080,6 +10942,9 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot return an enumerable that maps automatically to the named key on the member objects. + If you merely want to watch for any items being added or removed to the array, + use the `[]` property instead of `@each`. + @property @each */ '@each': Ember.computed(function() { @@ -10093,6 +10958,2208 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot +(function() { +var e_get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + metaFor = Ember.meta, + addBeforeObserver = Ember.addBeforeObserver, + removeBeforeObserver = Ember.removeBeforeObserver, + addObserver = Ember.addObserver, + removeObserver = Ember.removeObserver, + ComputedProperty = Ember.ComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach, + // Here we explicitly don't allow `@each.foo`; it would require some special + // testing, but there's no particular reason why it should be disallowed. + eachPropertyPattern = /^(.*)\.@each\.(.*)/, + doubleEachPropertyPattern = /(.*\.@each){2,}/; + +function get(obj, key) { + if (Ember.FEATURES.isEnabled('reduceComputedSelf')) { + if (key === '@self') { + return obj; + } + } + + return e_get(obj, key); +} + +/* + Tracks changes to dependent arrays, as well as to properties of items in + dependent arrays. + + @class DependentArraysObserver +*/ +function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { + // user specified callbacks for `addedItem` and `removedItem` + this.callbacks = callbacks; + + // the computed property: remember these are shared across instances + this.cp = cp; + + // the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is + // associated with + this.instanceMeta = instanceMeta; + + // A map of array guids to dependentKeys, for the given context. We track + // this because we want to set up the computed property potentially before the + // dependent array even exists, but when the array observer fires, we lack + // enough context to know what to update: we can recover that context by + // getting the dependentKey. + this.dependentKeysByGuid = {}; + + // a map of dependent array guids -> Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; + + // This is used to coalesce item changes from property observers. + this.changedItems = {}; +} + +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); + + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; + + this.destroyed = false; +} + +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue); + }, + getValue: function () { + return this.instanceMeta.getValue(); + }, + + setupObservers: function (dependentArray, dependentKey) { + Ember.assert("dependent array must be an `Ember.Array`", Ember.Array.detect(dependentArray)); + + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, + + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + + delete this.dependentKeysByGuid[guidFor(dependentArray)]; + + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + }, + + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); + + this.resetTransformations(dependentKey, observerContexts); + + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; + + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, + + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; + + if (!trackedArray) { return; } + + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } + + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; + + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, + + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); + + this.createPropertyObserver(observerContext); + + return observerContext; + }, + + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; + + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + }, + + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + }, + + addTransformation: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, + + removeTransformation: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } + + return []; + }, + + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === Ember.TrackedArray.DELETE) { return; } + if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } + + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, + + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + item, + itemIndex, + sliceIndex, + observerContexts; + + observerContexts = this.removeTransformation(dependentKey, index, removedCount); + + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } + + for (sliceIndex = removedCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = index + sliceIndex; + item = dependentArray.objectAt(itemIndex); + + forEach(itemPropertyKeys, removeObservers, this); + + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } + }, + + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + changeMeta, + observerContext; + + forEach(dependentArray.slice(index, index + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, index + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + } + + changeMeta = createChangeMeta(dependentArray, item, index + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); + + this.addTransformation(dependentKey, index, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; + } + + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + Ember.run.once(this, 'flushChanges'); + }, + + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } + + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + } + this.changedItems = {}; + } +}; + +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; + + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } + + return meta; +} + +function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); +} + +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; + + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } + + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} + +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; + + this.dependentArrays = {}; + this.sugarMeta = {}; + + this.initialValue = initialValue; +} + +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; + } + }, + + setValue: function(newValue) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue !== undefined) { + this.cache[this.propertyName] = newValue; + } else { + delete this.cache[this.propertyName]; + } + } +}; + +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. + + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; + + this.options = options; + this._instanceMetas = {}; + + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + + this.readOnly(); + this.cacheable(); + + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by <cp, propertyName>. + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + Ember.run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); + + reset.call(this, cp, propertyName); + + forEach(cp._dependentKeys, function (dependentKey) { + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; + + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } + + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + + forEach(cp._dependentKeys, function(dependentKey) { + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; + + this.func = function (propertyName) { + Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); + + recompute.call(this, propertyName); + + return cp._instanceMeta(this, propertyName).getValue(); + }; +} + +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + +function defaultCallback(computedValue) { + return computedValue; +} + +ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; +}; + +ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName; + + return !!this._instanceMetas[key]; +}; + +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName, + meta = this._instanceMetas[key]; + + if (!meta) { + meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; +}; + +ReduceComputedProperty.prototype.initialValue = function () { + switch (typeof this.options.initialValue) { + case 'undefined': + throw new Error("reduce computed properties require an initial value: did you forget to pass one to Ember.reduceComputed?"); + case 'function': + return this.options.initialValue(); + default: + return this.options.initialValue; + } +}; + +ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); +}; + +ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); +}; + +ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } +}; + +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = [], + match, + dependentArrayKey, + itemPropertyKey; + + forEach(a_slice.call(arguments), function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + itemPropertyKey = match[2]; + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + propertyArgs.push(dependentArrayKey); + } else { + propertyArgs.push(dependentKey); + } + }); + + return ComputedProperty.prototype.property.apply(this, propertyArgs); +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties. + + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is removed + from the array. + + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: + + ```javascript + function (initialValue, changeMeta, instanceMeta) + ``` + + `initialValue` - The value of the `initialValue` property from the + options object. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` + + Dependent keys may refer to `@self` to observe changes to the object itself, + which must be array-like, rather than a property of the object. This is + mostly useful for array proxies, to ensure objects are retrieved via + `objectAtContent`. This is how you could sort items by properties defined on an item controller. + + Example + + ```javascript + App.PeopleController = Ember.ArrayController.extend({ + itemController: 'person', + + sortedPeople: Ember.computed.sort('@self.@each.reversedName', function(personA, personB) { + // `reversedName` isn't defined on Person, but we have access to it via + // the item controller App.PersonController. If we'd used + // `content.@each.reversedName` above, we would be getting the objects + // directly and not have access to `reversedName`. + // + var reversedNameA = get(personA, 'reversedName'), + reversedNameB = get(personB, 'reversedName'); + + return Ember.compare(reversedNameA, reversedNameB); + }) + }); + + App.PersonController = Ember.ObjectController.extend({ + reversedName: function () { + return reverse(get(this, 'name')); + }.property('name') + }) + ``` + + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.reduceComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Error("Reduce Computed Property declared without an options hash"); + } + + if (Ember.isNone(options.initialValue)) { + throw new Error("Reduce Computed Property declared without an initial value"); + } + + var cp = new ReduceComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; + +function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + Ember.addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } + + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; +} +Ember.ArrayComputedProperty = ArrayComputedProperty; +ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); +ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); +}; +ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is + removed from the array. + + `addedItem` - A function that is called each time an element is + added to the array. + + + The `initialize` function has the following signature: + + ```javascript + function (array, changeMeta, instanceMeta) + ``` + + `array` - The initial value of the arrayComputed, an empty array. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); + }; + ``` + + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Error("Array Computed Property declared without an options hash"); + } + + var cp = new ArrayComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map; + +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('maxChildAge'); // 8 + ``` + + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array +*/ +Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('minChildAge'); // 5 + ``` + + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array +*/ +Ember.computed.min = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + Returns an array mapped via the callback + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hampster = App.Hampster.create({chores: ['cook', 'clean', 'write more unit tests']}); + hampster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` + + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback +*/ +Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Returns an array mapped to the specified key. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('childAges'); // [7, 5, 8] + ``` + + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key +*/ +Ember.computed.mapBy = function(dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey +*/ +Ember.computed.mapProperty = Ember.computed.mapBy; + +/** + Filters the array by the callback. + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); + + var hampster = App.Hampster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filter = function(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new Ember.SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); + } + + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); + } + + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Filters the array by the property and value + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); + + var hampster = App.Hampster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {String} value + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filterBy = function(dependentKey, propertyKey, value) { + var callback; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead +*/ +Ember.computed.filterProperty = Ember.computed.filterBy; + +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); + + var hampster = App.Hampster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hampster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` + + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.uniq = function() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); + + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; + + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). + + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.union = Ember.computed.uniq; + +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependeny arrays. + + Example + + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); + + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` + + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays +*/ +Ember.computed.intersect = function () { + var getDependentKeyGuids = function (changeMeta) { + return map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; + + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; + + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { + + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; + } + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); + + var hampster = App.Hampster.create({fruits: [ + 'grape', + 'kale', + ]}); + hampster.get('wants'); // ['banana'] + ``` + + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array +*/ +Ember.computed.setDiff = function (setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new Error("setDiff requires exactly two dependent arrays."); + } + return Ember.arrayComputed.call(null, setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); +}; + +function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; + + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } + + if (low === high) { + return low; + } + + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); + + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); + + if (guidMid === guidItem) { + return mid; + } + + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } + + + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } + + return mid; + + function _guidFor(item) { + if (Ember.ObjectProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} + +/** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. + + The callback method you provide should have the following signature: + + ```javascript + function(itemA, itemB); + ``` + + - `itemA` the first item to compare. + - `itemB` the second item to compare. + + This function should return `-1` when `itemA` should come before + `itemB`. It should return `1` when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + return 0; + }), + }); + var todoList = ToDoList.create({todos: [ + {name: 'Unit Test', priority: 2}, + {name: 'Documentation', priority: 3}, + {name: 'Release', priority: 1} + ]}); + + todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] + todoList.get('priroityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] + ``` + + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function +*/ +Ember.computed.sort = function (itemsKey, sortDefinition) { + Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); + + var initFn, sortPropertiesKey; + + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; + + Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); + + changeMeta.property.clearItemPropertyKeys(itemsKey); + + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } + + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); + + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } + + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } + + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } + + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + + setupSortProperties.call(this); + + + instanceMeta.order = function (itemA, itemB) { + var sortProperty, result, asc; + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = Ember.compare(get(itemA, sortProperty), get(itemB, sortProperty)); + + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } + + return 0; + }; + + instanceMeta.binarySearch = binarySearch; + }; + } + + return Ember.arrayComputed.call(null, itemsKey, { + initialize: initFn, + + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; + + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); + + searchItem = Ember.ObjectProxy.create(proxyProperties); + } else { + searchItem = item; + } + + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; + +})(); + + + +(function() { +/** + Expose RSVP implementation + + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md + + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); +var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); +var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); +var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); + +/** + Defines the hash of localized strings for the current language. Used by + the `Ember.String.loc()` helper. To localize, add string values to this + hash. + + @property STRINGS + @for Ember + @type Hash +*/ +Ember.STRINGS = {}; + +/** + Defines string helper methods including string formatting and localization. + Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be + added to the `String.prototype` as well. + + @class String + @namespace Ember + @static +*/ +Ember.String = { + + /** + Apply formatting options to the string. This will look for occurrences + of "%@" in your string and substitute them with the arguments you pass into + this method. If you want to control the specific order of replacement, + you can add a number after the key as well to indicate which argument + you want to insert. + + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. + + ```javascript + "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" + ``` + + @method fmt + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. + @return {String} formatted string + */ + fmt: function(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = formats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); + }) ; + }, + + /** + Formats the passed string, but first looks up the string in the localized + strings hash. This is a convenient way to localize text. See + `Ember.String.fmt()` for more information on formatting. + + Note that it is traditional but not required to prefix localized string + keys with an underscore or other character so you can easily identify + localized strings. + + ```javascript + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; + + Ember.String.loc("_Hello World"); // 'Bonjour le monde'; + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; + ``` + + @method loc + @param {String} str The string to format + @param {Array} formats Optional array of parameters to interpolate into string. + @return {String} formatted string + */ + loc: function(str, formats) { + str = Ember.STRINGS[str] || str; + return Ember.String.fmt(str, formats) ; + }, + + /** + Splits a string into separate units separated by spaces, eliminating any + empty strings in the process. This is a convenience method for split that + is mostly useful when applied to the `String.prototype`. + + ```javascript + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); + + // > alpha + // > beta + // > gamma + ``` + + @method w + @param {String} str The string to split + @return {String} split string + */ + w: function(str) { return str.split(/\s+/); }, + + /** + Converts a camelized string into all lower case separated by underscores. + + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ + decamelize: function(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); + }, + + /** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ + dasherize: function(str) { + var cache = STRING_DASHERIZE_CACHE, + hit = cache.hasOwnProperty(str), + ret; + + if (hit) { + return cache[str]; + } else { + ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; + } + + return ret; + }, + + /** + Returns the lowerCamelCase form of a string. + + ```javascript + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ + camelize: function(str) { + return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }).replace(/^([A-Z])/, function(match, separator, chr) { + return match.toLowerCase(); + }); + }, + + /** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ + classify: function(str) { + var parts = str.split("."), + out = []; + + for (var i=0, l=parts.length; i<l; i++) { + var camelized = Ember.String.camelize(parts[i]); + out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1)); + } + + return out.join("."); + }, + + /** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ + underscore: function(str) { + return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). + replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); + }, + + /** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ + capitalize: function(str) { + return str.charAt(0).toUpperCase() + str.substr(1); + } + +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + + + +var fmt = Ember.String.fmt, + w = Ember.String.w, + loc = Ember.String.loc, + camelize = Ember.String.camelize, + decamelize = Ember.String.decamelize, + dasherize = Ember.String.dasherize, + underscore = Ember.String.underscore, + capitalize = Ember.String.capitalize, + classify = Ember.String.classify; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { + + /** + See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). + + @method fmt + @for String + */ + String.prototype.fmt = function() { + return fmt(this, arguments); + }; + + /** + See [Ember.String.w](/api/classes/Ember.String.html#method_w). + + @method w + @for String + */ + String.prototype.w = function() { + return w(this); + }; + + /** + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc). + + @method loc + @for String + */ + String.prototype.loc = function() { + return loc(this, arguments); + }; + + /** + See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). + + @method camelize + @for String + */ + String.prototype.camelize = function() { + return camelize(this); + }; + + /** + See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). + + @method decamelize + @for String + */ + String.prototype.decamelize = function() { + return decamelize(this); + }; + + /** + See [Ember.String.dasherize](/api/classes/Ember.String.html#method_dasherize). + + @method dasherize + @for String + */ + String.prototype.dasherize = function() { + return dasherize(this); + }; + + /** + See [Ember.String.underscore](/api/classes/Ember.String.html#method_underscore). + + @method underscore + @for String + */ + String.prototype.underscore = function() { + return underscore(this); + }; + + /** + See [Ember.String.classify](/api/classes/Ember.String.html#method_classify). + + @method classify + @for String + */ + String.prototype.classify = function() { + return classify(this); + }; + + /** + See [Ember.String.capitalize](/api/classes/Ember.String.html#method_capitalize). + + @method capitalize + @for String + */ + String.prototype.capitalize = function() { + return capitalize(this); + }; + +} + + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var a_slice = Array.prototype.slice; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { + + /** + The `property` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + `true`, which is the default. + + Computed properties allow you to treat a function like a property: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Call this flag to mark the function as a property + }.property() + }); + + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" + }); + + president.get('fullName'); // "Barack Obama" + ``` + + Treating a function like a property is useful because they can work with + bindings, just like any other property. + + Many computed properties have dependencies on other properties. For + example, in the above example, the `fullName` property depends on + `firstName` and `lastName` to determine its value. You can tell Ember + about these dependencies like this: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') + }); + ``` + + Make sure you list these dependencies so Ember knows when to update + bindings that connect to a computed property. Changing a dependency + will not immediately trigger an update of the computed property, but + will instead clear the cache so that it is updated when the next `get` + is called on the property. + + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). + + @method property + @for Function + */ + Function.prototype.property = function() { + var ret = Ember.computed(this); + return ret.property.apply(ret, arguments); + }; + + /** + The `observes` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. + + You can observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. + + See `Ember.observer`. + + @method observes + @for Function + */ + Function.prototype.observes = function() { + this.__ember_observes__ = a_slice.call(arguments); + return this; + }; + + /** + The `observesImmediately` extension of Javascript's Function prototype is + available when `Ember.EXTEND_PROTOTYPES` or + `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. + + You can observe property changes simply by adding the `observesImmediately` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes immediately after the "value" property changes + }.observesImmediately('value') + }); + ``` + + In the future, `observes` may become asynchronous. In this event, + `observesImmediately` will maintain the synchronous behavior. + + See `Ember.immediateObserver`. + + @method observesImmediately + @for Function + */ + Function.prototype.observesImmediately = function() { + for (var i=0, l=arguments.length; i<l; i++) { + var arg = arguments[i]; + Ember.assert("Immediate observers must observe internal properties only, not properties on other objects.", arg.indexOf('.') === -1); + } + + return this.observes.apply(this, arguments); + }; + + /** + The `observesBefore` extension of Javascript's Function prototype is + available when `Ember.EXTEND_PROTOTYPES` or + `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. + + You can get notified when a property change is about to happen by + by adding the `observesBefore` call to the end of your method + declarations in classes that you write. For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property is about to change + }.observesBefore('value') + }); + ``` + + See `Ember.beforeObserver`. + + @method observesBefore + @for Function + */ + Function.prototype.observesBefore = function() { + this.__ember_observesBefore__ = a_slice.call(arguments); + return this; + }; + + /** + The `on` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. + + You can listen for events simply by adding the `on` call to the end of + your method declarations in classes or mixins that you write. For example: + + ```javascript + Ember.Mixin.create({ + doSomethingWithElement: function() { + // Executes whenever the "didInsertElement" event fires + }.on('didInsertElement') + }); + ``` + + See `Ember.on`. + + @method on + @for Function + */ + Function.prototype.on = function() { + var events = a_slice.call(arguments); + this.__ember_listens__ = events; + return this; + }; +} + + +})(); + + + +(function() { + +})(); + + + (function() { /** @module ember @@ -10728,7 +13795,10 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @submodule ember-runtime */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, + set = Ember.set, + slice = Array.prototype.slice, + getProperties = Ember.getProperties; /** ## Overview @@ -10858,15 +13928,7 @@ Ember.Observable = Ember.Mixin.create({ @return {Hash} */ getProperties: function() { - var ret = {}; - var propertyNames = arguments; - if (arguments.length === 1 && Ember.typeOf(arguments[0]) === 'array') { - propertyNames = arguments[0]; - } - for(var i = 0; i < propertyNames.length; i++) { - ret[propertyNames[i]] = get(this, propertyNames[i]); - } - return ret; + return getProperties.apply(null, [this].concat(slice.call(arguments))); }, /** @@ -10874,7 +13936,7 @@ Ember.Observable = Ember.Mixin.create({ This method is generally very similar to calling `object[key] = value` or `object.key = value`, except that it provides support for computed - properties, the `unknownProperty()` method and property observers. + properties, the `setUnknownProperty()` method and property observers. ### Computed Properties @@ -10888,9 +13950,9 @@ Ember.Observable = Ember.Mixin.create({ ### Unknown Properties If you try to set a value on a key that is undefined in the target - object, then the `unknownProperty()` handler will be called instead. This + object, then the `setUnknownProperty()` handler will be called instead. This gives you an opportunity to implement complex "virtual" properties that - are not predefined on the object. If `unknownProperty()` returns + are not predefined on the object. If `setUnknownProperty()` returns undefined, then `set()` will simply set the value on the object. ### Property Observers @@ -11120,29 +14182,6 @@ Ember.Observable = Ember.Mixin.create({ return Ember.hasListeners(this, key+':change'); }, - /** - @deprecated - @method getPath - @param {String} path The property path to retrieve - @return {Object} The property value or undefined. - */ - getPath: function(path) { - - return this.get(path); - }, - - /** - @deprecated - @method setPath - @param {String} path The path to the property that will be set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - setPath: function(path, value) { - - return this.set(path, value); - }, - /** Retrieves the value of a property, or a default value in the case that the property returns `undefined`. @@ -11175,7 +14214,7 @@ Ember.Observable = Ember.Mixin.create({ */ incrementProperty: function(keyName, increment) { if (Ember.isNone(increment)) { increment = 1; } - + Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); set(this, keyName, (get(this, keyName) || 0) + increment); return get(this, keyName); }, @@ -11195,7 +14234,7 @@ Ember.Observable = Ember.Mixin.create({ */ decrementProperty: function(keyName, decrement) { if (Ember.isNone(decrement)) { decrement = 1; } - + Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); set(this, keyName, (get(this, keyName) || 0) - decrement); return get(this, keyName); }, @@ -11346,9 +14385,13 @@ Ember.TargetActionSupport = Ember.Mixin.create({ */ triggerAction: function(opts) { opts = opts || {}; - var action = opts['action'] || get(this, 'action'), - target = opts['target'] || get(this, 'targetObject'), - actionContext = opts['actionContext'] || get(this, 'actionContextObject') || this; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; + + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } if (target && action) { var ret; @@ -11356,7 +14399,7 @@ Ember.TargetActionSupport = Ember.Mixin.create({ if (target.send) { ret = target.send.apply(target, [action, actionContext]); } else { - + Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); ret = target[action].apply(target, [actionContext]); } @@ -11492,11 +14535,6 @@ Ember.Evented = Ember.Mixin.create({ Ember.sendEvent(this, name, args); }, - fire: function(name) { - - this.trigger.apply(this, arguments); - }, - /** Cancels subscription for given name, target, and method. @@ -11550,8 +14588,8 @@ Ember.DeferredMixin = Ember.Mixin.create({ Add handlers to be called when the Deferred object is resolved or rejected. @method then - @param {Function} doneCallback a callback function to be called when done - @param {Function} failCallback a callback function to be called when failed + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed */ then: function(resolve, reject) { var deferred, promise, entity; @@ -11609,6 +14647,664 @@ Ember.DeferredMixin = Ember.Mixin.create({ (function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get; + +/** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. + + `Ember.ActionHandler` is used internally by Ember in `Ember.View`, + `Ember.Controller`, and `Ember.Route`. + + @class ActionHandler + @namespace Ember +*/ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], + + /** + @private + + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + if (props.actions && !props._actions) { + props._actions = Ember.merge(props._actions || {}, props.actions); + delete props.actions; + } + }, + + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; + + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } + + if (target = get(this, 'target')) { + Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); + target.send.apply(target, arguments); + } + } + +}); + +})(); + + + +(function() { +var set = Ember.set, get = Ember.get, + resolve = Ember.RSVP.resolve, + rethrow = Ember.RSVP.rethrow, + not = Ember.computed.not, + or = Ember.computed.or; + +/** + @module ember + @submodule ember-runtime + */ + +function installPromise(proxy, promise) { + promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + }).fail(rethrow); +} + +/** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` + + the controller has bindable attributes which + track the promises life cycle + + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` + + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. + + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` + + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. + + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } + + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` + + If the controller is backing a template, the attributes are + bindable from within that template + + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + reason: null, + isPending: not('isSettled').readOnly(), + isSettled: or('isRejected', 'isFulfilled').readOnly(), + isRejected: false, + isFulfilled: false, + + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + promise = resolve(promise); + installPromise(this, promise); + return promise; + } else { + throw new Error("PromiseProxy's promise must be set"); + } + }), + + then: function(fulfill, reject) { + return get(this, 'promise').then(fulfill, reject); + } +}); + + +})(); + + + +(function() { + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; + +/** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. + + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. +*/ +Ember.TrackedArray = function (items) { + if (arguments.length < 1) { items = []; } + + var length = get(items, 'length'); + + if (length) { + this._content = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._content = []; + } +}; + +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; + +Ember.TrackedArray.prototype = { + + /** + Track that `newItems` were added to the tracked array at `index`. + + @method addItems + @param index + @param newItems + */ + addItems: function (index, newItems) { + var count = get(newItems, 'length'), + match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; + + newArrayOperation = new ArrayOperation(INSERT, count, newItems); + + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._content.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._content.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } + + this._composeInsert(composeIndex); + }, + + /** + Track that `count` items were removed at `index`. + + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; + + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._content.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + + return this._composeDelete(composeIndex); + }, + + /** + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. + + `callback` will be called for each operation and will be passed the following arguments: + +* {array} items The items for the given operation +* {number} offset The computed offset of the items, ie the index in the +array of the first item for this operation. +* {string} operation The type of the operation. One of `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` +* + + @method apply + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; + + forEach(this._content, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.operation); + + if (arrayOperation.operation !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); + + this._content = [new ArrayOperation(RETAIN, items.length, items)]; + }, + + /** + Return an ArrayOperationMatch for the operation that contains the item at `index`. + + @method _findArrayOperation + + @param {number} index the index of the item whose operation information + should be returned. + @private + */ + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; + + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._content.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._content[arrayOperationIndex]; + + if (arrayOperation.operation === DELETE) { continue; } + + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } + + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, + + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._content[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.operation, splitItems.length, splitItems); + + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + + this._content.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, + + // TODO: unify _composeInsert, _composeDelete + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._content[index], + leftArrayOperation = this._content[index-1], // may be undefined + rightArrayOperation = this._content[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.operation, + rightOp = rightArrayOperation && rightArrayOperation.operation; + + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + + if (rightOp === INSERT) { + // also merge right + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._content.splice(index, 2); + } else { + // only merge left + this._content.splice(index, 1); + } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._content.splice(index + 1, 1); + } + }, + + _composeDelete: function (index) { + var arrayOperation = this._content[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._content[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.operation, + nextArrayOperation, + nextOp, + nextCount, + removedItems = []; + + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; + } + + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._content[i]; + nextOp = nextArrayOperation.operation; + nextCount = nextArrayOperation.count; + + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } + + if (nextCount > deletesToGo) { + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; + + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; + + deletesToGo = 0; + } else { + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + + if (nextOp === INSERT) { + arrayOperation.count -= nextCount; + } + } + + if (arrayOperation.count > 0) { + this._content.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in D:3 I:4 + this._content.splice(index, 1); + } + + return removedItems; + } +}; + +function ArrayOperation (operation, count, items) { + this.operation = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} + +/** + Internal data structure used to include information when looking up operations + by item index. + + @method ArrayOperationMatch + @private + @property {ArrayOperation} operation + @property {number} index The index of `operation` in the array of operations. + @property {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @property {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. +*/ +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; + +function Operation (type, count) { + this.type = type; + this.count = count; +} + +/** + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. + + @class SubArray + @namespace Ember +*/ +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } + + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; + +Ember.SubArray.prototype = { + /** + Track that an item was added to the tracked array. + + @method addItem + + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. + + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; + + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; + + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; + + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } + + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } + + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); + + if (match) { + returnValue = seenInSubArray; + } + + self._composeAt(self._operations.length-1); + }); + + return returnValue; + }, + + /** + Track that an item was removed from the tracked array. + + @method removeItem + + @param {number} index The index of the item in the tracked array. + + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; + + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } + + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }); + + return returnValue; + }, + + + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; + + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; + + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } + + notFoundCallback(seenInSubArray); + }, + + _composeAt: function(index) { + var op = this._operations[index], + otherOp; + + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } + + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + } + } + + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } + } +}; })(); @@ -11641,6 +15337,8 @@ var set = Ember.set, get = Ember.get, generateGuid = Ember.generateGuid, meta = Ember.meta, rewatch = Ember.rewatch, + finishChains = Ember.finishChains, + sendEvent = Ember.sendEvent, destroy = Ember.destroy, schedule = Ember.run.schedule, Mixin = Ember.Mixin, @@ -11671,7 +15369,7 @@ function makeCtor() { } o_defineProperty(this, GUID_KEY, undefinedDescriptor); o_defineProperty(this, '_super', undefinedDescriptor); - var m = meta(this); + var m = meta(this), proto = m.proto; m.proto = this; if (initMixins) { // capture locally so we can clear the closed over variable @@ -11689,6 +15387,7 @@ function makeCtor() { for (var i = 0, l = props.length; i < l; i++) { var properties = props[i]; + Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); for (var keyName in properties) { if (!properties.hasOwnProperty(keyName)) { continue; } @@ -11708,7 +15407,9 @@ function makeCtor() { var desc = m.descs[keyName]; - + Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); + Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); + Ember.assert("`actions` must be provided at extend time, not at create time, when Ember.ActionHandler is used (i.e. views, controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { var baseValue = this[keyName]; @@ -11738,19 +15439,11 @@ function makeCtor() { } } } - finishPartial(this, m); - var hasChains = (typeof m.chains) !== "undefined"; - delete m.proto; - - if (hasChains) { - if (m.chains.value() !== this) { - m.chains = m.chains.copy(this); - } - m.chains.didChange(true); - } - this.init.apply(this, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); }; Class.toString = Mixin.prototype.toString; @@ -11794,8 +15487,6 @@ CoreObject.PrototypeMixin = Mixin.create({ return this; }, - isInstance: true, - /** An overridable method called when objects are instantiated. By default, does nothing unless it is overridden during class definition. @@ -11884,7 +15575,10 @@ CoreObject.PrototypeMixin = Mixin.create({ are also concatenated, in addition to `classNames`. This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). @property concatenatedProperties @type Array @@ -11938,6 +15632,8 @@ CoreObject.PrototypeMixin = Mixin.create({ /** Override to implement teardown. + + @method willDestroy */ willDestroy: Ember.K, @@ -12101,12 +15797,65 @@ var ClassMixin = Mixin.create({ return new C(); }, + /** + + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + o = MyObject.create(); + o.get('name'); // 'an object' + + MyObject.reopen({ + say: function(msg){ + console.log(msg); + } + }) + + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" + + o.say("goodbye"); // logs "goodbye" + ``` + + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ reopen: function() { this.willReopen(); reopen.apply(this.PrototypeMixin, arguments); return this; }, + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` + + @method reopenClass + */ reopenClass: function() { reopen.apply(this.ClassMixin, arguments); applyMixin(this, arguments, false); @@ -12156,6 +15905,7 @@ var ClassMixin = Mixin.create({ metaForProperty: function(key) { var desc = meta(this.proto(), false).descs[key]; + Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); return desc._meta || {}; }, @@ -12357,7 +16107,7 @@ function findNamespaces() { } if (isNamespace) { - + Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); obj[NAME_KEY] = prop; } } @@ -12384,6 +16134,8 @@ function classToString() { if (this[NAME_KEY]) { ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; } else { var str = superClassString(this); if (str) { @@ -12573,6 +16325,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array _contentDidChange: Ember.observer(function() { var content = get(this, 'content'); + Ember.assert("Can't set ArrayProxy's content to itself", content !== this); this._setupContent(); }, 'content'), @@ -12602,6 +16355,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array var arrangedContent = get(this, 'arrangedContent'), len = arrangedContent ? get(arrangedContent, 'length') : 0; + Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); this._setupArrangedContent(); @@ -12646,7 +16400,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array _replace: function(idx, amt, objects) { var content = get(this, 'content'); - + Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); if (content) this.replaceContent(idx, amt, objects); return this; }, @@ -12778,7 +16532,9 @@ var get = Ember.get, removeBeforeObserver = Ember.removeBeforeObserver, removeObserver = Ember.removeObserver, propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange; + propertyDidChange = Ember.propertyDidChange, + meta = Ember.meta, + defineProperty = Ember.defineProperty; function contentPropertyWillChange(content, contentKey) { var key = contentKey.slice(8); // remove "content." @@ -12869,7 +16625,7 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * */ content: null, _contentDidChange: Ember.observer(function() { - + Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); }, 'content'), isTruthy: Ember.computed.bool('content'), @@ -12896,29 +16652,19 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * }, setUnknownProperty: function (key, value) { - var content = get(this, 'content'); + var m = meta(this); + if (m.proto === this) { + // if marked as prototype then just defineProperty + // rather than delegate + defineProperty(this, key, null, value); + return value; + } + var content = get(this, 'content'); + 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({ - create: function () { - var mixin, prototype, i, l, properties, keyName; - if (arguments.length) { - prototype = this.proto(); - for (i = 0, l = arguments.length; i < l; i++) { - properties = arguments[i]; - for (keyName in properties) { - if (!properties.hasOwnProperty(keyName) || keyName in prototype) { continue; } - if (!mixin) mixin = {}; - mixin[keyName] = null; - } - } - if (mixin) this._initMixins([mixin]); - } - return this._super.apply(this, arguments); - } }); })(); @@ -12933,7 +16679,8 @@ Ember.ObjectProxy.reopenClass({ var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; -var forEach = Ember.EnumerableUtils.forEach; +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.ArrayPolyfills.indexOf; var EachArray = Ember.Object.extend(Ember.Array, { @@ -12965,7 +16712,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'); @@ -12991,7 +16738,7 @@ function removeObserverForContentKey(content, keyName, proxy, idx, loc) { guid = guidFor(item); indicies = objects[guid]; - indicies[indicies.indexOf(loc)] = null; + indicies[indexOf.call(indicies, loc)] = null; } } } @@ -13140,7 +16887,7 @@ Ember.EachProxy = Ember.Object.extend({ */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; // Add Ember.Array to Array.prototype. Remove methods with native // implementations and supply some more optimized versions of generic methods @@ -13162,7 +16909,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember // primitive for array support. replace: function(idx, amt, objects) { - if (this.isFrozen) throw Ember.FROZEN_ERROR ; + if (this.isFrozen) throw Ember.FROZEN_ERROR; // if we replaced exactly the same number of items, then pass only the // replaced range. Otherwise, pass the full remaining array length @@ -13171,14 +16918,13 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember this.arrayContentWillChange(idx, amt, len); if (!objects || objects.length === 0) { - this.splice(idx, amt) ; + this.splice(idx, amt); } else { - var args = [idx, amt].concat(objects) ; - this.splice.apply(this,args) ; + replace(this, idx, amt, objects); } this.arrayContentDidChange(idx, amt, len); - return this ; + return this; }, // If you ask for an unknown property, then try to collect the value @@ -13255,7 +17001,26 @@ Ember.NativeArray = NativeArray; /** Creates an `Ember.NativeArray` from an Array like object. - Does not modify the original object. + Does not modify the original object. Ember.A is not needed if + `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, + it is recommended that you use Ember.A when creating addons for + ember or when you can not garentee that `Ember.EXTEND_PROTOTYPES` + will be `true`. + + Example + + ```js + var Pagination = Ember.CollectionView.extend({ + tagName: 'ul', + classNames: ['pagination'], + init: function() { + this._super(); + if (!this.get('content')) { + this.set('content', Ember.A([])); + } + } + }); + ``` @method A @for Ember @@ -13268,7 +17033,17 @@ Ember.A = function(arr) { /** Activates the mixin on the Array.prototype if not already applied. Calling - this method more than once is safe. + this method more than once is safe. This will be called when ember is loaded + unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` + set to `false`. + + Example + + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); + } + ``` @method activate @for Ember.NativeArray @@ -13363,8 +17138,8 @@ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.is When using `Ember.Set`, you can observe the `"[]"` property to be alerted whenever the content changes. You can also add an enumerable observer to the set to be notified of specific objects that are added and - removed from the set. See `Ember.Enumerable` for more information on - enumerables. + removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) + for more information on enumerables. This is often unhelpful. If you are filtering sets of objects, for instance, it is very inefficient to re-filter all of the items each time the set @@ -13779,6 +17554,20 @@ var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; var loaded = {}; /** + +Detects when a specific package of Ember (e.g. 'Ember.Handlebars') +has fully loaded and is available for extension. + +The provided `callback` will be called with the `name` passed +resolved from a string into the object: + +```javascript +Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); +}); +``` + + @method onLoad @for Ember @param name {String} name of hook @@ -13796,6 +17585,10 @@ Ember.onLoad = function(name, callback) { }; /** + +Called when an Ember.js package (e.g Ember.Handlebars) has finished +loading. Triggers any callbacks registered for this event. + @method runLoadHooks @for Ember @param name {String} name of hook @@ -13834,39 +17627,18 @@ var get = Ember.get; compose Ember's controller layer: `Ember.Controller`, `Ember.ArrayController`, and `Ember.ObjectController`. - Within an `Ember.Router`-managed application single shared instaces of every - Controller object in your application's namespace will be added to the - application's `Ember.Router` instance. See `Ember.Application#initialize` - for additional information. - - ## Views - - By default a controller instance will be the rendering context - for its associated `Ember.View.` This connection is made during calls to - `Ember.ControllerMixin#connectOutlet`. - - Within the view's template, the `Ember.View` instance can be accessed - through the controller with `{{view}}`. - - ## Target Forwarding - - By default a controller will target your application's `Ember.Router` - instance. Calls to `{{action}}` within the template of a controller's view - are forwarded to the router. See `Ember.Handlebars.helpers.action` for - additional information. - @class ControllerMixin @namespace Ember */ -Ember.ControllerMixin = Ember.Mixin.create({ +Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { /* ducktype as a controller */ isController: true, /** - The object to which events from the view should be sent. + The object to which actions from the view should be sent. For example, when a Handlebars template uses the `{{action}}` helper, - it will attempt to send the event to the view's controller's `target`. + it will attempt to send the action to the view's controller's `target`. By default, a controller's `target` is set to the router after it is instantiated by `Ember.Application#initialize`. @@ -13884,16 +17656,16 @@ Ember.ControllerMixin = Ember.Mixin.create({ model: Ember.computed.alias('content'), - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, - if (this[actionName]) { - - this[actionName].apply(this, args); - } else if (target = get(this, 'target')) { - - target.send.apply(target, arguments); - } + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + this[actionName].apply(this, args); + return; } }); @@ -13942,6 +17714,29 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} ``` + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. + + ```javascript + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ``` + + SortableMixin works by sorting the arrangedContent array, which is the array that + arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that + array will not display the sorted list: + + ```javascript + songsController.get('content').get('firstObject'); // Returns the unsorted original content + songsController.get('firstObject'); // Returns the sorted content. + ``` + + Although the sorted content can also be accessed through the arrangedContent property, + it is preferable to use the proxied class and not the arrangedContent array directly. + @class SortableMixin @namespace Ember @uses Ember.MutableEnumerable @@ -13951,6 +17746,9 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** Specifies which properties dictate the arrangedContent's sort order. + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. + @property {Array} sortProperties */ sortProperties: null, @@ -13964,7 +17762,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** 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 + want to do custom comparisons. Functions must be of the type expected by Array#sort, i.e. return 0 if the two parameters are equal, return a negative value if the first parameter is smaller than the second or @@ -13990,6 +17788,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { sortAscending = get(this, 'sortAscending'), sortFunction = get(this, 'sortFunction'); + Ember.assert("you need to define `sortProperties`", !!sortProperties); forEach(sortProperties, function(propertyName) { if (result === 0) { @@ -14020,6 +17819,13 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { isSorted: Ember.computed.bool('sortProperties'), + /** + Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. + Also sets up observers for each sortProperty on each item in the content Array. + + @property arrangedContent + */ + arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { var content = get(this, 'content'), isSorted = get(this, 'isSorted'), @@ -14337,28 +18143,36 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } this._super(); + this.set('_subControllers', Ember.A()); }, + content: Ember.computed(function () { + return Ember.A(); + }), + 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; }, @@ -14388,12 +18202,11 @@ 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. It is intended + to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying + content object, and to forward unhandled action attempts to its `target`. - `Ember.ObjectController` derives its functionality from its superclass + `Ember.ObjectController` derives this functionality from its superclass `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. @class ObjectController @@ -14431,7 +18244,7 @@ Ember Runtime */ var jQuery = Ember.imports.jQuery; - +Ember.assert("Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @@ -14575,7 +18388,7 @@ var setInnerHTML = function(element, html) { } else { // Firefox versions < 11 do not have support for element.outerHTML. var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); - + Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", outerHTML); var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], endTag = '</'+tagName+'>'; @@ -14677,9 +18490,14 @@ function escapeAttribute(value) { final representation. `Ember.RenderBuffer` will generate HTML which can be pushed to the DOM. + ```javascript + var buffer = Ember.RenderBuffer('div'); + ``` + @class RenderBuffer @namespace Ember @constructor + @param {String} tagName tag name (such as 'div' or 'p') used for the buffer */ Ember.RenderBuffer = function(tagName) { return new Ember._RenderBuffer(tagName); @@ -15226,11 +19044,13 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro rootElement = Ember.$(get(this, 'rootElement')); - - + Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); + Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); + Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length); rootElement.addClass('ember-application'); + Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application')); for (event in events) { if (events.hasOwnProperty(event)) { @@ -15316,7 +19136,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(); } @@ -15424,6 +19246,7 @@ var get = Ember.get, set = Ember.set; var guidFor = Ember.guidFor; var a_forEach = Ember.EnumerableUtils.forEach; var a_addObject = Ember.EnumerableUtils.addObject; +var meta = Ember.meta; var childViewsProperty = Ember.computed(function() { var childViews = this._childViews, ret = Ember.A(), view = this; @@ -15441,7 +19264,7 @@ var childViewsProperty = Ember.computed(function() { ret.replace = function (idx, removedCount, addedViews) { if (view instanceof Ember.ContainerView) { - + Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); return view.replace(idx, removedCount, addedViews); } throw new Error("childViews is immutable"); @@ -15450,6 +19273,7 @@ var childViewsProperty = Ember.computed(function() { return ret; }); +Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); /** Global hash of shared templates. This will automatically be populated @@ -15463,7 +19287,13 @@ var childViewsProperty = Ember.computed(function() { Ember.TEMPLATES = {}; /** - `Ember.CoreView` is + `Ember.CoreView` is an abstract class that exists to give view-like behavior + to both Ember's main view class `Ember.View` and other classes like + `Ember._SimpleMetamorphView` that don't need the fully functionaltiy of + `Ember.View`. + + Unless you have specific needs for `CoreView`, you will use `Ember.View` + in your applications. @class CoreView @namespace Ember @@ -15471,7 +19301,7 @@ Ember.TEMPLATES = {}; @uses Ember.Evented */ -Ember.CoreView = Ember.Object.extend(Ember.Evented, { +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { isView: true, states: states, @@ -15586,6 +19416,18 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { } }, + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, + + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); + Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object (' + actionName + ' on ' + this + ')', false); + this[actionName].apply(this, args); + return; + }, + has: function(name) { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, @@ -15687,7 +19529,7 @@ var EMPTY_ARRAY = []; The default HTML tag name used for a view's DOM representation is `div`. This can be customized by setting the `tagName` property. The following view -class: + class: ```javascript ParagraphView = Ember.View.extend({ @@ -15813,7 +19655,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 }); @@ -15836,7 +19678,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 }); @@ -15861,8 +19703,8 @@ class: will be removed. Both `classNames` and `classNameBindings` are concatenated properties. See - `Ember.Object` documentation for more information about concatenated - properties. + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. ## HTML Attributes @@ -15922,7 +19764,7 @@ class: Updates to the the property of an attribute binding will result in automatic update of the HTML attribute in the view's rendered HTML representation. - `attributeBindings` is a concatenated property. See `Ember.Object` + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) documentation for more information about concatenated properties. ## Templates @@ -15965,9 +19807,6 @@ class: Using a value for `templateName` that does not have a Handlebars template with a matching `data-template-name` attribute will throw an error. - Assigning a value to both `template` and `templateName` properties will throw - an error. - For views classes that may have a template later defined (e.g. as the block portion of a `{{view}}` Handlebars helper call in another template or in a subclass), you can provide a `defaultTemplate` property set to compiled @@ -16073,7 +19912,8 @@ class: </div> ``` - See `Handlebars.helpers.yield` for more information. + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. ## Responding to Browser Events @@ -16130,7 +19970,7 @@ class: }, eventManager: Ember.Object.create({ mouseEnter: function(event, view) { - // takes presedence over AView#mouseEnter + // takes precedence over AView#mouseEnter } }) }); @@ -16170,7 +20010,7 @@ class: ### Handlebars `{{action}}` Helper - See `Handlebars.helpers.action`. + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). ### Event Names @@ -16225,8 +20065,8 @@ class: ## Handlebars `{{view}}` Helper Other `Ember.View` instances can be included as part of a view's template by - using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for - additional information. + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. @class View @namespace Ember @@ -16301,6 +20141,7 @@ Ember.View = Ember.CoreView.extend( var templateName = get(this, 'templateName'), template = this.templateForName(templateName, 'template'); + Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); return template || get(this, 'defaultTemplate'); }).property('templateName'), @@ -16335,13 +20176,19 @@ Ember.View = Ember.CoreView.extend( var layoutName = get(this, 'layoutName'), layout = this.templateForName(layoutName, 'layout'); + Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); 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); // the defaultContainer is deprecated var container = this.container || (Ember.Container && Ember.Container.defaultContainer); @@ -16369,14 +20216,6 @@ Ember.View = Ember.CoreView.extend( } }).volatile(), - /** - The parent context for this template. - */ - parentContext: function() { - var parentView = get(this, '_parentView'); - return parentView && get(parentView, '_context'); - }, - /** @private @@ -16474,7 +20313,7 @@ Ember.View = Ember.CoreView.extend( @deprecated */ nearestInstanceOf: function(klass) { - + Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType."); var view = get(this, 'parentView'); while (view) { @@ -16610,6 +20449,7 @@ Ember.View = Ember.CoreView.extend( // is the view's controller by default. A hash of data is also passed that provides // the template with access to the view and render buffer. + Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function'); // The template should write directly to the render buffer instead // of returning a string. output = template(context, { data: data }); @@ -16880,7 +20720,8 @@ 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); }); @@ -16901,7 +20742,8 @@ 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() { Ember.$(target).empty(); @@ -17164,8 +21006,7 @@ Ember.View = Ember.CoreView.extend( */ _elementDidChange: Ember.observer(function() { this.forEachChildView(function(view) { - var meta = Ember.meta(view); - delete meta.cache['element']; + delete meta(view).cache.element; }); }, 'element'), @@ -17260,7 +21101,7 @@ Ember.View = Ember.CoreView.extend( visually challenged users navigate rich web applications. The full list of valid WAI-ARIA roles is available at: - http://www.w3.org/TR/wai-aria/roles#roles_categorization + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) @property ariaRole @type String @@ -17286,7 +21127,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the 'high' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['priority'] priority: 'high' }); @@ -17297,7 +21138,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the 'is-urgent' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isUrgent'] isUrgent: true }); @@ -17308,7 +21149,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the 'urgent' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isUrgent:urgent'] isUrgent: true }); @@ -17329,7 +21170,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the type attribute to the element // with the value "button", like <div type="button"> - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['type'], type: 'button' }); @@ -17340,7 +21181,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Renders something like <div enabled="enabled"> - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['enabled'], enabled: true }); @@ -17372,17 +21213,11 @@ Ember.View = Ember.CoreView.extend( // setup child views. be sure to clone the child views array first this._childViews = this._childViews.slice(); + Ember.assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array'); this.classNameBindings = Ember.A(this.classNameBindings.slice()); + 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) { @@ -17493,22 +21328,26 @@ 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 */ createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + if (view.isView && view._parentView === this && view.container === this.container) { return view; } 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 @@ -17516,14 +21355,24 @@ Ember.View = Ember.CoreView.extend( if (view.viewName) { set(get(this, 'concreteView'), view.viewName, view); } - } else { + } 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); + attrs.container = this.container; + + if (!get(view, 'templateData')) { + attrs.templateData = get(this, 'templateData'); + } Ember.setProperties(view, attrs); - if (!get(view, 'templateData')) { - set(view, 'templateData', get(this, 'templateData')); - } } return view; @@ -17732,7 +21581,7 @@ Ember.View.reopenClass({ Parse a path and return an object which holds the parsed properties. - For example a path like "content.isEnabled:enabled:disabled" wil return the + For example a path like "content.isEnabled:enabled:disabled" will return the following object: ```javascript @@ -17953,10 +21802,18 @@ Ember.merge(preRender, { var viewCollection = view.viewHierarchyCollection(); viewCollection.trigger('willInsertElement'); - // after createElement, the view will be in the hasElement state. + fn.call(view); - viewCollection.transitionTo('inDOM', false); - viewCollection.trigger('didInsertElement'); + + // We transition to `inDOM` if the element exists in the DOM + var element = view.get('element'); + while (element = element.parentNode) { + if (element === document) { + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); + } + } + }, renderToBufferIfNeeded: function(view, buffer) { @@ -18033,7 +21890,7 @@ Ember.merge(inBuffer, { }, empty: function() { - + Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications."); }, renderToBufferIfNeeded: function (view, buffer) { @@ -18160,7 +22017,7 @@ Ember.merge(inDOM, { // Register the view for event handling. This hash is used by // Ember.EventDispatcher to dispatch incoming events. if (!view.isVirtual) { - + Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); Ember.View.views[view.elementId] = view; } @@ -18470,7 +22327,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { replace: function(idx, removedCount, addedViews) { var addedCount = addedViews ? get(addedViews, 'length') : 0; var self = this; - + Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); this.arrayContentWillChange(idx, removedCount, addedCount); this.childViewsWillChange(this._childViews, idx, removedCount); @@ -18495,7 +22352,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { length: Ember.computed(function () { return this._childViews.length; - }), + }).volatile(), /** @private @@ -18572,7 +22429,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); @@ -18592,7 +22452,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { _currentViewDidChange: Ember.observer(function() { var currentView = get(this, 'currentView'); if (currentView) { - + Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); this.pushObject(currentView); } }, 'currentView'), @@ -18822,11 +22682,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; manipulated. Instead, add, remove, replace items from its `content` property. This will trigger appropriate changes to its rendered HTML. - ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper - - `Ember.Handlebars` provides a helper specifically for adding - `CollectionView`s to templates. See `Ember.Handlebars.collection` for more - details @class CollectionView @namespace Ember @@ -18871,12 +22726,25 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie */ itemViewClass: Ember.View, + /** + Setup a CollectionView + + @method init + */ init: function() { var ret = this._super(); this._contentDidChange(); return ret; }, + /** + @private + + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @method _contentWillChange + */ _contentWillChange: Ember.beforeObserver(function() { var content = this.get('content'); @@ -18907,10 +22775,22 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie this.arrayDidChange(content, 0, null, len); }, 'content'), - _assertArrayLike: function(content) { + /** + @private + Ensure that the content implements Ember.Array + + @method _assertArrayLike + */ + _assertArrayLike: function(content) { + Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); }, + /** + Removes the content and content observers. + + @method destroy + */ destroy: function() { if (!this._super()) { return; } @@ -18924,6 +22804,19 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie return this; }, + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ arrayWillChange: function(content, start, removedCount) { // If the contents were empty before and this template collection has an // empty view remove it now. @@ -18969,16 +22862,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); - } - + 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); @@ -18990,27 +22887,50 @@ 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); }, + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ createChildView: function(view, attrs) { 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; } @@ -19070,7 +22990,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; ```html <!-- app-profile template --> <h1>{{person.title}}</h1> - <img {{bindAttr src=person.avatar}}> + <img {{bind-attr src=person.avatar}}> <p class='signature'>{{person.signature}}</p> ``` @@ -19092,15 +23012,18 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; 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 - component. + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. For example, you could implement the action `hello` for the `app-profile` component: - ```js + ```javascript App.AppProfileComponent = Ember.Component.extend({ - hello: function(name) { - console.log("Hello", name) + actions: { + hello: function(name) { + console.log("Hello", name); + } } }); ``` @@ -19132,60 +23055,122 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { this._super(); set(this, 'context', this); set(this, 'controller', this); - set(this, 'templateData', {keywords: {}}); }, + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _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: '', + _contextView: parentView, + template: template, + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ 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. + Sends an action to component's controller. A component inherits its + controller from the context in which it is used. - By default, calling `sendAction()` will send an action with the name - of the component's `action` property. + By default, calling `sendAction()` will send an action with the name + of the component's `action` property. - For example, if the component had a property `action` with the value - `"addItem"`, calling `sendAction()` would send the `addItem` action - to the component's controller. + For example, if the component had a property `action` with the value + `"addItem"`, calling `sendAction()` would send the `addItem` action + to the component's controller. - If you provide an argument to `sendAction()`, that key will be used to look - up the action name. + If you provide the `action` argument to `sendAction()`, that key will + be used to look up the action name. - For example, if the component had a property `playing` with the value - `didStartPlaying`, calling `sendAction('playing')` would send the - `didStartPlaying` action to the component's controller. + For example, if the component had a property `playing` with the value + `didStartPlaying`, calling `sendAction('playing')` would send the + `didStartPlaying` action to the component's controller. - Whether or not you are using the default action or a named action, if - the action name is not defined on the component, calling `sendAction()` - does not have any effect. + Whether or not you are using the default action or a named action, if + the action name is not defined on the component, calling `sendAction()` + does not have any effect. - For example, if you call `sendAction()` on a component that does not have - an `action` property defined, no action will be sent to the controller, - nor will an exception be raised. + For example, if you call `sendAction()` on a component that does not have + an `action` property defined, no action will be sent to the controller, + nor will an exception be raised. - @param [action] {String} the action to trigger + You can send a context object with the action by supplying the `context` + argument. The context will be supplied as the first argument in the + target's action method. Example: + + ```javascript + App.MyTreeComponent = Ember.Component.extend({ + click: function() { + this.sendAction('didClickTreeNode', this.get('node')); + } + }); + + App.CategoriesController = Ember.Controller.extend({ + actions: { + didClickCategory: function(category) { + //Do something with the node/category that was clicked + } + } + }); + ``` + + ```handlebars + {{! categories.hbs}} + {{my-tree didClickTreeNode='didClickCategory'}} + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action */ - sendAction: function(action) { + sendAction: function(action, context) { var actionName; // Send the default action if (action === undefined) { actionName = get(this, 'action'); - + 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 + action: actionName, + actionContext: context }); } }); @@ -19288,9 +23273,10 @@ define("metamorph", 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 = false, + 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 @@ -19397,6 +23383,14 @@ define("metamorph", range.insertNode(fragment); }; + /** + * @public + * + * Remove this object (including starting and ending + * placeholders). + * + * @method remove + */ removeFunc = function() { // get a range for the current metamorph object including // the starting and ending placeholders. @@ -19437,7 +23431,7 @@ define("metamorph", }; } else { - /** + /* * This code is mostly taken from jQuery, with one exception. In jQuery's case, we * have some HTML and we need to figure out how to convert it into some nodes. * @@ -19491,12 +23485,12 @@ define("metamorph", } }; - /** + /* * Given a parent node and some HTML, generate a set of nodes. Return the first * node, which will allow us to traverse the rest using nextSibling. * * We need to do this because innerHTML in IE does not really parse the nodes. - **/ + */ var firstNodeFor = function(parentNode, html) { var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; var depth = arr[0], start = arr[1], end = arr[2]; @@ -19529,7 +23523,7 @@ define("metamorph", return element; }; - /** + /* * In some cases, Internet Explorer can create an anonymous node in * the hierarchy with no tagName. You can create this scenario via: * @@ -19539,7 +23533,7 @@ define("metamorph", * * If our script markers are inside such a node, we need to find that * node and use *it* as the marker. - **/ + */ var realNode = function(start) { while (start.parentNode.tagName === "") { start = start.parentNode; @@ -19548,7 +23542,7 @@ define("metamorph", return start; }; - /** + /* * When automatically adding a tbody, Internet Explorer inserts the * tbody immediately before the first <tr>. Other browsers create it * before the first node, no matter what. @@ -19575,7 +23569,8 @@ define("metamorph", * * This code reparents the first script tag by making it the tbody's * first child. - **/ + * + */ var fixParentage = function(start, end) { if (start.parentNode !== end.parentNode) { end.parentNode.insertBefore(start, end.parentNode.firstChild); @@ -19755,7 +23750,8 @@ if (!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } - +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 @@ -19812,7 +23808,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 @@ -19841,20 +23837,11 @@ function makeBindings(options) { @param {String} dependentKeys* */ Ember.Handlebars.helper = function(name, value) { - if (Ember.Component.detect(value)) { - - - var proto = value.proto(); - if (!proto.layoutName && !proto.templateName) { - value.reopen({ - layoutName: 'components/' + name - }); - } - } + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); if (Ember.View.detect(value)) { Ember.Handlebars.registerHelper(name, function(options) { - + 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); }); @@ -19903,7 +23890,6 @@ if (Handlebars.JavaScriptCompiler) { Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { return "''"; }; @@ -20005,7 +23991,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; }; } @@ -20089,7 +24078,6 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { } return value; }; -Ember.Handlebars.getPath = Ember.deprecateFunc('`Ember.Handlebars.getPath` has been changed to `Ember.Handlebars.get` for consistency.', Ember.Handlebars.get); Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; @@ -20268,59 +24256,101 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { numProperties = properties.length, options = arguments[arguments.length - 1], normalizedProperties = [], + types = options.types, data = options.data, hash = options.hash, view = data.view, currentContext = (options.contexts && options.contexts[0]) || this, - normalized, - pathRoot, path, prefixPathForDependentKeys = '', - loc, hashOption; + prefixPathForDependentKeys = '', + loc, len, hashOption, + boundOption, property, + normalizedValue = Ember._SimpleHandlebarsView.prototype.normalizedValue; + Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); // Detect bound options (e.g. countBinding="otherCount") - hash.boundOptions = {}; + var boundOptions = hash.boundOptions = {}; for (hashOption in hash) { - if (!hash.hasOwnProperty(hashOption)) { continue; } - - if (Ember.IS_BINDING.test(hashOption) && typeof hash[hashOption] === 'string') { + if (Ember.IS_BINDING.test(hashOption)) { // Lop off 'Binding' suffix. - hash.boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; + boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; } } // Expose property names on data.properties object. + var watchedProperties = []; data.properties = []; for (loc = 0; loc < numProperties; ++loc) { data.properties.push(properties[loc]); - normalizedProperties.push(normalizePath(currentContext, properties[loc], data)); + if (types[loc] === 'ID') { + var normalizedProp = normalizePath(currentContext, properties[loc], data); + normalizedProperties.push(normalizedProp); + watchedProperties.push(normalizedProp); + } else { + normalizedProperties.push(null); + } } + // Handle case when helper invocation is preceded by `unbound`, e.g. + // {{unbound myHelper foo}} if (data.isUnbound) { return evaluateUnboundHelper(this, fn, normalizedProperties, options); } - if (dependentKeys.length === 0) { - return evaluateMultiPropertyBoundHelper(currentContext, fn, normalizedProperties, options); - } - - - normalized = normalizedProperties[0]; - - pathRoot = normalized.root; - path = normalized.path; - - var bindView = new Ember._SimpleHandlebarsView( - path, pathRoot, !options.hash.unescaped, options.data - ); + var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); + // Override SimpleHandlebarsView's method for generating the view's content. bindView.normalizedValue = function() { - var value = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - return fn.call(view, value, options); + var args = [], boundOption; + + // Copy over bound hash options. + for (boundOption in boundOptions) { + if (!boundOptions.hasOwnProperty(boundOption)) { continue; } + property = normalizePath(currentContext, boundOptions[boundOption], data); + bindView.path = property.path; + bindView.pathRoot = property.root; + hash[boundOption] = normalizedValue.call(bindView); + } + + for (loc = 0; loc < numProperties; ++loc) { + property = normalizedProperties[loc]; + if (property) { + bindView.path = property.path; + bindView.pathRoot = property.root; + args.push(normalizedValue.call(bindView)); + } else { + args.push(properties[loc]); + } + } + args.push(options); + + // Run the supplied helper function. + return fn.apply(currentContext, args); }; view.appendChild(bindView); - view.registerObserver(pathRoot, path, bindView, bindView.rerender); + // Assemble list of watched properties that'll re-render this helper. + for (boundOption in boundOptions) { + if (boundOptions.hasOwnProperty(boundOption)) { + watchedProperties.push(normalizePath(currentContext, boundOptions[boundOption], data)); + } + } + + // Observe each property. + for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { + property = watchedProperties[loc]; + view.registerObserver(property.root, property.path, bindView, bindView.rerender); + } + + if (types[0] !== 'ID' || normalizedProperties.length === 0) { + return; + } + + // Add dependent key observers to the first param + var normalized = normalizedProperties[0], + pathRoot = normalized.root, + path = normalized.path; if(!Ember.isEmpty(path)) { prefixPathForDependentKeys = path + '.'; @@ -20334,68 +24364,6 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { Ember.Handlebars.registerHelper(name, helper); }; -/** - @private - - Renders the unbound form of an otherwise bound helper function. - - @method evaluateMultiPropertyBoundHelper - @param {Function} fn - @param {Object} context - @param {Array} normalizedProperties - @param {String} options -*/ -function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, options) { - var numProperties = normalizedProperties.length, - data = options.data, - view = data.view, - hash = options.hash, - boundOptions = hash.boundOptions, - watchedProperties, - boundOption, bindView, loc, property, len; - - bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); - bindView.normalizedValue = function() { - var args = [], boundOption; - - // Copy over bound options. - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - property = normalizePath(context, boundOptions[boundOption], data); - bindView.path = property.path; - bindView.pathRoot = property.root; - hash[boundOption] = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - } - - for (loc = 0; loc < numProperties; ++loc) { - property = normalizedProperties[loc]; - bindView.path = property.path; - bindView.pathRoot = property.root; - args.push(Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView)); - } - args.push(options); - return fn.apply(context, args); - }; - - view.appendChild(bindView); - - // Assemble list of watched properties that'll re-render this helper. - watchedProperties = []; - for (boundOption in boundOptions) { - if (boundOptions.hasOwnProperty(boundOption)) { - watchedProperties.push(normalizePath(context, boundOptions[boundOption], data)); - } - } - watchedProperties = watchedProperties.concat(normalizedProperties); - - // Observe each property. - for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { - property = watchedProperties[loc]; - view.registerObserver(property.root, property.path, bindView, bindView.rerender); - } - -} - /** @private @@ -20445,19 +24413,19 @@ Ember.Handlebars.template = function(spec) { (function() { /** - * Mark a string as safe for unescaped output with Handlebars. If you - * return HTML from a Handlebars helper, use this function to - * ensure Handlebars does not escape the HTML. - * - * ```javascript - * Ember.String.htmlSafe('<div>someString</div>') - * ``` - * - * @method htmlSafe - * @for Ember.String - * @static - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as safe for unescaped output with Handlebars. If you + return HTML from a Handlebars helper, use this function to + ensure Handlebars does not escape the HTML. + + ```javascript + Ember.String.htmlSafe('<div>someString</div>') + ``` + + @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); }; @@ -20467,18 +24435,18 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - * Mark a string as being safe for unescaped output with Handlebars. - * - * ```javascript - * '<div>someString</div>'.htmlSafe() - * ``` - * - * See `Ember.String.htmlSafe`. - * - * @method htmlSafe - * @for String - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as being safe for unescaped output with Handlebars. + + ```javascript + '<div>someString</div>'.htmlSafe() + ``` + + See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_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); }; @@ -20554,19 +24522,21 @@ var DOMManager = { view.clearRenderedChildren(); var buffer = view.renderToBuffer(); - view.propertyWillChange('element'); + view.invokeRecursively(function(view) { + view.propertyWillChange('element'); + }); view.triggerRecursively('willInsertElement'); morph.replaceWith(buffer.string()); view.transitionTo('inDOM'); - view.propertyDidChange('element'); + view.invokeRecursively(function(view) { + view.propertyDidChange('element'); + }); view.triggerRecursively('didInsertElement'); notifyMutationListeners(); }); - - }, empty: function(view) { @@ -20592,7 +24562,7 @@ Ember._Metamorph = Ember.Mixin.create({ init: function() { this._super(); this.morph = Metamorph(); - + Ember.deprecate('Supplying a tagName to Metamorph views is unreliable and is deprecated. You may be setting the tagName on a Handlebars helper that creates a Metamorph.', !this.tagName); }, beforeRender: function(buffer) { @@ -20656,6 +24626,8 @@ function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) { this.morph = Metamorph(); this.state = 'preRender'; this.updateId = null; + this._parentView = null; + this.buffer = null; } Ember._SimpleHandlebarsView = SimpleHandlebarsView; @@ -20669,7 +24641,11 @@ SimpleHandlebarsView.prototype = { Ember.run.cancel(this.updateId); this.updateId = null; } + if (this._parentView) { + this._parentView.removeChild(this); + } this.morph = null; + this.state = 'destroyed'; }, propertyWillChange: Ember.K, @@ -20724,7 +24700,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroying': + case 'destroyed': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -21115,7 +25091,7 @@ function simpleBind(property, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { - + Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); if (helpers[property]) { return helpers[property].call(this, fn); } @@ -21149,7 +25125,7 @@ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { @return {String} HTML string */ EmberHandlebars.registerHelper('bind', function(property, options) { - + Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); var context = (options.contexts && options.contexts[0]) || this; @@ -21205,10 +25181,12 @@ EmberHandlebars.registerHelper('with', function(context, options) { if (arguments.length === 4) { var keywordName, path, rootPath, normalized; + Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); options = arguments[3]; keywordName = arguments[2]; path = arguments[0]; + Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); if (Ember.isGlobalPath(path)) { Ember.bind(options.data.keywords, keywordName, path); @@ -21229,15 +25207,15 @@ EmberHandlebars.registerHelper('with', function(context, options) { return bind.call(this, path, options, true, exists); } else { - - + Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); + Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); return helpers.bind.call(options.contexts[0], context, options); } }); /** - See `boundIf` + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) @method if @for Ember.Handlebars.helpers @@ -21246,8 +25224,8 @@ EmberHandlebars.registerHelper('with', function(context, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('if', function(context, options) { - - + Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); + Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); return helpers.boundIf.call(options.contexts[0], context, options); }); @@ -21260,8 +25238,8 @@ EmberHandlebars.registerHelper('if', function(context, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('unless', function(context, options) { - - + Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); + Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); var fn = options.fn, inverse = options.inverse; @@ -21272,11 +25250,11 @@ EmberHandlebars.registerHelper('unless', function(context, options) { }); /** - `bindAttr` allows you to create a binding between DOM element attributes and + `bind-attr` allows you to create a binding between DOM element attributes and Ember objects. For example: ```handlebars - <img {{bindAttr src="imageUrl" alt="imageTitle"}}> + <img {{bind-attr src="imageUrl" alt="imageTitle"}}> ``` The above handlebars template will fill the `<img>`'s `src` attribute will @@ -21298,17 +25276,17 @@ EmberHandlebars.registerHelper('unless', function(context, options) { <img src="http://lolcats.info/haz-a-funny" alt="A humorous image of a cat"> ``` - `bindAttr` cannot redeclare existing DOM element attributes. The use of `src` - in the following `bindAttr` example will be ignored and the hard coded value + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value of `src="/failwhale.gif"` will take precedence: ```handlebars - <img src="/failwhale.gif" {{bindAttr src="imageUrl" alt="imageTitle"}}> + <img src="/failwhale.gif" {{bind-attr src="imageUrl" alt="imageTitle"}}> ``` - ### `bindAttr` and the `class` attribute + ### `bind-attr` and the `class` attribute - `bindAttr` supports a special syntax for handling a number of cases unique + `bind-attr` supports a special syntax for handling a number of cases unique to the `class` DOM element attribute. The `class` attribute combines multiple discreet values into a single attribute as a space-delimited list of strings. Each string can be: @@ -21317,7 +25295,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { * a boolean return value of an object's property * a hard-coded value - A string return value works identically to other uses of `bindAttr`. The + A string return value works identically to other uses of `bind-attr`. The return value of the property will become the value of the attribute. For example, the following view and template: @@ -21330,7 +25308,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - <img {{bindAttr class="view.someProperty}}> + <img {{bind-attr class="view.someProperty}}> ``` Result in the following rendered output: @@ -21352,7 +25330,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - <img {{bindAttr class="view.someBool:class-name-if-true"}}> + <img {{bind-attr class="view.someBool:class-name-if-true"}}> ``` Result in the following rendered output: @@ -21366,14 +25344,14 @@ EmberHandlebars.registerHelper('unless', function(context, options) { value changes: ```handlebars - <img {{bindAttr class="view.someBool:class-name-if-true:class-name-if-false"}}> + <img {{bind-attr class="view.someBool:class-name-if-true:class-name-if-false"}}> ``` A hard-coded value can be used by prepending `:` to the desired class name: `:class-name-to-always-apply`. ```handlebars - <img {{bindAttr class=":class-name-to-always-apply"}}> + <img {{bind-attr class=":class-name-to-always-apply"}}> ``` Results in the following rendered output: @@ -21386,18 +25364,19 @@ EmberHandlebars.registerHelper('unless', function(context, options) { hard-coded value – can be combined in a single declaration: ```handlebars - <img {{bindAttr class=":class-name-to-always-apply view.someBool:class-name-if-true view.someProperty"}}> + <img {{bind-attr class=":class-name-to-always-apply view.someBool:class-name-if-true view.someProperty"}}> ``` - @method bindAttr + @method bind-attr @for Ember.Handlebars.helpers @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('bindAttr', function(options) { +EmberHandlebars.registerHelper('bind-attr', function(options) { var attrs = options.hash; + Ember.assert("You must specify at least one hash argument to bind-attr", !!Ember.keys(attrs).length); var view = options.data.view; var ret = []; @@ -21425,18 +25404,21 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { var path = attrs[attr], normalized; + 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); var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), type = Ember.typeOf(value); + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); var observer, invoker; observer = function observer() { var result = handlebarsGet(ctx, path, options); + Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); @@ -21456,7 +25438,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // When the observer fires, find the element using the // unique data id and update the attribute to the new value. // Note: don't add observer when path is 'this' or path - // is whole keyword e.g. {{#each x in list}} ... {{bindAttr attr="x"}} + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { view.registerObserver(normalized.root, normalized.path, observer); } @@ -21476,6 +25458,18 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { return new EmberHandlebars.SafeString(ret.join(' ')); }); +/** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); + /** @private @@ -21647,7 +25641,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ } if (hash.attributeBindings) { - + Ember.assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); extensions.attributeBindings = null; dup = true; } @@ -21727,16 +25721,18 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ // 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; } + Ember.assert(Ember.String.fmt('You must pass a view to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView) || Ember.View.detectInstance(newView)); var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); var currentView = data.view; @@ -21744,7 +25740,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ var newViewProto = newView.proto ? newView.proto() : newView; if (fn) { - + Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName')); viewOptions.template = fn; } @@ -21924,7 +25920,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ @return {String} HTML string */ EmberHandlebars.registerHelper('view', function(path, options) { - + Ember.assert("The view helper only takes a single argument", arguments.length <= 2); // If no path is provided, treat path param as options. if (path && path.data && path.data.isRenderData) { @@ -21953,8 +25949,8 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm /** `{{collection}}` is a `Ember.Handlebars` helper for adding instances of - `Ember.CollectionView` to a template. See `Ember.CollectionView` for - additional information on how a `CollectionView` functions. + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. `{{collection}}`'s primary use is as a block helper with a `contentBinding` option pointing towards an `Ember.Array`-compatible object. An `Ember.View` @@ -22074,15 +26070,15 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm @deprecated Use `{{each}}` helper instead. */ Ember.Handlebars.registerHelper('collection', function(path, options) { - + Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); // If no path is provided, treat path param as options. if (path && path.data && path.data.isRenderData) { options = path; path = undefined; - + Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); } else { - + Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); } var fn = options.fn; @@ -22094,7 +26090,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { // Otherwise, just default to the standard class. var collectionClass; collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView; - + Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); var hash = options.hash, itemHash = {}, match; @@ -22104,16 +26100,17 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { 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; delete hash.itemView; @@ -22225,7 +26222,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 @@ -22278,12 +26275,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); @@ -22303,8 +26300,8 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { }, _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) { @@ -22406,6 +26403,8 @@ GroupedEach.prototype = { }, addArrayObservers: function() { + if (!this.content) { return; } + this.content.addArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -22413,6 +26412,8 @@ GroupedEach.prototype = { }, removeArrayObservers: function() { + if (!this.content) { return; } + this.content.removeArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -22430,6 +26431,8 @@ GroupedEach.prototype = { }, render: function() { + if (!this.content) { return; } + var content = this.content, contentLength = get(content, 'length'), data = this.options.data, @@ -22442,12 +26445,21 @@ GroupedEach.prototype = { }, rerenderContainingView: function() { - Ember.run.scheduleOnce('render', this.containingView, 'rerender'); + var self = this; + Ember.run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); }, destroy: function() { this.removeContentObservers(); - this.removeArrayObservers(); + if (this.content) { + this.removeArrayObservers(); + } + this.destroyed = true; } }; @@ -22571,6 +26583,49 @@ GroupedEach.prototype = { Each itemController will receive a reference to the current controller as a `parentController` property. + ### (Experimental) Grouped Each + + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). + + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` + + This can be faster than the normal way that Handlebars re-renders items + in some cases. + + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). + + For example, + + ```handlebars + {{dealershipName}} + + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} + + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} + ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). + + Note that `group` behavior is also disabled by specifying an `itemViewClass`. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -22578,10 +26633,11 @@ GroupedEach.prototype = { @param [options] {Object} Handlebars key/value pairs of options @param [options.itemViewClass] {String} a path to a view class used for each item @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper */ Ember.Handlebars.registerHelper('each', function(path, options) { if (arguments.length === 4) { - + Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); var keywordName = arguments[0]; @@ -22663,7 +26719,7 @@ Ember.Handlebars.registerHelper('each', function(path, options) { */ Ember.Handlebars.registerHelper('template', function(name, options) { - + 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); }); @@ -22714,6 +26770,7 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { template = view.templateForName(underscoredName), deprecatedTemplate = !template && view.templateForName(name); + Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); template = template || deprecatedTemplate; @@ -22733,6 +26790,10 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var get = Ember.get, set = Ember.set; /** + `{{yield}}` denotes an area of a template that will be rendered inside + of another template. It has two main uses: + + ### Use with `layout` When used in a Handlebars template that is assigned to an `Ember.View` instance's `layout` property Ember will render the layout template first, inserting the view's own rendered output at the `{{yield}}` location. @@ -22775,7 +26836,34 @@ var get = Ember.get, set = Ember.set; bView.appendTo('body'); // throws - // Uncaught Error: assertion failed: You called yield in a template that was not a layout + // Uncaught Error: assertion failed: + // You called yield in a template that was not a layout + ``` + + ### Use with Ember.Component + When designing components `{{yield}}` is used to denote where, inside the component's + template, an optional block passed to the component should render: + + ```handlebars + <!-- application.hbs --> + {{#labeled-textfield value=someProperty}} + First name: + {{/my-component}} + ``` + + ```handlebars + <!-- components/my-component.hbs --> + <label> + {{yield}} {{input value=value}} + </label> + ``` + + Result: + + ```html + <label> + First name: <input type="text" /> + <label> ``` @method yield @@ -22784,25 +26872,19 @@ var get = Ember.get, set = Ember.set; @return {String} HTML string */ Ember.Handlebars.registerHelper('yield', function(options) { - var currentView = options.data.view, view = currentView, template; + var view = options.data.view; while (view && !get(view, 'layout')) { - view = get(view, 'parentView'); + if (view._contextView) { + view = view._contextView; + } else { + view = get(view, 'parentView'); + } } + Ember.assert("You called yield in a template that was not a layout", !!view); - template = get(view, 'template'); - - var keywords = view._parentView.cloneKeywords(); - - currentView.appendChild(Ember.View, { - isVirtual: true, - tagName: '', - template: template, - context: get(view._parentView, 'context'), - controller: get(view._parentView, 'controller'), - templateData: {keywords: keywords} - }); + view._yield(this, options); }); })(); @@ -22821,11 +26903,11 @@ Ember.Handlebars.registerHelper('yield', function(options) { ```html <script type="text/x-handlebars" data-template-name="home"> - {{loc welcome}} + {{loc "welcome"}} </script> ``` - Take note that `welcome` is a string and not an object + Take note that `"welcome"` is a string and not an object reference. @method loc @@ -22862,26 +26944,12 @@ Ember.Handlebars.registerHelper('loc', function(str) { var set = Ember.set, get = Ember.get; /** - The `Ember.Checkbox` view class renders a checkbox - [input](https://developer.mozilla.org/en/HTML/Element/Input) element. It - allows for binding an Ember property (`checked`) to the status of the - checkbox. + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `checkbox`. - Example: + See Handlebars.helpers.input for usage details. - ```handlebars - {{view Ember.Checkbox checkedBinding="receiveEmail"}} - ``` - - You can add a `label` tag yourself in the template where the `Ember.Checkbox` - is being used. - - ```handlebars - <label> - {{view Ember.Checkbox classNames="applicaton-specific-checkbox"}} - Some Title - </label> - ``` + ## Direct manipulation of `checked` The `checked` attribute of an `Ember.Checkbox` object should always be set through the Ember object or by interacting with its rendered element @@ -22892,8 +26960,8 @@ var set = Ember.set, get = Ember.get; ## Layout and LayoutName properties Because HTML `input` elements are self closing `layout` and `layoutName` - properties will not be applied. See `Ember.View`'s layout section for more - information. + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. @class Checkbox @namespace Ember @@ -22948,14 +27016,11 @@ var get = Ember.get, set = Ember.set; Ember.TextSupport = Ember.Mixin.create({ value: "", - attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex'], + attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly'], placeholder: null, disabled: false, maxlength: null, - insertNewline: Ember.K, - cancel: Ember.K, - init: function() { this._super(); this.on("focusOut", this, this._elementValueDidChange); @@ -22966,120 +27031,6 @@ Ember.TextSupport = Ember.Mixin.create({ this.on("keyUp", this, this.interpretKeyEvents); }, - interpretKeyEvents: function(event) { - var map = Ember.TextSupport.KEY_EVENTS; - var method = map[event.keyCode]; - - this._elementValueDidChange(); - if (method) { return this[method](event); } - }, - - _elementValueDidChange: function() { - set(this, 'value', this.$().val()); - } - -}); - -Ember.TextSupport.KEY_EVENTS = { - 13: 'insertNewline', - 27: 'cancel' -}; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-handlebars -*/ - -var get = Ember.get, set = Ember.set; - -/** - The `Ember.TextField` view class renders a text - [input](https://developer.mozilla.org/en/HTML/Element/Input) element. It - allows for binding Ember properties to the text field contents (`value`), - live-updating as the user inputs text. - - Example: - - ```handlebars - {{view Ember.TextField valueBinding="firstName"}} - ``` - - ## Layout and LayoutName properties - - Because HTML `input` elements are self closing `layout` and `layoutName` - properties will not be applied. See `Ember.View`'s layout section for more - information. - - ## HTML Attributes - - By default `Ember.TextField` provides support for `type`, `value`, `size`, - `pattern`, `placeholder`, `disabled`, `maxlength` and `tabindex` attributes - on a text field. If you need to support more attributes have a look at the - `attributeBindings` property in `Ember.View`'s HTML Attributes section. - - To globally add support for additional attributes you can reopen - `Ember.TextField` or `Ember.TextSupport`. - - ```javascript - Ember.TextSupport.reopen({ - attributeBindings: ["required"] - }) - ``` - - @class TextField - @namespace Ember - @extends Ember.View - @uses Ember.TextSupport -*/ -Ember.TextField = Ember.View.extend(Ember.TextSupport, - /** @scope Ember.TextField.prototype */ { - - classNames: ['ember-text-field'], - tagName: "input", - attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], - - /** - The `value` attribute of the input element. As the user inputs text, this - property is updated live. - - @property value - @type String - @default "" - */ - value: "", - - /** - The `type` attribute of the input element. - - @property type - @type String - @default "text" - */ - type: "text", - - /** - The `size` of the text field in characters. - - @property size - @type String - @default null - */ - size: null, - - /** - The `pattern` the pattern attribute of input element. - - @property pattern - @type String - @default null - */ - pattern: null, - /** The action to be sent when the user presses the return key. @@ -23124,31 +27075,109 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, */ bubbles: false, - insertNewline: function(event) { - sendAction('enter', this, event); + interpretKeyEvents: function(event) { + var map = Ember.TextSupport.KEY_EVENTS; + var method = map[event.keyCode]; + + this._elementValueDidChange(); + if (method) { return this[method](event); } }, + _elementValueDidChange: function() { + set(this, 'value', this.$().val()); + }, + + /** + The action to be sent when the user inserts a new line. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method insertNewLine + @param {Event} event + */ + insertNewline: function(event) { + sendAction('enter', this, event); + sendAction('insert-newline', this, event); + }, + + /** + Called when the user hits escape. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method cancel + @param {Event} event + */ + cancel: function(event) { + sendAction('escape-press', this, event); + }, + + /** + Called when the text area is focused. + + @method focusIn + @param {Event} event + */ + focusIn: function(event) { + sendAction('focus-in', this, event); + }, + + /** + Called when the text area is blurred. + + @method focusOut + @param {Event} event + */ + focusOut: function(event) { + sendAction('focus-out', this, event); + }, + + /** + The action to be sent when the user presses a key. Enabled by setting + the `onEvent` property to `keyPress`. + + Uses sendAction to send the `keyPress` action to the controller. + + @method keyPress + @param {Event} event + */ keyPress: function(event) { - sendAction('keyPress', this, event); + sendAction('key-press', this, event); } + }); +Ember.TextSupport.KEY_EVENTS = { + 13: 'insertNewline', + 27: 'cancel' +}; + +// In principle, this shouldn't be necessary, but the legacy +// sectionAction semantics for TextField are different from +// the component semantics so this method normalizes them. function sendAction(eventName, view, event) { - var action = get(view, 'action'), - on = get(view, 'onEvent'); + var action = get(view, eventName), + on = get(view, 'onEvent'), + value = get(view, 'value'); - if (action && on === eventName) { - var controller = get(view, 'controller'), - value = get(view, 'value'), - bubbles = get(view, 'bubbles'); + // back-compat support for keyPress as an event name even though + // it's also a method name that consumes the event (and therefore + // incompatible with sendAction semantics). + if (on === eventName || (on === 'keyPress' && eventName === 'key-press')) { + view.sendAction('action', value); + } - controller.send(action, value, view); + view.sendAction(eventName, value); - if (!bubbles) { + if (action || on === eventName) { + if(!get(view, 'bubbles')) { event.stopPropagation(); } } } + })(); @@ -23162,6 +27191,81 @@ function sendAction(eventName, view, event) { var get = Ember.get, set = Ember.set; /** + + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `text`. + + See [handlebars.helpers.input](api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextField + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport +*/ +Ember.TextField = Ember.Component.extend(Ember.TextSupport, + /** @scope Ember.TextField.prototype */ { + + classNames: ['ember-text-field'], + tagName: "input", + attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], + + /** + The `value` attribute of the input element. As the user inputs text, this + property is updated live. + + @property value + @type String + @default "" + */ + value: "", + + /** + The `type` attribute of the input element. + + @property type + @type String + @default "text" + */ + type: "text", + + /** + The `size` of the text field in characters. + + @property size + @type String + @default null + */ + size: null, + + /** + The `pattern` the pattern attribute of input element. + + @property pattern + @type String + @default null + */ + pattern: null +}); + +})(); + + + +(function() { +/* +@module ember +@submodule ember-handlebars +*/ + +var get = Ember.get, set = Ember.set; + +/* @class Button @namespace Ember @extends Ember.View @@ -23178,7 +27282,7 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { attributeBindings: ['type', 'disabled', 'href', 'tabindex'], - /** + /* @private Overrides `TargetActionSupport`'s `targetObject` computed @@ -23272,7 +27376,7 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { }, init: function() { - + Ember.deprecate("Ember.Button is deprecated and will be removed from future releases. Consider using the `{{action}}` helper."); this._super(); } }); @@ -23290,39 +27394,23 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { var get = Ember.get, set = Ember.set; /** - The `Ember.TextArea` view class renders a - [textarea](https://developer.mozilla.org/en/HTML/Element/textarea) element. - It allows for binding Ember properties to the text area contents (`value`), - live-updating as the user inputs text. + The internal class used to create textarea element when the `{{textarea}}` + helper is used. + + See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. ## Layout and LayoutName properties Because HTML `textarea` elements do not contain inner HTML the `layout` and - `layoutName` properties will not be applied. See `Ember.View`'s layout - section for more information. - - ## HTML Attributes - - By default `Ember.TextArea` provides support for `rows`, `cols`, - `placeholder`, `disabled`, `maxlength` and `tabindex` attributes on a - textarea. If you need to support more attributes have a look at the - `attributeBindings` property in `Ember.View`'s HTML Attributes section. - - To globally add support for additional attributes you can reopen - `Ember.TextArea` or `Ember.TextSupport`. - - ```javascript - Ember.TextSupport.reopen({ - attributeBindings: ["required"] - }) - ``` + `layoutName` properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. @class TextArea @namespace Ember - @extends Ember.View + @extends Ember.Component @uses Ember.TextSupport */ -Ember.TextArea = Ember.View.extend(Ember.TextSupport, { +Ember.TextArea = Ember.Component.extend(Ember.TextSupport, { classNames: ['ember-text-area'], tagName: "textarea", @@ -23438,7 +27526,7 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `content` property. The underlying data object of the selected `<option>` is stored in the `Element.Select`'s `value` property. - ### `content` as an array of Strings + ## The Content Property (array of strings) The simplest version of an `Ember.Select` takes an array of strings as its `content` property. The string will be used as both the `value` property and @@ -23447,11 +27535,13 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ Example: ```javascript - App.names = ["Yehuda", "Tom"]; + App.ApplicationController = Ember.Controller.extend({ + names: ["Yehuda", "Tom"] + }); ``` ```handlebars - {{view Ember.Select contentBinding="App.names"}} + {{view Ember.Select contentBinding="names"}} ``` Would result in the following HTML: @@ -23467,16 +27557,16 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `value` property directly or as a binding: ```javascript - App.names = Ember.Object.create({ - selected: 'Tom', - content: ["Yehuda", "Tom"] + App.ApplicationController = Ember.Controller.extend({ + selectedName: 'Tom', + names: ["Yehuda", "Tom"] }); ``` ```handlebars {{view Ember.Select - contentBinding="App.names.content" - valueBinding="App.names.selected" + contentBinding="names" + valueBinding="selectedName" }} ``` @@ -23490,9 +27580,9 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` A user interacting with the rendered `<select>` to choose "Yehuda" would - update the value of `App.names.selected` to "Yehuda". + update the value of `selectedName` to "Yehuda". - ### `content` as an Array of Objects + ## The Content Property (array of Objects) An `Ember.Select` can also take an array of JavaScript or Ember objects as its `content` property. @@ -23507,15 +27597,17 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ element's text. Both paths must reference each object itself as `content`: ```javascript - App.programmers = [ - Ember.Object.create({firstName: "Yehuda", id: 1}), - Ember.Object.create({firstName: "Tom", id: 2}) - ]; + App.ApplicationController = Ember.Controller.extend({ + programmers: [ + {firstName: "Yehuda", id: 1}, + {firstName: "Tom", id: 2} + ] + }); ``` ```handlebars {{view Ember.Select - contentBinding="App.programmers" + contentBinding="programmers" optionValuePath="content.id" optionLabelPath="content.firstName"}} ``` @@ -23534,22 +27626,23 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `valueBinding` option: ```javascript - App.programmers = [ - Ember.Object.create({firstName: "Yehuda", id: 1}), - Ember.Object.create({firstName: "Tom", id: 2}) - ]; - - App.currentProgrammer = Ember.Object.create({ - id: 2 + App.ApplicationController = Ember.Controller.extend({ + programmers: [ + {firstName: "Yehuda", id: 1}, + {firstName: "Tom", id: 2} + ], + currentProgrammer: { + id: 2 + } }); ``` ```handlebars {{view Ember.Select - contentBinding="App.programmers" + contentBinding="programmers" optionValuePath="content.id" optionLabelPath="content.firstName" - valueBinding="App.currentProgrammer.id"}} + valueBinding="currentProgrammer.id"}} ``` Would result in the following HTML with a selected option: @@ -23562,7 +27655,7 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` Interacting with the rendered element by selecting the first option - ('Yehuda') will update the `id` value of `App.currentProgrammer` + ('Yehuda') will update the `id` of `currentProgrammer` to match the `value` property of the newly selected `<option>`. Alternatively, you can control selection through the underlying objects @@ -23572,21 +27665,21 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ element: ```javascript - App.controller = Ember.Object.create({ + App.ApplicationController = Ember.Controller.extend({ selectedPerson: null, - content: [ - Ember.Object.create({firstName: "Yehuda", id: 1}), - Ember.Object.create({firstName: "Tom", id: 2}) + programmers: [ + {firstName: "Yehuda", id: 1}, + {firstName: "Tom", id: 2} ] }); ``` ```handlebars {{view Ember.Select - contentBinding="App.controller.content" + contentBinding="programmers" optionValuePath="content.id" optionLabelPath="content.firstName" - selectionBinding="App.controller.selectedPerson"}} + selectionBinding="selectedPerson"}} ``` Would result in the following HTML with a selected option: @@ -23599,19 +27692,19 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` Interacting with the rendered element by selecting the first option - ('Yehuda') will update the `selectedPerson` value of `App.controller` - to match the content object of the newly selected `<option>`. In this - case it is the first object in the `App.controller.content` + ('Yehuda') will update the `selectedPerson` to match the object of + the newly selected `<option>`. In this case it is the first object + in the `programmers` - ### Supplying a Prompt + ## Supplying a Prompt A `null` value for the `Ember.Select`'s `value` or `selection` property results in there being no `<option>` with a `selected` attribute: ```javascript - App.controller = Ember.Object.create({ - selected: null, - content: [ + App.ApplicationController = Ember.Controller.extend({ + selectedProgrammer: null, + programmers: [ "Yehuda", "Tom" ] @@ -23620,8 +27713,8 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` handlebars {{view Ember.Select - contentBinding="App.controller.content" - valueBinding="App.controller.selected" + contentBinding="programmers" + valueBinding="selectedProgrammer" }} ``` @@ -23634,16 +27727,16 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ </select> ``` - Although `App.controller.selected` is `null` and no `<option>` + Although `selectedProgrammer` is `null` and no `<option>` has a `selected` attribute the rendered HTML will display the first item as though it were selected. You can supply a string value for the `Ember.Select` to display when there is no selection with the `prompt` option: ```javascript - App.controller = Ember.Object.create({ - selected: null, - content: [ + App.ApplicationController = Ember.Controller.extend({ + selectedProgrammer: null, + programmers: [ "Yehuda", "Tom" ] @@ -23652,8 +27745,8 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ```handlebars {{view Ember.Select - contentBinding="App.controller.content" - valueBinding="App.controller.selected" + contentBinding="programmers" + valueBinding="selectedProgrammer" prompt="Please select a name" }} ``` @@ -23683,7 +27776,7 @@ 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("<option value=\"\">"); hashTypes = {}; @@ -23694,7 +27787,7 @@ function program1(depth0,data) { } function program3(depth0,data) { - + var stack1, hashTypes, hashContexts; hashTypes = {}; hashContexts = {}; @@ -23703,7 +27796,7 @@ function program3(depth0,data) { else { data.buffer.push(''); } } function program4(depth0,data) { - + var hashContexts, hashTypes; hashContexts = {'contentBinding': depth0,'labelBinding': depth0}; hashTypes = {'contentBinding': "ID",'labelBinding': "ID"}; @@ -23714,7 +27807,7 @@ function program4(depth0,data) { } function program6(depth0,data) { - + var stack1, hashTypes, hashContexts; hashTypes = {}; hashContexts = {}; @@ -23723,7 +27816,7 @@ function program6(depth0,data) { else { data.buffer.push(''); } } function program7(depth0,data) { - + var hashContexts, hashTypes; hashContexts = {'contentBinding': depth0}; hashTypes = {'contentBinding': "STRING"}; @@ -23741,7 +27834,7 @@ function program7(depth0,data) { stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } return buffer; - + }), attributeBindings: ['multiple', 'disabled', 'tabindex', 'name'], @@ -23755,6 +27848,14 @@ function program7(depth0,data) { */ multiple: false, + /** + The `disabled` attribute of the select element. Indicates whether + the element is disabled from interactions. + + @property multiple + @type Boolean + @default false + */ disabled: false, /** @@ -23821,7 +27922,7 @@ function program7(depth0,data) { prompt: null, /** - The path of the option labels. See `content`. + The path of the option labels. See [content](/api/classes/Ember.Select.html#property_content). @property optionLabelPath @type String @@ -23830,7 +27931,7 @@ function program7(depth0,data) { optionLabelPath: 'content', /** - The path of the option values. See `content`. + The path of the option values. See [content](/api/classes/Ember.Select.html#property_content). @property optionValuePath @type String @@ -24011,6 +28112,11 @@ function program7(depth0,data) { (function() { +/** +@module ember +@submodule ember-handlebars-compiler +*/ + function normalizeHash(hash, hashTypes) { for (var prop in hash) { if (hashTypes[prop] === 'ID') { @@ -24021,16 +28127,145 @@ function normalizeHash(hash, hashTypes) { } /** - * `{{input}}` inserts a new instance of either Ember.TextField or - * Ember.Checkbox, depending on the `type` option passed in. If no `type` - * is supplied it defaults to Ember.TextField. - * - * @method input - * @for Ember.Handlebars.helpers - * @param {Hash} options - */ -Ember.Handlebars.registerHelper('input', function(options) { + The `{{input}}` helper inserts an HTML `<input>` tag into the template, + with a `type` value of either `text` or `checkbox`. If no `type` is provided, + `text` will be the default value applied. The attributes of `{{input}}` + match those of the native HTML tag as closely as possible for these two types. + + ## Use as text field + An `{{input}}` with no `type` or a `type` of `text` will render an HTML text input. + The following HTML attributes can be set via the helper: + +* `value` +* `size` +* `name` +* `pattern` +* `placeholder` +* `disabled` +* `maxlength` +* `tabindex` + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input value="http://www.facebook.com"}} + ``` + + + ```html + <input type="text" value="http://www.facebook.com"/> + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + firstName: "Stanley", + entryNotAllowed: true + }); + ``` + + + ```handlebars + {{input type="text" value=firstName disabled=entryNotAllowed size="50"}} + ``` + + + ```html + <input type="text" value="Stanley" disabled="disabled" size="50"/> + ``` + + ## Extension + + Internally, `{{input type="text"}}` creates an instance of `Ember.TextField`, passing + arguments from the helper to `Ember.TextField`'s `create` method. You can extend the + capablilties of text inputs in your applications by reopening this class. For example, + if you are deploying to browsers where the `required` attribute is used, you + can add this to the `TextField`'s `attributeBindings` property: + + ```javascript + Ember.TextField.reopen({ + attributeBindings: ['required'] + }); + ``` + + Keep in mind when writing `Ember.TextField` subclasses that `Ember.TextField` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + + ## Use as checkbox + + An `{{input}}` with a `type` of `checkbox` will render an HTML checkbox input. + The following HTML attributes can be set via the helper: + +* `checked` +* `disabled` +* `tabindex` +* `indeterminate` +* `name` + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input type="checkbox" name="isAdmin"}} + ``` + + ```html + <input type="checkbox" name="isAdmin" /> + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + isAdmin: true + }); + ``` + + + ```handlebars + {{input type="checkbox" checked=isAdmin }} + ``` + + + ```html + <input type="checkbox" checked="checked" /> + ``` + + ## Extension + + Internally, `{{input type="checkbox"}}` creates an instance of `Ember.Checkbox`, passing + arguments from the helper to `Ember.Checkbox`'s `create` method. You can extend the + capablilties of checkbox inputs in your applications by reopening this class. For example, + if you wanted to add a css class to all checkboxes in your application: + + ```javascript + Ember.Checkbox.reopen({ + classNames: ['my-app-checkbox'] + }); + ``` + + + @method input + @for Ember.Handlebars.helpers + @param {Hash} options +*/ +Ember.Handlebars.registerHelper('input', function(options) { + Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2); var hash = options.hash, types = options.hashTypes, @@ -24052,15 +28287,154 @@ Ember.Handlebars.registerHelper('input', function(options) { }); /** - * `{{textarea}}` inserts a new instance of Ember.TextArea into the template - * passing its options to `Ember.TextArea`'s `create` method. - * - * @method textarea - * @for Ember.Handlebars.helpers - * @param {Hash} options - */ -Ember.Handlebars.registerHelper('textarea', function(options) { + `{{textarea}}` inserts a new instance of `<textarea>` tag into the template. + The attributes of `{{textarea}}` match those of the native HTML tags as + closely as possible. + The following HTML attributes can be set: + + * `value` + * `name` + * `rows` + * `cols` + * `placeholder` + * `disabled` + * `maxlength` + * `tabindex` + + When set to a quoted string, these value will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + Unbound: + + ```handlebars + {{textarea value="Lots of static text that ISN'T bound"}} + ``` + + Would result in the following HTML: + + ```html + <textarea class="ember-text-area"> + Lots of static text that ISN'T bound + </textarea> + ``` + + Bound: + + In the following example, the `writtenWords` property on `App.ApplicationController` + will be updated live as the user types 'Lots of text that IS bound' into + the text area of their browser's window. + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound" + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + ``` + + Would result in the following HTML: + + ```html + <textarea class="ember-text-area"> + Lots of text that IS bound + </textarea> + ``` + + If you wanted a one way binding between the text area and a div tag + somewhere else on your screen, you could use `Ember.computed.oneWay`: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + outputWrittenWords: Ember.computed.oneWay("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + + <div> + {{outputWrittenWords}} + </div> + ``` + + Would result in the following HTML: + + ```html + <textarea class="ember-text-area"> + Lots of text that IS bound + </textarea> + + <-- the following div will be updated in real time as you type --> + + <div> + Lots of text that IS bound + </div> + ``` + + Finally, this example really shows the power and ease of Ember when two + properties are bound to eachother via `Ember.computed.alias`. Type into + either text area box and they'll both stay in sync. Note that + `Ember.computed.alias` costs more in terms of performance, so only use it when + your really binding in both directions: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + twoWayWrittenWords: Ember.computed.alias("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + {{textarea value=twoWayWrittenWords}} + ``` + + ```html + <textarea id="ember1" class="ember-text-area"> + Lots of text that IS bound + </textarea> + + <-- both updated in real time --> + + <textarea id="ember2" class="ember-text-area"> + Lots of text that IS bound + </textarea> + ``` + + ## Extension + + Internally, `{{textarea}}` creates an instance of `Ember.TextArea`, passing + arguments from the helper to `Ember.TextArea`'s `create` method. You can + extend the capabilities of text areas in your application by reopening this + class. For example, if you are deploying to browsers where the `required` + attribute is used, you can globally add support for the `required` attribute + on all {{textarea}}'s' in your app by reopening `Ember.TextArea` or + `Ember.TextSupport` and adding it to the `attributeBindings` concatenated + property: + + ```javascript + Ember.TextArea.reopen({ + attributeBindings: ['required'] + }); + ``` + + Keep in mind when writing `Ember.TextArea` subclasses that `Ember.TextArea` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + @method textarea + @for Ember.Handlebars.helpers + @param {Hash} options +*/ +Ember.Handlebars.registerHelper('textarea', function(options) { + Ember.assert('You can only pass attributes to the `textarea` helper, not arguments', arguments.length < 2); var hash = options.hash, types = options.hashTypes; @@ -24114,6 +28488,11 @@ Ember.Handlebars.bootstrap = function(ctx) { templateName = script.attr('data-template-name') || script.attr('id') || 'application', template = compile(script.html()); + // Check if template of same name already exists + if (Ember.TEMPLATES[templateName] !== undefined) { + throw new Error('Template named "' + templateName + '" already exists.'); + } + // For templates which have a name, we save them and then remove them from the DOM Ember.TEMPLATES[templateName] = template; @@ -24137,18 +28516,22 @@ function registerComponents(container) { } } + function registerComponent(container, name) { + Ember.assert("You provided a template named 'components/" + name + "', but custom components must include a '-'", name.match(/-/)); + var fullName = 'component:' + name; - var className = name.replace(/-/g, '_'); - var Component = container.lookupFactory('component:' + className) || container.lookupFactory('component:' + name); - var View = Component || Ember.Component.extend(); + container.injection(fullName, 'layout', 'template:components/' + name); - View.reopen({ - layoutName: 'components/' + name - }); + var Component = container.lookupFactory(fullName); - Ember.Handlebars.helper(name, View); + if (!Component) { + container.register(fullName, Ember.Component); + Component = container.lookupFactory(fullName); + } + + Ember.Handlebars.helper(name, Component); } /* @@ -24163,21 +28546,16 @@ function registerComponent(container, name) { */ Ember.onLoad('Ember.Application', function(Application) { - if (Application.initializer) { - Application.initializer({ - name: 'domTemplates', - initialize: bootstrap - }); + Application.initializer({ + name: 'domTemplates', + initialize: bootstrap + }); - Application.initializer({ - name: 'registerComponents', - after: 'domTemplates', - initialize: registerComponents - }); - } else { - // for ember-old-router - Ember.onLoad('application', bootstrap); - } + Application.initializer({ + name: 'registerComponents', + after: 'domTemplates', + initialize: registerComponents + }); }); })(); @@ -24301,7 +28679,7 @@ define("route-recognizer", results.push(new StarSegment(match[1])); names.push(match[1]); types.stars++; - } else if (segment === "") { + } else if(segment === "") { results.push(new EpsilonSegment()); } else { results.push(new StaticSegment(segment)); @@ -24450,19 +28828,31 @@ define("route-recognizer", return nextStates; } - function findHandler(state, path) { + function findHandler(state, path, queryParams) { var handlers = state.handlers, regex = state.regex; var captures = path.match(regex), currentCapture = 1; var result = []; for (var i=0, l=handlers.length; i<l; i++) { - var handler = handlers[i], names = handler.names, params = {}; + var handler = handlers[i], names = handler.names, params = {}, + watchedQueryParams = handler.queryParams || [], + activeQueryParams = {}, + j, m; - for (var j=0, m=names.length; j<m; j++) { + for (j=0, m=names.length; j<m; j++) { params[names[j]] = captures[currentCapture++]; } - - result.push({ handler: handler.handler, params: params, isDynamic: !!names.length }); + for (j=0, m=watchedQueryParams.length; j < m; j++) { + var key = watchedQueryParams[j]; + if(queryParams[key]){ + activeQueryParams[key] = queryParams[key]; + } + } + var currentResult = { handler: handler.handler, params: params, isDynamic: !!names.length }; + if(watchedQueryParams && watchedQueryParams.length > 0) { + currentResult.queryParams = activeQueryParams; + } + result.push(currentResult); } return result; @@ -24517,7 +28907,11 @@ define("route-recognizer", regex += segment.regex(); } - handlers.push({ handler: route.handler, names: names }); + var handler = { handler: route.handler, names: names }; + if(route.queryParams) { + handler.queryParams = route.queryParams; + } + handlers.push(handler); } if (isEmpty) { @@ -24569,12 +28963,61 @@ define("route-recognizer", if (output.charAt(0) !== '/') { output = '/' + output; } + if (params && params.queryParams) { + output += this.generateQueryString(params.queryParams, route.handlers); + } + return output; }, + generateQueryString: function(params, handlers) { + var pairs = [], allowedParams = []; + for(var i=0; i < handlers.length; i++) { + var currentParamList = handlers[i].queryParams; + if(currentParamList) { + allowedParams.push.apply(allowedParams, currentParamList); + } + } + for(var key in params) { + if (params.hasOwnProperty(key)) { + if(!~allowedParams.indexOf(key)) { + throw 'Query param "' + key + '" is not specified as a valid param for this route'; + } + var value = params[key]; + var pair = encodeURIComponent(key); + if(value !== true) { + pair += "=" + encodeURIComponent(value); + } + pairs.push(pair); + } + } + + if (pairs.length === 0) { return ''; } + + return "?" + pairs.join("&"); + }, + + parseQueryString: function(queryString) { + var pairs = queryString.split("&"), queryParams = {}; + for(var i=0; i < pairs.length; i++) { + var pair = pairs[i].split('='), + key = decodeURIComponent(pair[0]), + value = pair[1] ? decodeURIComponent(pair[1]) : true; + queryParams[key] = value; + } + return queryParams; + }, + recognize: function(path) { var states = [ this.rootState ], - pathLen, i, l; + pathLen, i, l, queryStart, queryParams = {}; + + queryStart = path.indexOf('?'); + if (~queryStart) { + var queryString = path.substr(queryStart + 1, path.length); + path = path.substr(0, queryStart); + queryParams = this.parseQueryString(queryString); + } // DEBUG GROUP path @@ -24602,7 +29045,7 @@ define("route-recognizer", var state = solutions[0]; if (state && state.handlers) { - return findHandler(state, path); + return findHandler(state, path, queryParams); } } }; @@ -24627,12 +29070,25 @@ define("route-recognizer", if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); } this.matcher.addChild(this.path, target, callback, this.delegate); } + return this; + }, + + withQueryParams: function() { + if (arguments.length === 0) { throw new Error("you must provide arguments to the withQueryParams method"); } + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] !== "string") { + throw new Error('you should call withQueryParams with a list of strings, e.g. withQueryParams("foo", "bar")'); + } + } + var queryParams = [].slice.call(arguments); + this.matcher.addQueryParams(this.path, queryParams); } }; function Matcher(target) { this.routes = {}; this.children = {}; + this.queryParams = {}; this.target = target; } @@ -24641,6 +29097,10 @@ define("route-recognizer", this.routes[path] = handler; }, + addQueryParams: function(path, params) { + this.queryParams[path] = params; + }, + addChild: function(path, target, callback, delegate) { var matcher = new Matcher(target); this.children[path] = matcher; @@ -24667,23 +29127,26 @@ define("route-recognizer", }; } - function addRoute(routeArray, path, handler) { + function addRoute(routeArray, path, handler, queryParams) { var len = 0; for (var i=0, l=routeArray.length; i<l; i++) { len += routeArray[i].path.length; } path = path.substr(len); - routeArray.push({ path: path, handler: handler }); + var route = { path: path, handler: handler }; + if(queryParams) { route.queryParams = queryParams; } + routeArray.push(route); } function eachRoute(baseRoute, matcher, callback, binding) { var routes = matcher.routes; + var queryParams = matcher.queryParams; for (var path in routes) { if (routes.hasOwnProperty(path)) { var routeArray = baseRoute.slice(); - addRoute(routeArray, path, routes[path]); + addRoute(routeArray, path, routes[path], queryParams[path]); if (matcher.children[path]) { eachRoute(routeArray, matcher.children[path], callback, binding); @@ -24822,9 +29285,9 @@ define("router", */ retry: function() { this.abort(); - var recogHandlers = this.router.recognizer.handlersFor(this.targetName), - newTransition = performTransition(this.router, recogHandlers, this.providedModelsArray, this.params, this.data); + handlerInfos = generateHandlerInfosWithQueryParams(this.router, recogHandlers, this.queryParams), + newTransition = performTransition(this.router, handlerInfos, this.providedModelsArray, this.params, this.queryParams, this.data); return newTransition; }, @@ -24849,6 +29312,10 @@ define("router", method: function(method) { this.urlMethod = method; return this; + }, + + toString: function() { + return "Transition (sequence " + this.sequence + ")"; } }; @@ -24993,8 +29460,21 @@ define("router", @param {Array[Object]} contexts @return {Object} a serialized parameter hash */ - paramsForHandler: function(handlerName, callback) { - return paramsForHandler(this, handlerName, slice.call(arguments, 1)); + + paramsForHandler: function(handlerName, contexts) { + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)); + return paramsForHandler(this, handlerName, partitionedArgs[0], partitionedArgs[1]); + }, + + /** + This method takes a handler name and returns a list of query params + that are valid to pass to the handler or its parents + + @param {String} handlerName + @return {Array[String]} a list of query parameters + */ + queryParamsForHandler: function (handlerName) { + return queryParamsForHandler(this, handlerName); }, /** @@ -25008,12 +29488,41 @@ define("router", @return {String} a URL */ generate: function(handlerName) { - var params = paramsForHandler(this, handlerName, slice.call(arguments, 1)); + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), + suppliedParams = partitionedArgs[0], + queryParams = partitionedArgs[1]; + + var params = paramsForHandler(this, handlerName, suppliedParams, queryParams), + validQueryParams = queryParamsForHandler(this, handlerName); + + var missingParams = []; + + for (var key in queryParams) { + if (queryParams.hasOwnProperty(key) && !~validQueryParams.indexOf(key)) { + missingParams.push(key); + } + } + + if (missingParams.length > 0) { + var err = 'You supplied the params '; + err += missingParams.map(function(param) { + return '"' + param + "=" + queryParams[param] + '"'; + }).join(' and '); + + err += ' which are not valid for the "' + handlerName + '" handler or its parents'; + + throw new Error(err); + } + return this.recognizer.generate(handlerName, params); }, isActive: function(handlerName) { - var contexts = slice.call(arguments, 1); + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), + contexts = partitionedArgs[0], + queryParams = partitionedArgs[1], + activeQueryParams = {}, + effectiveQueryParams = {}; var targetHandlerInfos = this.targetHandlerInfos, found = false, names, object, handlerInfo, handlerObj; @@ -25021,20 +29530,25 @@ define("router", if (!targetHandlerInfos) { return false; } var recogHandlers = this.recognizer.handlersFor(targetHandlerInfos[targetHandlerInfos.length - 1].name); - for (var i=targetHandlerInfos.length-1; i>=0; i--) { handlerInfo = targetHandlerInfos[i]; if (handlerInfo.name === handlerName) { found = true; } if (found) { - if (contexts.length === 0) { break; } + var recogHandler = recogHandlers[i]; - if (handlerInfo.isDynamic) { + merge(activeQueryParams, handlerInfo.queryParams); + if (queryParams !== false) { + merge(effectiveQueryParams, handlerInfo.queryParams); + mergeSomeKeys(effectiveQueryParams, queryParams, recogHandler.queryParams); + } + + if (handlerInfo.isDynamic && contexts.length > 0) { object = contexts.pop(); if (isParam(object)) { - var recogHandler = recogHandlers[i], name = recogHandler.names[0]; - if (object.toString() !== this.currentParams[name]) { return false; } + var name = recogHandler.names[0]; + if ("" + object !== this.currentParams[name]) { return false; } } else if (handlerInfo.context !== object) { return false; } @@ -25042,12 +29556,13 @@ define("router", } } - return contexts.length === 0 && found; + + return contexts.length === 0 && found && queryParamsEqual(activeQueryParams, effectiveQueryParams); }, trigger: function(name) { var args = slice.call(arguments); - trigger(this.currentHandlerInfos, false, args); + trigger(this, this.currentHandlerInfos, false, args); }, /** @@ -25065,7 +29580,7 @@ define("router", a shared pivot parent route and other data necessary to perform a transition. */ - function getMatchPoint(router, handlers, objects, inputParams) { + function getMatchPoint(router, handlers, objects, inputParams, queryParams) { var matchPoint = handlers.length, providedModels = {}, i, @@ -25120,6 +29635,12 @@ define("router", } } + // If there is an old handler, see if query params are the same. If there isn't an old handler, + // hasChanged will already be true here + if(oldHandlerInfo && !queryParamsEqual(oldHandlerInfo.queryParams, handlerObj.queryParams)) { + hasChanged = true; + } + if (hasChanged) { matchPoint = i; } } @@ -25144,8 +29665,8 @@ define("router", } } else if (activeTransition) { // Use model from previous transition attempt, preferably the resolved one. - return (paramName && activeTransition.providedModels[handlerName]) || - activeTransition.resolvedModels[handlerName]; + return activeTransition.resolvedModels[handlerName] || + (paramName && activeTransition.providedModels[handlerName]); } } @@ -25153,6 +29674,28 @@ define("router", return (typeof object === "string" || object instanceof String || !isNaN(object)); } + + + /** + @private + + This method takes a handler name and returns a list of query params + that are valid to pass to the handler or its parents + + @param {Router} router + @param {String} handlerName + @return {Array[String]} a list of query parameters + */ + function queryParamsForHandler(router, handlerName) { + var handlers = router.recognizer.handlersFor(handlerName), + queryParams = []; + + for (var i = 0; i < handlers.length; i++) { + queryParams.push.apply(queryParams, handlers[i].queryParams || []); + } + + return queryParams; + } /** @private @@ -25164,13 +29707,17 @@ define("router", @param {Array[Object]} objects @return {Object} a serialized parameter hash */ - function paramsForHandler(router, handlerName, objects) { + function paramsForHandler(router, handlerName, objects, queryParams) { var handlers = router.recognizer.handlersFor(handlerName), params = {}, - matchPoint = getMatchPoint(router, handlers, objects).matchPoint, + handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams), + matchPoint = getMatchPoint(router, handlerInfos, objects).matchPoint, + mergedQueryParams = {}, object, handlerObj, handler, names, i; + params.queryParams = {}; + for (i=0; i<handlers.length; i++) { handlerObj = handlers[i]; handler = router.getHandler(handlerObj.handler); @@ -25189,7 +29736,13 @@ define("router", // Serialize to generate params merge(params, serialize(handler, object, names)); } + if (queryParams !== false) { + mergeSomeKeys(params.queryParams, router.currentQueryParams, handlerObj.queryParams); + mergeSomeKeys(params.queryParams, queryParams, handlerObj.queryParams); + } } + + if (queryParamsEqual(params.queryParams, {})) { delete params.queryParams; } return params; } @@ -25199,24 +29752,84 @@ define("router", } } + function mergeSomeKeys(hash, other, keys) { + if (!other || !keys) { return; } + for(var i = 0; i < keys.length; i++) { + var key = keys[i], value; + if(other.hasOwnProperty(key)) { + value = other[key]; + if(value === null || value === false || typeof value === "undefined") { + delete hash[key]; + } else { + hash[key] = other[key]; + } + } + } + } + + /** + @private + */ + + function generateHandlerInfosWithQueryParams(router, handlers, queryParams) { + var handlerInfos = []; + + for (var i = 0; i < handlers.length; i++) { + var handler = handlers[i], + handlerInfo = { handler: handler.handler, names: handler.names, context: handler.context, isDynamic: handler.isDynamic }, + activeQueryParams = {}; + + if (queryParams !== false) { + mergeSomeKeys(activeQueryParams, router.currentQueryParams, handler.queryParams); + mergeSomeKeys(activeQueryParams, queryParams, handler.queryParams); + } + + if (handler.queryParams && handler.queryParams.length > 0) { + handlerInfo.queryParams = activeQueryParams; + } + + handlerInfos.push(handlerInfo); + } + + return handlerInfos; + } + + /** + @private + */ + function createQueryParamTransition(router, queryParams) { + var currentHandlers = router.currentHandlerInfos, + currentHandler = currentHandlers[currentHandlers.length - 1], + name = currentHandler.name; + + log(router, "Attempting query param transition"); + + return createNamedTransition(router, [name, queryParams]); + } + /** @private */ function createNamedTransition(router, args) { - var handlers = router.recognizer.handlersFor(args[0]); + var partitionedArgs = extractQueryParams(args), + pureArgs = partitionedArgs[0], + queryParams = partitionedArgs[1], + handlers = router.recognizer.handlersFor(pureArgs[0]), + handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams); - log(router, "Attempting transition to " + args[0]); - return performTransition(router, handlers, slice.call(args, 1), router.currentParams); + log(router, "Attempting transition to " + pureArgs[0]); + + return performTransition(router, handlerInfos, slice.call(pureArgs, 1), router.currentParams, queryParams); } /** @private */ function createURLTransition(router, url) { - var results = router.recognizer.recognize(url), - currentHandlerInfos = router.currentHandlerInfos; + currentHandlerInfos = router.currentHandlerInfos, + queryParams = {}; log(router, "Attempting URL transition to " + url); @@ -25224,7 +29837,11 @@ define("router", return errorTransition(router, new Router.UnrecognizedURLError(url)); } - return performTransition(router, results, [], {}); + for(var i = 0; i < results.length; i++) { + merge(queryParams, results[i].queryParams); + } + + return performTransition(router, results, [], {}, queryParams); } @@ -25291,10 +29908,6 @@ define("router", eachHandler(partition.entered, function(handlerInfo) { handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); }); - - if (router.didTransition) { - router.didTransition(handlerInfos); - } } /** @@ -25312,13 +29925,14 @@ define("router", checkAbort(transition); setContext(handler, context); + setQueryParams(handler, handlerInfo.queryParams); - if (handler.setup) { handler.setup(context); } + if (handler.setup) { handler.setup(context, handlerInfo.queryParams); } checkAbort(transition); } catch(e) { if (!(e instanceof Router.TransitionAborted)) { // Trigger the `error` event starting from this failed handler. - trigger(currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]); + trigger(transition.router, currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]); } // Propagate the error so that the transition promise will reject. @@ -25344,6 +29958,29 @@ define("router", } } + /** + @private + + determines if two queryparam objects are the same or not + **/ + function queryParamsEqual(a, b) { + a = a || {}; + b = b || {}; + var checkedKeys = [], key; + for(key in a) { + if (!a.hasOwnProperty(key)) { continue; } + if(b[key] !== a[key]) { return false; } + checkedKeys.push(key); + } + for(key in b) { + if (!b.hasOwnProperty(key)) { continue; } + if (~checkedKeys.indexOf(key)) { continue; } + // b has a key not in a + return false; + } + return true; + } + /** @private @@ -25393,19 +30030,21 @@ define("router", unchanged: [] }; - var handlerChanged, contextChanged, i, l; + var handlerChanged, contextChanged, queryParamsChanged, i, l; for (i=0, l=newHandlers.length; i<l; i++) { var oldHandler = oldHandlers[i], newHandler = newHandlers[i]; if (!oldHandler || oldHandler.handler !== newHandler.handler) { handlerChanged = true; + } else if (!queryParamsEqual(oldHandler.queryParams, newHandler.queryParams)) { + queryParamsChanged = true; } if (handlerChanged) { handlers.entered.push(newHandler); if (oldHandler) { handlers.exited.unshift(oldHandler); } - } else if (contextChanged || oldHandler.context !== newHandler.context) { + } else if (contextChanged || oldHandler.context !== newHandler.context || queryParamsChanged) { contextChanged = true; handlers.updatedContext.push(newHandler); } else { @@ -25420,7 +30059,11 @@ define("router", return handlers; } - function trigger(handlerInfos, ignoreFailure, args) { + function trigger(router, handlerInfos, ignoreFailure, args) { + if (router.triggerEvent) { + router.triggerEvent(handlerInfos, ignoreFailure, args); + return; + } var name = args.shift(); @@ -25454,20 +30097,45 @@ define("router", if (handler.contextDidChange) { handler.contextDidChange(); } } + function setQueryParams(handler, queryParams) { + handler.queryParams = queryParams; + if (handler.queryParamsDidChange) { handler.queryParamsDidChange(); } + } + + + /** + @private + + Extracts query params from the end of an array + **/ + + function extractQueryParams(array) { + var len = (array && array.length), head, queryParams; + + if(len && len > 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { + queryParams = array[len - 1].queryParams; + head = slice.call(array, 0, len - 1); + return [head, queryParams]; + } else { + return [array, null]; + } + } + /** @private Creates, begins, and returns a Transition. */ - function performTransition(router, recogHandlers, providedModelsArray, params, data) { + function performTransition(router, recogHandlers, providedModelsArray, params, queryParams, data) { - var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params), + var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params, queryParams), targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false; + wasTransitioning = false, + currentHandlerInfos = router.currentHandlerInfos; // Check if there's already a transition underway. if (router.activeTransition) { - if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray)) { + if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray, queryParams)) { return router.activeTransition; } router.activeTransition.abort(); @@ -25482,6 +30150,7 @@ define("router", transition.providedModelsArray = providedModelsArray; transition.params = matchPointResults.params; transition.data = data || {}; + transition.queryParams = queryParams; router.activeTransition = transition; var handlerInfos = generateHandlerInfos(router, recogHandlers); @@ -25489,7 +30158,7 @@ define("router", // Fire 'willTransition' event on current handlers, but don't fire it // if a transition was already underway. if (!wasTransitioning) { - trigger(router.currentHandlerInfos, true, ['willTransition', transition]); + trigger(router, currentHandlerInfos, true, ['willTransition', transition]); } log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); @@ -25502,7 +30171,20 @@ define("router", checkAbort(transition); try { - finalizeTransition(transition, handlerInfos); + log(router, transition.sequence, "Validation succeeded, finalizing transition;"); + + // Don't overwrite contexts / update URL if this was a noop transition. + if (!currentHandlerInfos || !currentHandlerInfos.length || + !router.recognizer.hasRoute(currentHandlerInfos[currentHandlerInfos.length - 1].name) || + currentHandlerInfos.length !== matchPointResults.matchPoint) { + finalizeTransition(transition, handlerInfos); + } + + if (router.didTransition) { + router.didTransition(handlerInfos); + } + + log(router, transition.sequence, "TRANSITION COMPLETE."); // Resolve with the final handler. deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); @@ -25535,11 +30217,16 @@ define("router", var handlerObj = recogHandlers[i], isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length); - handlerInfos.push({ + + var handlerInfo = { isDynamic: !!isDynamic, name: handlerObj.handler, handler: router.getHandler(handlerObj.handler) - }); + }; + if(handlerObj.queryParams) { + handlerInfo.queryParams = handlerObj.queryParams; + } + handlerInfos.push(handlerInfo); } return handlerInfos; } @@ -25547,7 +30234,7 @@ define("router", /** @private */ - function transitionsIdentical(oldTransition, targetName, providedModelsArray) { + function transitionsIdentical(oldTransition, targetName, providedModelsArray, queryParams) { if (oldTransition.targetName !== targetName) { return false; } @@ -25557,6 +30244,11 @@ define("router", for (var i = 0, len = oldModels.length; i < len; ++i) { if (oldModels[i] !== providedModelsArray[i]) { return false; } } + + if(!queryParamsEqual(oldTransition.queryParams, queryParams)) { + return false; + } + return true; } @@ -25570,13 +30262,12 @@ define("router", var router = transition.router, seq = transition.sequence, - handlerName = handlerInfos[handlerInfos.length - 1].name; - - log(router, seq, "Validation succeeded, finalizing transition;"); + handlerName = handlerInfos[handlerInfos.length - 1].name, + i; // Collect params for URL. var objects = [], providedModels = transition.providedModelsArray.slice(); - for (var i = handlerInfos.length - 1; i>=0; --i) { + for (i = handlerInfos.length - 1; i>=0; --i) { var handlerInfo = handlerInfos[i]; if (handlerInfo.isDynamic) { var providedModel = providedModels.pop(); @@ -25584,12 +30275,18 @@ define("router", } } - var params = paramsForHandler(router, handlerName, objects); + var newQueryParams = {}; + for (i = handlerInfos.length - 1; i>=0; --i) { + merge(newQueryParams, handlerInfos[i].queryParams); + } + router.currentQueryParams = newQueryParams; + + + var params = paramsForHandler(router, handlerName, objects, transition.queryParams); - transition.providedModelsArray = []; - transition.providedContexts = {}; router.currentParams = params; + var urlMethod = transition.urlMethod; if (urlMethod) { var url = router.recognizer.generate(handlerName, params); @@ -25603,7 +30300,6 @@ define("router", } setupContexts(transition, handlerInfos); - log(router, seq, "TRANSITION COMPLETE."); } /** @@ -25632,7 +30328,9 @@ define("router", // We're before the match point, so don't run any hooks, // just use the already resolved context from the handler. - transition.resolvedModels[handlerInfo.name] = handlerInfo.handler.context; + transition.resolvedModels[handlerInfo.name] = + transition.providedModels[handlerInfo.name] || + handlerInfo.handler.context; return proceed(); } @@ -25643,8 +30341,8 @@ define("router", .then(handleAbort) .then(afterModel) .then(handleAbort) - .then(proceed) - .then(null, handleError); + .then(null, handleError) + .then(proceed); function handleAbort(result) { if (transition.isAborted) { @@ -25669,11 +30367,7 @@ define("router", // An error was thrown / promise rejected, so fire an // `error` event from this handler info up to root. - trigger(handlerInfos.slice(0, index + 1), true, ['error', reason, transition]); - - if (handler.error) { - handler.error(reason, transition); - } + trigger(router, handlerInfos.slice(0, index + 1), true, ['error', reason, transition]); // Propagate the original error. return RSVP.reject(reason); @@ -25683,13 +30377,20 @@ define("router", log(router, seq, handlerName + ": calling beforeModel hook"); - var p = handler.beforeModel && handler.beforeModel(transition); + var args; + + if (handlerInfo.queryParams) { + args = [handlerInfo.queryParams, transition]; + } else { + args = [transition]; + } + + var p = handler.beforeModel && handler.beforeModel.apply(handler, args); return (p instanceof Transition) ? null : p; } function model() { log(router, seq, handlerName + ": resolving model"); - var p = getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); return (p instanceof Transition) ? null : p; } @@ -25704,7 +30405,15 @@ define("router", transition.resolvedModels[handlerInfo.name] = context; - var p = handler.afterModel && handler.afterModel(context, transition); + var args; + + if (handlerInfo.queryParams) { + args = [context, handlerInfo.queryParams, transition]; + } else { + args = [context, transition]; + } + + var p = handler.afterModel && handler.afterModel.apply(handler, args); return (p instanceof Transition) ? null : p; } @@ -25735,9 +30444,8 @@ define("router", or use one of the models provided to `transitionTo`. */ function getModel(handlerInfo, transition, handlerParams, needsUpdate) { - var handler = handlerInfo.handler, - handlerName = handlerInfo.name; + handlerName = handlerInfo.name, args; if (!needsUpdate && handler.hasOwnProperty('context')) { return handler.context; @@ -25748,7 +30456,13 @@ define("router", return typeof providedModel === 'function' ? providedModel() : providedModel; } - return handler.model && handler.model(handlerParams || {}, transition); + if (handlerInfo.queryParams) { + args = [handlerParams || {}, handlerInfo.queryParams, transition]; + } else { + args = [handlerParams || {}, transition, handlerInfo.queryParams]; + } + + return handler.model && handler.model.apply(handler, args); } /** @@ -25781,10 +30495,12 @@ define("router", // Normalize blank transitions to root URL transitions. var name = args[0] || '/'; - if (name.charAt(0) === '/') { + if(args.length === 1 && args[0].hasOwnProperty('queryParams')) { + return createQueryParamTransition(router, args[0]); + } else if (name.charAt(0) === '/') { return createURLTransition(router, name); } else { - return createNamedTransition(router, args); + return createNamedTransition(router, slice.call(args)); } } @@ -25862,21 +30578,21 @@ DSL.prototype = { if (callback) { var dsl = new DSL(name); callback.call(dsl); - this.push(options.path, name, dsl.generate()); + this.push(options.path, name, dsl.generate(), options.queryParams); } else { - this.push(options.path, name); + this.push(options.path, name, null, options.queryParams); } }, - push: function(url, name, callback) { + push: function(url, name, callback, queryParams) { var parts = name.split('.'); if (url === "" || url === "/" || parts[parts.length-1] === "index") { this.explicitIndex = true; } - this.matches.push([url, name, callback]); + this.matches.push([url, name, callback, queryParams]); }, route: function(name, options) { - + Ember.assert("You must use `this.resource` to nest", typeof options !== 'function'); options = options || {}; @@ -25888,7 +30604,7 @@ DSL.prototype = { name = this.parent + "." + name; } - this.push(options.path, name); + this.push(options.path, name, null, options.queryParams); }, generate: function() { @@ -25901,7 +30617,12 @@ DSL.prototype = { return function(match) { for (var i=0, l=dslMatches.length; i<l; i++) { var dslMatch = dslMatches[i]; - match(dslMatch[0]).to(dslMatch[1], dslMatch[2]); + var matchObj = match(dslMatch[0]).to(dslMatch[1], dslMatch[2]); + if (Ember.FEATURES.isEnabled("query-params")) { + if(dslMatch[3]) { + matchObj.withQueryParams.apply(matchObj, dslMatch[3]); + } + } } }; } @@ -25927,43 +30648,51 @@ var get = Ember.get; @submodule ember-routing */ +/** + + Finds a controller instance. + + @for Ember + @method controllerFor + @private +*/ Ember.controllerFor = function(container, controllerName, lookupOptions) { return container.lookup('controller:' + controllerName, lookupOptions); }; -/* + +/** Generates a controller automatically if none was provided. The type of generated controller depends on the context. You can customize your generated controllers by defining `App.ObjectController` and `App.ArrayController` + + @for Ember + @method generateController + @private */ Ember.generateController = function(container, controllerName, context) { - var controller, DefaultController, fullName, instance; + var ControllerFactory, fullName, instance, name, factoryName, controllerType; if (context && Ember.isArray(context)) { - DefaultController = container.resolve('controller:array'); - controller = DefaultController.extend({ - isGenerated: true - }); + controllerType = 'array'; } else if (context) { - DefaultController = container.resolve('controller:object'); - controller = DefaultController.extend({ - isGenerated: true - }); + controllerType = 'object'; } else { - DefaultController = container.resolve('controller:basic'); - controller = DefaultController.extend({ - isGenerated: true - }); + controllerType = 'basic'; } - controller.toString = function() { - return "(generated " + controllerName + " controller)"; - }; + factoryName = 'controller:' + controllerType; - controller.isGenerated = true; + ControllerFactory = container.lookupFactory(factoryName).extend({ + isGenerated: true, + toString: function() { + return "(generated " + controllerName + " controller)"; + } + }); fullName = 'controller:' + controllerName; - container.register(fullName, controller); + + container.register(fullName, ControllerFactory); instance = container.lookup(fullName); @@ -25989,22 +30718,6 @@ var get = Ember.get, set = Ember.set; var defineProperty = Ember.defineProperty; var DefaultView = Ember._MetamorphView; -function setupLocation(router) { - var location = get(router, 'location'), - rootURL = get(router, 'rootURL'), - options = {}; - - if (typeof rootURL === 'string') { - options.rootURL = rootURL; - } - - if ('string' === typeof location) { - options.implementation = location; - location = set(router, 'location', Ember.Location.create(options)); - - } -} - /** The `Ember.Router` class manages the application state and URLs. Refer to the [routing guide](http://emberjs.com/guides/routing/) for documentation. @@ -26019,7 +30732,7 @@ Ember.Router = Ember.Object.extend({ init: function() { this.router = this.constructor.router || this.constructor.map(Ember.K); this._activeViews = {}; - setupLocation(this); + this._setupLocation(); }, url: Ember.computed(function() { @@ -26034,7 +30747,7 @@ Ember.Router = Ember.Object.extend({ container = this.container, self = this; - setupRouter(this, router, location); + this._setupRouter(router, location); container.register('view:default', DefaultView); container.register('view:toplevel', Ember.View.extend()); @@ -26048,12 +30761,14 @@ Ember.Router = Ember.Object.extend({ didTransition: function(infos) { var appController = this.container.lookup('controller:application'), - path = routePath(infos); + path = Ember.Router._routePath(infos); - if (!('currentPath' in appController)) { - defineProperty(appController, 'currentPath'); - } + if (!('currentPath' in appController)) { defineProperty(appController, 'currentPath'); } set(appController, 'currentPath', path); + + if (!('currentRouteName' in appController)) { defineProperty(appController, 'currentRouteName'); } + set(appController, 'currentRouteName', infos[infos.length - 1].name); + this.notifyPropertyChange('url'); if (get(this, 'namespace').LOG_TRANSITIONS) { @@ -26062,15 +30777,15 @@ Ember.Router = Ember.Object.extend({ }, handleURL: function(url) { - return doTransition(this, 'handleURL', [url]); + return this._doTransition('handleURL', [url]); }, transitionTo: function() { - return doTransition(this, 'transitionTo', arguments); + return this._doTransition('transitionTo', arguments); }, replaceWith: function() { - return doTransition(this, 'replaceWith', arguments); + return this._doTransition('replaceWith', arguments); }, generate: function() { @@ -26121,165 +30836,213 @@ Ember.Router = Ember.Object.extend({ this._activeViews[templateName] = [view, disconnect]; view.one('willDestroyElement', this, disconnect); + }, + + _setupLocation: function() { + var location = get(this, 'location'), + rootURL = get(this, 'rootURL'), + options = {}; + + if (typeof rootURL === 'string') { + options.rootURL = rootURL; + } + + if ('string' === typeof location) { + options.implementation = location; + location = set(this, 'location', Ember.Location.create(options)); + } + }, + + _getHandlerFunction: function() { + var seen = {}, container = this.container, + DefaultRoute = container.lookupFactory('route:basic'), + self = this; + + return function(name) { + var routeName = 'route:' + name, + handler = container.lookup(routeName); + + if (seen[name]) { return handler; } + + seen[name] = true; + + if (!handler) { + if (name === 'loading') { return {}; } + + container.register(routeName, DefaultRoute.extend()); + handler = container.lookup(routeName); + + if (get(self, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + } + } + + if (name === 'application') { + // Inject default `error` handler. + // Note: `events` is deprecated, but we'll let the + // deprecation warnings be handled at event-handling time rather + // than duplicating that logic here. + var actions = handler._actions || handler.events; + if (!actions) { actions = handler._actions = {}; } + actions.error = actions.error || Ember.Router._defaultErrorHandler; + } + + handler.routeName = name; + return handler; + }; + }, + + _setupRouter: function(router, location) { + var lastURL, emberRouter = this; + + router.getHandler = this._getHandlerFunction(); + + var doUpdateURL = function() { + location.setURL(lastURL); + }; + + router.updateURL = function(path) { + lastURL = path; + Ember.run.once(doUpdateURL); + }; + + if (location.replaceURL) { + var doReplaceURL = function() { + location.replaceURL(lastURL); + }; + + router.replaceURL = function(path) { + lastURL = path; + Ember.run.once(doReplaceURL); + }; + } + + router.didTransition = function(infos) { + emberRouter.didTransition(infos); + }; + }, + + _doTransition: function(method, args) { + // Normalize blank route to root URL. + args = [].slice.call(args); + args[0] = args[0] || '/'; + + var passedName = args[0], name, self = this, + isQueryParamsOnly = false; + + if (Ember.FEATURES.isEnabled("query-params")) { + isQueryParamsOnly = (args.length === 1 && args[0].hasOwnProperty('queryParams')); + } + + if (!isQueryParamsOnly && passedName.charAt(0) === '/') { + name = passedName; + } else if (!isQueryParamsOnly) { + if (!this.router.hasRoute(passedName)) { + name = args[0] = passedName + '.index'; + } else { + name = passedName; + } + + Ember.assert("The route " + passedName + " was not found", this.router.hasRoute(name)); + } + + var transitionPromise = this.router[method].apply(this.router, args); + + // Don't schedule loading state entry if user has already aborted the transition. + if (this.router.activeTransition) { + this._scheduleLoadingStateEntry(); + } + + transitionPromise.then(function(route) { + self._transitionCompleted(route); + }, function(error) { + if (error.name === "UnrecognizedURLError") { + Ember.assert("The URL '" + error.message + "' did not match any routes in your application"); + } + }); + + // We want to return the configurable promise object + // so that callers of this function can use `.method()` on it, + // which obviously doesn't exist for normal RSVP promises. + return transitionPromise; + }, + + _scheduleLoadingStateEntry: function() { + if (this._loadingStateActive) { return; } + this._shouldEnterLoadingState = true; + Ember.run.scheduleOnce('routerTransitions', this, this._enterLoadingState); + }, + + _enterLoadingState: function() { + if (this._loadingStateActive || !this._shouldEnterLoadingState) { return; } + + var loadingRoute = this.router.getHandler('loading'); + if (loadingRoute) { + if (loadingRoute.enter) { loadingRoute.enter(); } + if (loadingRoute.setup) { loadingRoute.setup(); } + this._loadingStateActive = true; + } + }, + + _exitLoadingState: function () { + this._shouldEnterLoadingState = false; + if (!this._loadingStateActive) { return; } + + var loadingRoute = this.router.getHandler('loading'); + if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } + this._loadingStateActive = false; + }, + + _transitionCompleted: function(route) { + this.notifyPropertyChange('url'); + this._exitLoadingState(); } }); -function getHandlerFunction(router) { - var seen = {}, container = router.container, - DefaultRoute = container.resolve('route:basic'); +function triggerEvent(handlerInfos, ignoreFailure, args) { + var name = args.shift(); - return function(name) { - var routeName = 'route:' + name, - handler = container.lookup(routeName); + if (!handlerInfos) { + if (ignoreFailure) { return; } + throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); + } - if (seen[name]) { return handler; } + var eventWasHandled = false; - seen[name] = true; + for (var i=handlerInfos.length-1; i>=0; i--) { + var handlerInfo = handlerInfos[i], + handler = handlerInfo.handler; - if (!handler) { - if (name === 'loading') { return {}; } - - container.register(routeName, DefaultRoute.extend()); - handler = container.lookup(routeName); - - if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) { - Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + if (handler._actions && handler._actions[name]) { + if (handler._actions[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; + } + } else if (handler.events && handler.events[name]) { + Ember.deprecate('Action handlers contained in an `events` object are deprecated in favor of putting them in an `actions` object (' + name + ' on ' + handler + ')', false); + if (handler.events[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; } } - - if (name === 'application') { - // Inject default `error` handler. - handler.events = handler.events || {}; - handler.events.error = handler.events.error || defaultErrorHandler; - } - - handler.routeName = name; - return handler; - }; -} - -function defaultErrorHandler(error, transition) { - Ember.Logger.error('Error while loading route:', error); - - // Using setTimeout allows us to escape from the Promise's try/catch block - setTimeout(function() { throw error; }); -} - - -function routePath(handlerInfos) { - var path = []; - - for (var i=1, l=handlerInfos.length; i<l; i++) { - var name = handlerInfos[i].name, - nameParts = name.split("."); - - path.push(nameParts[nameParts.length - 1]); } - return path.join("."); -} - -function setupRouter(emberRouter, router, location) { - var lastURL; - - router.getHandler = getHandlerFunction(emberRouter); - - var doUpdateURL = function() { - location.setURL(lastURL); - }; - - router.updateURL = function(path) { - lastURL = path; - Ember.run.once(doUpdateURL); - }; - - if (location.replaceURL) { - var doReplaceURL = function() { - location.replaceURL(lastURL); - }; - - router.replaceURL = function(path) { - lastURL = path; - Ember.run.once(doReplaceURL); - }; + if (!eventWasHandled && !ignoreFailure) { + throw new Error("Nothing handled the event '" + name + "'."); } - - router.didTransition = function(infos) { - emberRouter.didTransition(infos); - }; -} - -function doTransition(router, method, args) { - // Normalize blank route to root URL. - args = [].slice.call(args); - args[0] = args[0] || '/'; - - var passedName = args[0], name; - - if (passedName.charAt(0) === '/') { - name = passedName; - } else { - if (!router.router.hasRoute(passedName)) { - name = args[0] = passedName + '.index'; - } else { - name = passedName; - } - - } - - var transitionPromise = router.router[method].apply(router.router, args); - - // Don't schedule loading state entry if user has already aborted the transition. - if (router.router.activeTransition) { - scheduleLoadingStateEntry(router); - } - - transitionPromise.then(transitionCompleted); - - // We want to return the configurable promise object - // so that callers of this function can use `.method()` on it, - // which obviously doesn't exist for normal RSVP promises. - return transitionPromise; -} - -function scheduleLoadingStateEntry(router) { - if (router._loadingStateActive) { return; } - router._shouldEnterLoadingState = true; - Ember.run.scheduleOnce('routerTransitions', null, enterLoadingState, router); -} - -function enterLoadingState(router) { - if (router._loadingStateActive || !router._shouldEnterLoadingState) { return; } - - var loadingRoute = router.router.getHandler('loading'); - if (loadingRoute) { - if (loadingRoute.enter) { loadingRoute.enter(); } - if (loadingRoute.setup) { loadingRoute.setup(); } - router._loadingStateActive = true; - } -} - -function exitLoadingState(router) { - router._shouldEnterLoadingState = false; - if (!router._loadingStateActive) { return; } - - var loadingRoute = router.router.getHandler('loading'); - if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } - router._loadingStateActive = false; -} - -function transitionCompleted(route) { - var router = route.router; - router.notifyPropertyChange('url'); - exitLoadingState(router); } Ember.Router.reopenClass({ + router: null, map: function(callback) { var router = this.router; if (!router) { - router = this.router = new Router(); + router = new Router(); router.callbacks = []; + router.triggerEvent = triggerEvent; + this.reopenClass({ router: router }); } if (get(this, 'namespace.LOG_TRANSITIONS_INTERNAL')) { @@ -26298,6 +31061,26 @@ Ember.Router.reopenClass({ router.callbacks.push(callback); router.map(dsl.generate()); return router; + }, + + _defaultErrorHandler: function(error, transition) { + Ember.Logger.error('Error while loading route:', error); + + // Using setTimeout allows us to escape from the Promise's try/catch block + setTimeout(function() { throw error; }); + }, + + _routePath: function(handlerInfos) { + var path = []; + + for (var i=1, l=handlerInfos.length; i<l; i++) { + var name = handlerInfos[i].name, + nameParts = name.split("."); + + path.push(nameParts[nameParts.length - 1]); + } + + return path.join("."); } }); @@ -26313,6 +31096,7 @@ Ember.Router.reopenClass({ */ var get = Ember.get, set = Ember.set, + getProperties = Ember.getProperties, classify = Ember.String.classify, fmt = Ember.String.fmt, a_forEach = Ember.EnumerableUtils.forEach, @@ -26326,7 +31110,7 @@ var get = Ember.get, set = Ember.set, @namespace Ember @extends Ember.Object */ -Ember.Route = Ember.Object.extend({ +Ember.Route = Ember.Object.extend(Ember.ActionHandler, { /** @private @@ -26353,28 +31137,124 @@ Ember.Route = Ember.Object.extend({ These functions will be invoked when a matching `{{action}}` is triggered from within a template and the application's current route is this route. - Events can also be invoked from other parts of your application via `Route#send` + Actions can also be invoked from other parts of your application via `Route#send` or `Controller#send`. - The context of the event will be this route. + The `actions` hash will inherit action handlers from + the `actions` hash defined on extended Route parent classes + or mixins rather than just replace the entire hash, e.g.: + + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + actions: { + displayBanner: function(msg) { + // ... + } + } + }); + + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + actions: { + playMusic: function() { + // ... + } + } + }); + + // `WelcomeRoute`, when active, will be able to respond + // to both actions, since the actions hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` + + Within a route's action handler, the value of the `this` context + is the Route object: + + ```js + App.SongRoute = Ember.Route.extend({ + actions: { + myAction: function() { + this.controllerFor("song"); + this.transitionTo("other.route"); + ... + } + } + }); + ``` + + It is also possible to call `this._super()` from within an + action handler if it overrides a handler defined on a parent + class or mixin: + + Take for example the following routes: + + ```js + App.DebugRoute = Ember.Mixin.create({ + actions: { + debugRouteInformation: function() { + console.debug("trololo"); + } + } + }); + + App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { + actions: { + debugRouteInformation: function() { + // also call the debugRouteInformation of mixed in App.DebugRoute + this._super(); + + // show additional annoyance + window.alert(...); + } + } + }); + ``` ## Bubbling - By default, an event will stop bubbling once a handler defined - on the `events` hash handles it. To continue bubbling the event, - you must return `true` from the handler. + By default, an action will stop bubbling once a handler defined + on the `actions` hash handles it. To continue bubbling the action, + you must return `true` from the handler: - ## Built-in events + ```js + App.Router.map(function() { + this.resource("album", function() { + this.route("song"); + }); + }); - There are a few built-in events pertaining to transitions that you + App.AlbumRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + } + } + }); + + App.AlbumSongRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + // ... + + if (actionShouldAlsoBeTriggeredOnParentRoute) { + return true; + } + } + } + }); + ``` + + ## Built-in actions + + There are a few built-in actions pertaining to transitions that you can use to customize transition behavior: `willTransition` and `error`. ### `willTransition` - The `willTransition` event is fired at the beginning of any + The `willTransition` action is fired at the beginning of any attempted transition with a `Transition` object as the sole - argument. This event can be used for aborting, redirecting, + argument. This action can be used for aborting, redirecting, or decorating the transition from the currently active routes. A good example is preventing navigation when a form is @@ -26382,7 +31262,7 @@ Ember.Route = Ember.Object.extend({ ```js App.ContactFormRoute = Ember.Route.extend({ - events: { + actions: { willTransition: function(transition) { if (this.controller.get('userHasEnteredData')) { this.controller.displayNavigationConfirm(); @@ -26398,7 +31278,7 @@ Ember.Route = Ember.Object.extend({ Note that `willTransition` will not be fired for the redirecting `transitionTo`, since `willTransition` doesn't fire when there is already a transition underway. If you want - subsequent `willTransition` events to fire for the redirecting + subsequent `willTransition` actions to fire for the redirecting transition, you must first explicitly call `transition.abort()`. @@ -26406,7 +31286,7 @@ Ember.Route = Ember.Object.extend({ When attempting to transition into a route, any of the hooks may throw an error, or return a promise that rejects, at which - point an `error` event will be fired on the partially-entered + point an `error` action will be fired on the partially-entered routes, allowing for per-route error handling logic, or shared error handling logic defined on a parent route. @@ -26423,7 +31303,7 @@ Ember.Route = Ember.Object.extend({ return Ember.RSVP.reject("bad things!"); }, - events: { + actions: { error: function(error, transition) { // Assuming we got here due to the error in `beforeModel`, // we can expect that error === "bad things!", @@ -26441,14 +31321,14 @@ Ember.Route = Ember.Object.extend({ }); ``` - `error` events that bubble up all the way to `ApplicationRoute` + `error` actions that bubble up all the way to `ApplicationRoute` will fire a default error handler that logs the error. You can specify your own global default error handler by overriding the `error` handler on `ApplicationRoute`: ```js App.ApplicationRoute = Ember.Route.extend({ - events: { + actions: { error: function(error, transition) { this.controllerFor('banner').displayError(error.message); } @@ -26459,12 +31339,22 @@ Ember.Route = Ember.Object.extend({ @see {Ember.Route#send} @see {Handlebars.helpers.action} - @property events + @property actions @type Hash @default null */ + actions: null, + + /** + @deprecated + + Please use `actions` instead. + @method events + */ events: null, + mergedProperties: ['events'], + /** This hook is executed when the router completely exits this route. It is not executed when the model for the route changes. @@ -26486,9 +31376,30 @@ Ember.Route = Ember.Object.extend({ route in question. The model will be serialized into the URL using the `serialize` hook. + Example + + ```javascript + App.Router.map(function() { + this.route("index"); + this.route("secret"); + this.route("fourOhFour", { path: "*:"}); + }); + + App.IndexRoute = Ember.Route.extend({ + actions: { + moveToSecret: function(context){ + if (authorized()){ + this.transitionTo('secret', context); + } + this.transitionTo('fourOhFour'); + } + } + }); + ``` + @method transitionTo @param {String} name the name of the route - @param {...Object} models the + @param {...Object} models */ transitionTo: function(name, context) { var router = this.router; @@ -26499,18 +31410,66 @@ Ember.Route = Ember.Object.extend({ Transition into another route while replacing the current URL if possible. Identical to `transitionTo` in all other respects. - Of the bundled location types, only `history` currently supports - this behavior. + Example + + ```javascript + App.Router.map(function() { + this.route("index"); + this.route("secret"); + }); + + App.SecretRoute = Ember.Route.extend({ + afterModel: function() { + if (!authorized()){ + this.replaceWith('index'); + } + } + }); + ``` @method replaceWith @param {String} name the name of the route - @param {...Object} models the + @param {...Object} models */ replaceWith: function() { var router = this.router; return this.router.replaceWith.apply(this.router, arguments); }, + /** + Sends an action to the router, which will delegate it to the currently + active route hierarchy per the bubbling rules explained under `actions`. + + Example + + ```javascript + App.Router.map(function() { + this.route("index"); + }); + + App.ApplicationRoute = Ember.Route.extend({ + actions: { + track: function(arg) { + console.log(arg, 'was clicked'); + } + } + }); + + App.IndexRoute = Ember.Route.extend({ + actions: { + trackIfDebug: function(arg) { + if (debug) { + this.send('track', arg); + } + } + } + }); + ``` + + @method send + @param {String} name the name of the action to trigger + @param {...*} args + */ send: function() { return this.router.send.apply(this.router, arguments); }, @@ -26522,7 +31481,7 @@ Ember.Route = Ember.Object.extend({ @method setup */ - setup: function(context) { + setup: function(context, queryParams) { var controllerName = this.controllerName || this.routeName, controller = this.controllerFor(controllerName, true); if (!controller) { @@ -26530,21 +31489,27 @@ Ember.Route = Ember.Object.extend({ } // Assign the route's controller so that it can more easily be - // referenced in event handlers + // referenced in action handlers this.controller = controller; - if (this.setupControllers) { + var args = [controller, context]; + if (Ember.FEATURES.isEnabled("query-params")) { + args.push(queryParams); + } + + if (this.setupControllers) { + Ember.deprecate("Ember.Route.setupControllers is deprecated. Please use Ember.Route.setupController(controller, model) instead."); this.setupControllers(controller, context); } else { - this.setupController(controller, context); + this.setupController.apply(this, args); } if (this.renderTemplates) { - + Ember.deprecate("Ember.Route.renderTemplates is deprecated. Please use Ember.Route.renderTemplate(controller, model) instead."); this.renderTemplates(context); } else { - this.renderTemplate(controller, context); + this.renderTemplate.apply(this, args); } }, @@ -26635,6 +31600,7 @@ Ember.Route = Ember.Object.extend({ @method beforeModel @param {Transition} transition + @param {Object} queryParams the active query params for this route @return {Promise} if the value returned from this hook is a promise, the transition will pause until the transition resolves. Otherwise, non-promise return values are not @@ -26651,7 +31617,7 @@ Ember.Route = Ember.Object.extend({ resolved. ```js - App.PostRoute = Ember.Route.extend({ + App.PostsRoute = Ember.Route.extend({ afterModel: function(posts, transition) { if (posts.length === 1) { this.transitionTo('post.show', posts[0]); @@ -26665,13 +31631,16 @@ Ember.Route = Ember.Object.extend({ from this hook. @method afterModel + @param {Object} resolvedModel the value returned from `model`, + or its resolved value if it was a promise @param {Transition} transition + @param {Object} queryParams the active query params for this handler @return {Promise} if the value returned from this hook is a promise, the transition will pause until the transition resolves. Otherwise, non-promise return values are not utilized in any way. */ - afterModel: function(resolvedModel, transition) { + afterModel: function(resolvedModel, transition, queryParams) { this.redirect(resolvedModel, transition); }, @@ -26718,10 +31687,26 @@ Ember.Route = Ember.Object.extend({ if a promise returned from `model` fails, the error will be handled by the `error` hook on `Ember.Route`. + Example + + ```js + App.PostRoute = Ember.Route.extend({ + model: function(params) { + return App.Post.find(params.post_id); + } + }); + ``` + @method model @param {Object} params the parameters extracted from the URL + @param {Transition} transition + @param {Object} queryParams the query params for this route + @return {Object|Promise} the model for this route. If + a promise is returned, the transition will pause until + the promise resolves, and the resolved value of the promise + will be used as the model for this route. */ - model: function(params, resolvedParentModels) { + model: function(params, transition) { var match, name, sawParams, value; for (var prop in params) { @@ -26735,11 +31720,48 @@ Ember.Route = Ember.Object.extend({ if (!name && sawParams) { return params; } else if (!name) { return; } - var modelClass = this.container.lookupFactory('model:' + name); + return this.findModel(name, value); + }, + + /** + + @method findModel + @param {String} type the model type + @param {Object} value the value passed to find + */ + findModel: function(){ + var store = get(this, 'store'); + return store.find.apply(store, arguments); + }, + + /** + Store property provides a hook for data persistence libraries to inject themselves. + + By default, this store property provides the exact same functionality previously + in the model hook. + + Currently, the required interface is: + + `store.find(modelName, findArguments)` + + @method store + @param {Object} store + */ + store: Ember.computed(function(){ + var container = this.container; + var routeName = this.routeName; var namespace = get(this, 'router.namespace'); - return modelClass.find(value); - }, + return { + find: function(name, value) { + var modelClass = container.lookupFactory('model:' + name); + + Ember.assert("You used the dynamic segment " + name + "_id in your route "+ routeName + ", but " + namespace + "." + classify(name) + " did not exist and you did not override your route's `model` hook.", modelClass); + + return modelClass.find(value); + } + }; + }), /** A hook you can implement to convert the route's model into parameters @@ -26763,8 +31785,10 @@ Ember.Route = Ember.Object.extend({ }); ``` - The default `serialize` method inserts the model's `id` into the - route's dynamic segment (in this case, `:post_id`). + The default `serialize` method will insert the model's `id` into the + route's dynamic segment (in this case, `:post_id`) if the segment contains '_id'. + If the route has multiple dynamic segments or does not contain '_id', `serialize` + will return `Ember.getProperties(model, params)` This method is called when `transitionTo` is called with a context in order to populate the URL. @@ -26776,14 +31800,14 @@ Ember.Route = Ember.Object.extend({ @return {Object} the serialized parameters */ serialize: function(model, params) { - if (params.length !== 1) { return; } + if (params.length < 1) { return; } var name = params[0], object = {}; - if (/_id$/.test(name)) { - object[name] = get(model, 'id'); + if (/_id$/.test(name) && params.length === 1) { + object[name] = get(model, "id"); } else { - object[name] = model; + object = getProperties(model, params); } return object; @@ -26823,7 +31847,18 @@ Ember.Route = Ember.Object.extend({ be used if it is defined. If it is not defined, an `Ember.ObjectController` instance would be used. + Example + ```js + App.PostRoute = Ember.Route.extend({ + setupController: function(controller, model) { + controller.set('model', model); + } + }); + ``` + @method setupController + @param {Controller} controller instance + @param {Object} model */ setupController: function(controller, context) { if (controller && (context !== undefined)) { @@ -26832,7 +31867,10 @@ Ember.Route = Ember.Object.extend({ }, /** - Returns the controller for a particular route. + Returns the controller for a particular route or name. + + The controller instance must already have been created, either through entering the + associated route or using `generateController`. ```js App.PostRoute = Ember.Route.extend({ @@ -26844,17 +31882,24 @@ Ember.Route = Ember.Object.extend({ ``` @method controllerFor - @param {String} name the name of the route + @param {String} name the name of the route or controller @return {Ember.Controller} */ controllerFor: function(name, _skipAssert) { - var container = this.router.container, - controller = container.lookup('controller:' + name); + var container = this.container, + route = container.lookup('route:'+name), + controller; + + if (route && route.controllerName) { + name = route.controllerName; + } + + controller = container.lookup('controller:' + name); // NOTE: We're specifically checking that skipAssert is true, because according // to the old API the second parameter was model. We do not want people who // passed a model to skip the assertion. - + Ember.assert("The controller named '"+name+"' could not be found. Make sure that this route exists and has already been entered at least once. If you are accessing a controller not associated with a route, make sure the controller class is explicitly defined.", controller || _skipAssert === true); return controller; }, @@ -26865,16 +31910,26 @@ Ember.Route = Ember.Object.extend({ If the optional model is passed then the controller type is determined automatically, e.g., an ArrayController for arrays. + Example + + ```js + App.PostRoute = Ember.Route.extend({ + setupController: function(controller, post) { + this._super(controller, post); + this.generateController('posts', post); + } + }); + ``` + @method generateController @param {String} name the name of the controller @param {Object} model the model to infer the type of the controller (optional) */ generateController: function(name, model) { - var container = this.router.container; + var container = this.container; model = model || this.modelFor(name); - return Ember.generateController(container, name, model); }, @@ -26884,6 +31939,22 @@ Ember.Route = Ember.Object.extend({ This is the object returned by the `model` hook of the route in question. + Example + + ```js + App.Router.map(function() { + this.resource('post', { path: '/post/:post_id' }, function() { + this.resource('comments'); + }); + }); + + App.CommentsRoute = Ember.Route.extend({ + afterModel: function() { + this.set('post', this.modelFor('post')); + } + }); + ``` + @method modelFor @param {String} name the name of the route @return {Object} the model object @@ -26915,6 +31986,22 @@ Ember.Route = Ember.Object.extend({ This method can be overridden to set up and render additional or alternative templates. + ```js + App.PostsRoute = Ember.Route.extend({ + renderTemplate: function(controller, model) { + var favController = this.controllerFor('favoritePost'); + + // Render the `favoritePost` template into + // the outlet `posts`, and display the `favoritePost` + // controller. + this.render('favoritePost', { + outlet: 'posts', + controller: favController + }); + } + }); + ``` + @method renderTemplate @param {Object} controller the route's controller @param {Object} model the route's model @@ -26975,7 +32062,7 @@ Ember.Route = Ember.Object.extend({ @param {Object} options the options */ render: function(name, options) { - + Ember.assert("The name in the given arguments is undefined", arguments.length > 0 ? !Ember.isNone(arguments[0]) : true); var namePassed = !!name; @@ -26984,14 +32071,21 @@ Ember.Route = Ember.Object.extend({ name = this.routeName; } + options = options || {}; name = name ? name.replace(/\//g, '.') : this.routeName; + var viewName = options.view || this.viewName || name; + var templateName = this.templateName || name; var container = this.container, - view = container.lookup('view:' + name), - template = container.lookup('template:' + name); + view = container.lookup('view:' + viewName), + template = view ? view.get('template') : null; + + if (!template) { + template = container.lookup('template:' + templateName); + } if (!view && !template) { - + Ember.assert("Could not find \"" + name + "\" template or view.", !namePassed); if (get(this.router, 'namespace.LOG_VIEW_LOOKUPS')) { Ember.Logger.info("Could not find \"" + name + "\" template or view. Nothing will be rendered", { fullName: 'template:' + name }); } @@ -27019,7 +32113,7 @@ Ember.Route = Ember.Object.extend({ ```js App.ApplicationRoute = App.Route.extend({ - events: { + actions: { showModal: function(evt) { this.render(evt.modalName, { outlet: 'modal', @@ -27052,6 +32146,11 @@ Ember.Route = Ember.Object.extend({ this.teardownViews(); }, + /** + @private + + @method teardownViews + */ teardownViews: function() { // Tear down the top level view if (this.teardownTopLevelView) { this.teardownTopLevelView(); } @@ -27102,6 +32201,7 @@ function normalizeOptions(route, name, template, options) { options.template = template; options.LOG_VIEW_LOOKUPS = get(route.router, 'namespace.LOG_VIEW_LOOKUPS'); + Ember.assert("An outlet ("+options.outlet+") was specified but was not found.", options.outlet === 'main' || options.into); var controller = options.controller, namedController; @@ -27110,7 +32210,7 @@ function normalizeOptions(route, name, template, options) { } else if (namedController = route.container.lookup('controller:' + name)) { controller = namedController; } else { - controller = route.routeName; + controller = route.controllerName || route.routeName; } if (typeof controller === 'string') { @@ -27188,24 +32288,45 @@ function generateOutletTeardown(parentView, outlet) { Ember.onLoad('Ember.Handlebars', function() { var handlebarsResolve = Ember.Handlebars.resolveParams, map = Ember.ArrayPolyfills.map, - get = Ember.get; + get = Ember.get, + handlebarsGet = Ember.Handlebars.get; function resolveParams(context, params, options) { - var resolved = handlebarsResolve(context, params, options); - return map.call(resolved, unwrap); + return map.call(resolvePaths(context, params, options), function(path, i) { + if (null === path) { + // Param was string/number, not a path, so just return raw string/number. + return params[i]; + } else { + return handlebarsGet(context, path, options); + } + }); + } - function unwrap(object, i) { - if (params[i] === 'controller') { return object; } + function resolvePaths(context, params, options) { + var resolved = handlebarsResolve(context, params, options), + types = options.types; + + return map.call(resolved, function(object, i) { + if (types[i] === 'ID') { + return unwrap(object, params[i]); + } else { + return null; + } + }); + + function unwrap(object, path) { + if (path === 'controller') { return path; } if (Ember.ControllerMixin.detect(object)) { - return unwrap(get(object, 'model')); + return unwrap(get(object, 'model'), path ? path + '.model' : 'model'); } else { - return object; + return path; } } } Ember.Router.resolveParams = resolveParams; + Ember.Router.resolvePaths = resolvePaths; }); })(); @@ -27222,6 +32343,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Ember.onLoad('Ember.Handlebars', function(Handlebars) { var resolveParams = Ember.Router.resolveParams, + resolvePaths = Ember.Router.resolvePaths, isSimpleClick = Ember.ViewUtils.isSimpleClick; function fullRouteName(router, name) { @@ -27232,19 +32354,12 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return name; } - function resolvedPaths(options) { - var types = options.options.types.slice(1), + function getResolvedPaths(options) { + + var types = options.options.types, data = options.options.data; - return resolveParams(options.context, options.params, { types: types, data: data }); - } - - function createPath(path) { - var fullPath = 'paramsContext'; - if (path !== '') { - fullPath += '.' + path; - } - return fullPath; + return resolvePaths(options.context, options.params, { types: types, data: data }); } /** @@ -27253,17 +32368,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { a supplied route by name. Instances of `LinkView` will most likely be created through - the `linkTo` Handlebars helper, but properties of this class + the `link-to` Handlebars helper, but properties of this class can be overridden to customize application-wide behavior. @class LinkView @namespace Ember @extends Ember.View - @see {Handlebars.helpers.linkTo} + @see {Handlebars.helpers.link-to} **/ var LinkView = Ember.LinkView = Ember.View.extend({ tagName: 'a', - namedRoute: null, currentWhen: null, /** @@ -27274,6 +32388,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { **/ title: null, + /** + Sets the `rel` attribute of the `LinkView`'s HTML element. + + @property rel + @default null + **/ + rel: null, + /** The CSS class to apply to `LinkView`'s element when its `active` property is `true`. @@ -27314,11 +32436,30 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @default false **/ replace: false, - attributeBindings: ['href', 'title'], + + /** + By default the `{{link-to}}` helper will bind to the `href` and + `title` attributes. It's discourage that you override these defaults, + however you can push onto the array if needed. + + @property attributeBindings + @type Array | String + @default ['href', 'title', 'rel'] + **/ + attributeBindings: ['href', 'title', 'rel'], + + /** + By default the `{{link-to}}` helper will bind to the `active`, `loading`, and + `disabled` classes. It is discouraged to override these directly. + + @property classNameBindings + @type Array + @default ['active', 'loading', 'disabled'] + **/ classNameBindings: ['active', 'loading', 'disabled'], /** - By default the `{{linkTo}}` helper responds to the `click` event. You + By default the `{{link-to}}` helper responds to the `click` event. You can override this globally by setting this property to your custom event name. @@ -27343,6 +32484,28 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @event click **/ + /** + An overridable method called when LinkView objects are instantiated. + + Example: + + ```javascript + App.MyLinkView = Ember.LinkView.extend({ + init: function() { + this._super(); + Ember.Logger.log('Event is ' + this.get('eventName')); + } + }); + ``` + + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. + + @method init + */ init: function() { this._super.apply(this, arguments); @@ -27350,27 +32513,57 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { var eventName = get(this, 'eventName'); this.on(eventName, this, this._invoke); - var params = this.parameters.params, - length = params.length, - context = this.parameters.context, - self = this, - path, paths = Ember.A([]), i; - - set(this, 'paramsContext', context); + var helperParameters = this.parameters, + templateContext = helperParameters.context, + paths = getResolvedPaths(helperParameters), + length = paths.length, + path, i; for(i=0; i < length; i++) { - paths.pushObject(createPath(params[i])); + path = paths[i]; + if (null === path) { + // A literal value was provided, not a path, so nothing to observe. + continue; + } + + var normalizedPath = + Ember.Handlebars.normalizePath(templateContext, path, helperParameters.options.data); + this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); } - var observer = function(object, path) { - this.notifyPropertyChange('routeArgs'); - }; - for(i=0; i < length; i++) { - this.registerObserver(this, paths[i], this, observer); + if (Ember.FEATURES.isEnabled("query-params")) { + var queryParams = get(this, '_potentialQueryParams') || []; + + for(i=0; i < queryParams.length; i++) { + this.registerObserver(this, queryParams[i], this, this._queryParamsChanged); + } } }, + /** + @private + + This method is invoked by observers installed during `init` that fire + whenever the params change + @method _paramsChanged + */ + _paramsChanged: function() { + this.notifyPropertyChange('resolvedParams'); + }, + + + /** + @private + + This method is invoked by observers installed during `init` that fire + whenever the query params change + */ + _queryParamsChanged: function (object, path) { + this.notifyPropertyChange('queryParams'); + }, + + /** @private @@ -27394,7 +32587,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { disabled: Ember.computed(function(key, value) { if (value !== undefined) { this.set('_isDisabled', value); } - return value ? this.get('disabledClass') : false; + return value ? get(this, 'disabledClass') : false; }), /** @@ -27408,33 +32601,44 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @property active **/ active: Ember.computed(function() { + if (get(this, 'loading')) { return false; } + var router = get(this, 'router'), - params = resolvedPaths(this.parameters), - currentWhen = this.currentWhen || get(this, 'namedRoute'), + routeArgs = get(this, 'routeArgs'), + contexts = routeArgs.slice(1), + resolvedParams = get(this, 'resolvedParams'), + currentWhen = this.currentWhen || resolvedParams[0], currentWithIndex = currentWhen + '.index', - isActive = router.isActive.apply(router, [currentWhen].concat(params)) || - router.isActive.apply(router, [currentWithIndex].concat(params)); + isActive = router.isActive.apply(router, [currentWhen].concat(contexts)) || + router.isActive.apply(router, [currentWithIndex].concat(contexts)); if (isActive) { return get(this, 'activeClass'); } - }).property('namedRoute', 'router.url'), + }).property('resolvedParams', 'routeArgs', 'router.url'), + /** + Accessed as a classname binding to apply the `LinkView`'s `loadingClass` + CSS `class` to the element when the link is loading. + + A `LinkView` is considered loading when it has at least one + parameter whose value is currently null or undefined. During + this time, clicking the link will perform no transition and + emit a warning that the link is still in a loading state. + + @property loading + **/ loading: Ember.computed(function() { if (!get(this, 'routeArgs')) { return get(this, 'loadingClass'); } }).property('routeArgs'), /** - Accessed as a classname binding to apply the `LinkView`'s `activeClass` - CSS `class` to the element when the link is active. + @private - A `LinkView` is considered active when its `currentWhen` property is `true` - or the application's current route is the route the `LinkView` would trigger - transitions into. + Returns the application's main router from the container. - @property active + @property router **/ - router: Ember.computed(function() { - return this.get('controller').container.lookup('router:main'); + return get(this, 'controller').container.lookup('router:main'); }), /** @@ -27454,43 +32658,103 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (get(this, '_isDisabled')) { return false; } if (get(this, 'loading')) { - Ember.Logger.warn("This linkTo is in an inactive loading state because at least one of its parameters' presently has a null/undefined value, or the provided route name is invalid."); + Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid."); return false; } var router = get(this, 'router'), routeArgs = get(this, 'routeArgs'); - if (this.get('replace')) { + if (get(this, 'replace')) { router.replaceWith.apply(router, routeArgs); } else { router.transitionTo.apply(router, routeArgs); } }, + /** + @private + + Computed property that returns the resolved parameters. + + @property + @return {Array} + */ + resolvedParams: Ember.computed(function() { + var parameters = this.parameters, + options = parameters.options, + types = options.types, + data = options.data; + + return resolveParams(parameters.context, parameters.params, { types: types, data: data }); + }).property(), + + /** + @private + + Computed property that returns the current route name and + any dynamic segments. + + @property + @return {Array} An array with the route name and any dynamic segments + */ routeArgs: Ember.computed(function() { + var resolvedParams = get(this, 'resolvedParams').slice(0), + router = get(this, 'router'), + namedRoute = resolvedParams[0]; - var router = get(this, 'router'), - namedRoute = get(this, 'namedRoute'), routeName; + if (!namedRoute) { return; } - if (!namedRoute && this.namedRouteBinding) { - // The present value of namedRoute is falsy, but since it's a binding - // and could be valid later, don't treat as error. - return; + namedRoute = fullRouteName(router, namedRoute); + resolvedParams[0] = namedRoute; + + Ember.assert(fmt("The attempt to link-to route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [namedRoute, namedRoute, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(namedRoute)); + + for (var i = 1, len = resolvedParams.length; i < len; ++i) { + var param = resolvedParams[i]; + if (param === null || typeof param === 'undefined') { + // If contexts aren't present, consider the linkView unloaded. + return; + } } + + if (Ember.FEATURES.isEnabled("query-params")) { + var queryParams = get(this, 'queryParams'); + + if (queryParams || queryParams === false) { resolvedParams.push({queryParams: queryParams}); } + } + + return resolvedParams; + }).property('resolvedParams', 'queryParams', 'router.url'), + + + _potentialQueryParams: Ember.computed(function () { + var namedRoute = get(this, 'resolvedParams')[0]; + if (!namedRoute) { return null; } + var router = get(this, 'router'); + namedRoute = fullRouteName(router, namedRoute); + return router.router.queryParamsForHandler(namedRoute); + }).property('resolvedParams'), - var resolvedContexts = resolvedPaths(this.parameters), paramsPresent = true; - for (var i = 0, l = resolvedContexts.length; i < l; ++i) { - var context = resolvedContexts[i]; + queryParams: Ember.computed(function () { + var self = this, + queryParams = null, + allowedQueryParams = get(this, '_potentialQueryParams'); - // If contexts aren't present, consider the linkView unloaded. - if (context === null || typeof context === 'undefined') { return; } - } + if (!allowedQueryParams) { return null; } + allowedQueryParams.forEach(function (param) { + var value = get(self, param); + if (typeof value !== 'undefined') { + queryParams = queryParams || {}; + queryParams[param] = value; + } + }); - return [ namedRoute ].concat(resolvedContexts); - }).property('namedRoute'), + + return queryParams; + }).property('_potentialQueryParams.[]'), /** Sets the element's `href` attribute to the url for @@ -27502,7 +32766,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @property href **/ href: Ember.computed(function() { - if (this.get('tagName') !== 'a') { return false; } + if (get(this, 'tagName') !== 'a') { return false; } var router = get(this, 'router'), routeArgs = get(this, 'routeArgs'); @@ -27511,7 +32775,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }).property('routeArgs'), /** - The default href value to use while a linkTo is loading. + The default href value to use while a link-to is loading. Only applies when tagName is 'a' @property loadingHref @@ -27524,16 +32788,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { LinkView.toString = function() { return "LinkView"; }; /** - The `{{linkTo}}` helper renders a link to the supplied + The `{{link-to}}` helper renders a link to the supplied `routeName` passing an optionally supplied model to the route as its `model` context of the route. The block - for `{{linkTo}}` becomes the innerHTML of the rendered + for `{{link-to}}` becomes the innerHTML of the rendered element: ```handlebars - {{#linkTo 'photoGallery'}} + {{#link-to 'photoGallery'}} Great Hamster Photos - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27543,14 +32807,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ``` ### Supplying a tagName - By default `{{linkTo}}` renders an `<a>` element. This can - be overridden for a single use of `{{linkTo}}` by supplying + By default `{{link-to}}` renders an `<a>` element. This can + be overridden for a single use of `{{link-to}}` by supplying a `tagName` option: ```handlebars - {{#linkTo 'photoGallery' tagName="li"}} + {{#link-to 'photoGallery' tagName="li"}} Great Hamster Photos - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27561,25 +32825,62 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { To override this option for your entire application, see "Overriding Application-wide Defaults". - + + ### Disabling the `link-to` heper + By default `{{link-to}}` is enabled. + any passed value to `disabled` helper property will disable the `link-to` helper. + + static use: the `disabled` option: + + ```handlebars + {{#link-to 'photoGallery' disabled=true}} + Great Hamster Photos + {{/link-to}} + ``` + + dynamic use: the `disabledWhen` option: + + ```handlebars + {{#link-to 'photoGallery' disabledWhen=controller.someProperty}} + Great Hamster Photos + {{/link-to}} + ``` + + any passed value to `disabled` will disable it except `undefined`. + to ensure that only `true` disable the `link-to` helper you can + override the global behaviour of `Ember.LinkView`. + + ```javascript + Ember.LinkView.reopen({ + disabled: Ember.computed(function(key, value) { + if (value !== undefined) { + this.set('_isDisabled', value === true); + } + return value === true ? get(this, 'disabledClass') : false; + }) + }); + ``` + + see "Overriding Application-wide Defaults" for more. + ### Handling `href` - `{{linkTo}}` will use your application's Router to + `{{link-to}}` will use your application's Router to fill the element's `href` property with a url that matches the path to the supplied `routeName` for your routers's configured `Location` scheme, which defaults to Ember.HashLocation. ### Handling current route - `{{linkTo}}` will apply a CSS class name of 'active' + `{{link-to}}` will apply a CSS class name of 'active' when the application's current route matches the supplied routeName. For example, if the application's current route is 'photoGallery.recent' the following - use of `{{linkTo}}`: + use of `{{link-to}}`: ```handlebars - {{#linkTo 'photoGallery.recent'}} + {{#link-to 'photoGallery.recent'}} Great Hamster Photos from the last week - {{/linkTo}} + {{/link-to}} ``` will result in @@ -27591,13 +32892,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ``` The CSS class name used for active classes can be customized - for a single use of `{{linkTo}}` by passing an `activeClass` + for a single use of `{{link-to}}` by passing an `activeClass` option: ```handlebars - {{#linkTo 'photoGallery.recent' activeClass="current-url"}} + {{#link-to 'photoGallery.recent' activeClass="current-url"}} Great Hamster Photos from the last week - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27617,13 +32918,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```javascript App.Router.map(function() { this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); - }) + }); ``` ```handlebars - {{#linkTo 'photoGallery' aPhoto}} + {{#link-to 'photoGallery' aPhoto}} {{aPhoto.title}} - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27649,9 +32950,9 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { This argument will become the model context of the linked route: ```handlebars - {{#linkTo 'photoGallery.comment' aPhoto comment}} + {{#link-to 'photoGallery.comment' aPhoto comment}} {{comment.body}} - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27660,8 +32961,50 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { </a> ``` + ### Supplying an explicit dynamic segment value + If you don't have a model object available to pass to `{{link-to}}`, + an optional string or integer argument can be passed for routes whose + paths contain dynamic segments. This argument will become the value + of the dynamic segment: + + ```javascript + App.Router.map(function() { + this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); + }); + ``` + + ```handlebars + {{#link-to 'photoGallery' aPhotoId}} + {{aPhoto.title}} + {{/link-to}} + ``` + + ```html + <a href="/hamster-photos/42"> + Tomster + </a> + ``` + + When transitioning into the linked route, the `model` hook will + be triggered with parameters including this passed identifier. + + ### Overriding attributes + You can override any given property of the Ember.LinkView + that is generated by the `{{link-to}}` helper by passing + key/value pairs, like so: + + ```handlebars + {{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames=['pic', 'sweet']}} + Uh-mazing! + {{/link-to}} + ``` + + See [Ember.LinkView](/api/classes/Ember.LinkView.html) for a + complete list of overrideable properties. Be sure to also + check out inherited properties of `LinkView`. + ### Overriding Application-wide Defaults - ``{{linkTo}}`` creates an instance of Ember.LinkView + ``{{link-to}}`` creates an instance of Ember.LinkView for rendering. To override options for your entire application, reopen Ember.LinkView and supply the desired values: @@ -27682,28 +33025,18 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }); ``` - @method linkTo + @method link-to @for Ember.Handlebars.helpers @param {String} routeName @param {Object} [context]* + @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkView @return {String} HTML string + @see {Ember.LinkView} */ - Ember.Handlebars.registerHelper('linkTo', function(name) { + Ember.Handlebars.registerHelper('link-to', function(name) { var options = [].slice.call(arguments, -1)[0], - params = [].slice.call(arguments, 1, -1); - - var hash = options.hash; - - if (options.types[0] === "ID") { - if (Ember.ENV.HELPER_PARAM_LOOKUPS) { - hash.namedRouteBinding = name; - } else { - - hash.namedRoute = name; - } - } else { - hash.namedRoute = name; - } + params = [].slice.call(arguments, 0, -1), + hash = options.hash; hash.disabledBinding = hash.disabledWhen; @@ -27715,6 +33048,18 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return Ember.Handlebars.helpers.view.call(this, LinkView, options); }); + + /** + See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) + + @method linkTo + @for Ember.Handlebars.helpers + @deprecated + @param {String} routeName + @param {Object} [context]* + @return {String} HTML string + */ + Ember.Handlebars.registerHelper('linkTo', Ember.Handlebars.helpers['link-to']); }); @@ -27764,8 +33109,8 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { You can create custom named outlets for more control. ``` handlebars - {{outlet favoritePost}} - {{outlet posts}} + {{outlet 'favoritePost'}} + {{outlet 'posts'}} ``` Then you can define what template is rendered into each outlet in your @@ -27799,6 +33144,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @for Ember.Handlebars.helpers @param {String} property the property on the controller that holds the view for this outlet + @return {String} HTML string */ Handlebars.registerHelper('outlet', function(property, options) { var outletSource, outletContainerClass; @@ -27809,7 +33155,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { } outletSource = options.data.view; - while (!(outletSource.get('template.isTop'))) { + while (!outletSource.get('template.isTop')) { outletSource = outletSource.get('_parentView'); } @@ -27836,33 +33182,79 @@ var get = Ember.get, set = Ember.set; Ember.onLoad('Ember.Handlebars', function(Handlebars) { /** - Renders the named template in the current context with the same-named - controller. + Calling ``{{render}}`` from within a template will insert another + template that matches the provided name. The inserted template will + access its properties on its own controller (rather than the controller + of the parent template). - If a view class with the same name exists, the view class will be used. + If a view class with the same name exists, the view class also will be used. + + Note: A given controller may only be used *once* in your app in this manner. + A singleton instance of the controller will be created for you. - The optional second argument is a property path that will be bound - to the `model` property of the controller. + Example: + + ```javascript + App.NavigationController = Ember.Controller.extend({ + who: "world" + }); + ``` + + ```handelbars + <!-- navigation.hbs --> + Hello, {{who}}. + ``` + + ```handelbars + <!-- applications.hbs --> + <h1>My great app</h1> + {{render navigaton}} + ``` + + ```html + <h1>My great app</h1> + <div class='ember-view'> + Hello, world. + </div> + ``` + + Optionally you may provide a second argument: a property path + that will be bound to the `model` property of the controller. If a `model` property path is specified, then a new instance of the - controller will be created. + controller will be created and `{{render}}` can be used multiple times + with the same name. - If no `model` property path is provided, then the helper will use the - singleton instance of the controller. A given controller may only be used - one time in your app in this manner. + For example if you had this `author` template. - The default target for `{{action}}`s in the rendered template is the - controller. + ```handlebars +<div class="author"> + Written by {{firstName}} {{lastName}}. + Total Posts: {{postCount}} +</div> + ``` + + You could render it inside the `post` template using the `render` helper. + + ```handlebars +<div class="post"> + <h1>{{title}}</h1> + <div>{{body}}</div> + {{render "author" author}} +</div> + ``` @method render @for Ember.Handlebars.helpers @param {String} name @param {Object?} contextString @param {Hash} options + @return {String} HTML string */ Ember.Handlebars.registerHelper('render', function(name, contextString, options) { - - var container, router, controller, view, context, lookupOptions; + Ember.assert("You must pass a template to render", arguments.length >= 2); + var contextProvided = arguments.length === 3, + container, router, controller, view, context, lookupOptions; if (arguments.length === 2) { options = contextString; @@ -27878,6 +33270,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { container = options.data.keywords.controller.container; router = container.lookup('router:main'); + Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", contextProvided || !router || !router._lookupActiveView(name)); view = container.lookup('view:' + name) || container.lookup('view:default'); @@ -27886,13 +33279,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { // Look up the controller by name, if provided. if (controllerName) { controller = container.lookup('controller:' + controllerName, lookupOptions); - + Ember.assert("The controller name you supplied '" + controllerName + "' did not resolve to a controller.", !!controller); } else { controller = container.lookup('controller:' + name, lookupOptions) || Ember.generateController(container, name, context); } - if (controller && context) { + if (controller && contextProvided) { controller.set('model', context); } @@ -27961,6 +33354,10 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return isSimpleClick(event); } + if (allowedKeys.indexOf("any") >= 0) { + return true; + } + var allowed = true; forEach.call(keys, function(key) { @@ -27998,7 +33395,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (target.send) { target.send.apply(target, args(options.parameters, actionName)); } else { - + Ember.assert("The action '" + actionName + "' did not exist on " + target, typeof target[actionName] === 'function'); target[actionName].apply(target, args(options.parameters)); } }); @@ -28026,11 +33423,9 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { Given the following Handlebars template on the page ```handlebars - <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName}}> - click me - </div> - </script> + <div {{action 'anActionName'}}> + click me + </div> ``` And application code @@ -28084,7 +33479,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ``` If you need the default handler to trigger you should either register your - own event handler, or use event methods on your view class. See `Ember.View` + own event handler, or use event methods on your view class. See [Ember.View](/api/classes/Ember.View.html) 'Responding to Browser Events' for more information. ### Specifying DOM event type @@ -28094,7 +33489,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName on="doubleClick"}}> + <div {{action 'anActionName' on="doubleClick"}}> click me </div> </script> @@ -28116,7 +33511,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName allowedKeys="alt"}}> + <div {{action 'anActionName' allowedKeys="alt"}}> click me </div> </script> @@ -28124,6 +33519,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { This way the `{{action}}` will fire when clicking with the alt key pressed down. + Alternatively, supply "any" to the `allowedKeys` option to accept any combination of modifier keys. + + ```handlebars + <script type="text/x-handlebars" data-template-name='a-template'> + <div {{action 'anActionName' allowedKeys="any"}}> + click me with any key pressed + </div> + </script> + ``` + ### Specifying a Target There are several possible target objects for `{{action}}` helpers: @@ -28138,7 +33543,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName target="MyApplication.someObject"}}> + <div {{action 'anActionName' target="MyApplication.someObject"}}> click me </div> </script> @@ -28152,7 +33557,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action aMethodNameThatIsMissing}}> + <div {{action 'aMethodNameThatIsMissing'}}> click me </div> </script> @@ -28183,7 +33588,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> {{#each person in people}} - <div {{action edit person}}> + <div {{action 'edit' person}}> click me </div> {{/each}} @@ -28251,9 +33656,30 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { var get = Ember.get, set = Ember.set; /** + `{{control}}` works like render, except it uses a new controller instance for every call, instead of reusing the singleton controller. + The control helper is currently under development and is considered experimental. To enable it, set `ENV.EXPERIMENTAL_CONTROL_HELPER = true` before requiring Ember. + For example if you had this `author` template. + + ```handlebars +<div class="author"> + Written by {{firstName}} {{lastName}}. + Total Posts: {{postCount}} +</div> + ``` + + You could render it inside the `post` template using the `control` helper. + + ```handlebars +<div class="post"> + <h1>{{title}}</h1> + <div>{{body}}</div> + {{control "author" author}} +</div> + ``` + @method control @for Ember.Handlebars.helpers @param {String} path @@ -28293,7 +33719,8 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { childController = subContainer.lookup('controller:' + normalizedPath), childTemplate = subContainer.lookup('template:' + path); - + Ember.assert("Could not find controller for path: " + normalizedPath, childController); + Ember.assert("Could not find view for path: " + normalizedPath, childView); set(childController, 'target', controller); set(childController, 'model', model); @@ -28372,10 +33799,32 @@ Ember.ControllerMixin.reopen({ @method transitionTo */ transitionTo: function() { - + Ember.deprecate("transitionTo is deprecated. Please use transitionToRoute."); return this.transitionToRoute.apply(this, arguments); }, + /** + Alternative to `transitionToRoute`. Transition the application into another route. The route may + be either a single route or route path: + + ```javascript + aController.replaceRoute('blogPosts'); + aController.replaceRoute('blogPosts.recentEntries'); + ``` + + Optionally supply a model for the route in question. The model + will be serialized into the URL using the `serialize` hook of + the route: + + ```javascript + aController.replaceRoute('blogPost', aPost); + ``` + + @param {String} name the name of the route + @param {...Object} models the + @for Ember.ControllerMixin + @method replaceRoute + */ replaceRoute: function() { // target may be either another controller or a router var target = get(this, 'target'), @@ -28389,7 +33838,7 @@ Ember.ControllerMixin.reopen({ @method replaceWith */ replaceWith: function() { - + Ember.deprecate("replaceWith is deprecated. Please use replaceRoute."); return this.replaceRoute.apply(this, arguments); } }); @@ -28407,11 +33856,44 @@ Ember.ControllerMixin.reopen({ var get = Ember.get, set = Ember.set; Ember.View.reopen({ + + /** + Sets the private `_outlets` object on the view. + + @method init + */ init: function() { set(this, '_outlets', {}); this._super(); }, + /** + Manually fill any of a view's `{{outlet}}` areas with the + supplied view. + + Example + + ```javascript + var MyView = Ember.View.extend({ + template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') + }); + var myView = MyView.create(); + myView.appendTo('body'); + // The html for myView now looks like: + // <div id="ember228" class="ember-view">Child view: </div> + + myView.connectOutlet('main', Ember.View.extend({ + template: Ember.Handlebars.compile('<h1>Foo</h1> ') + })); + // The html for myView now looks like: + // <div id="ember228" class="ember-view">Child view: + // <div id="ember234" class="ember-view"><h1>Foo</h1> </div> + // </div> + ``` + @method connectOutlet + @param {String} outletName A unique name for the outlet + @param {Object} view An Ember.View + */ connectOutlet: function(outletName, view) { if (this._pendingDisconnections) { delete this._pendingDisconnections[outletName]; @@ -28434,6 +33916,18 @@ Ember.View.reopen({ } }, + /** + @private + + Determines if the view has already been created by checking if + the view has the same constructor, template, and context as the + view in the `_outlets` object. + + @method _hasEquivalentView + @param {String} outletName The name of the outlet we are checking + @param {Object} view An Ember.View + @return {Boolean} + */ _hasEquivalentView: function(outletName, view) { var existingView = get(this, '_outlets.'+outletName); return existingView && @@ -28442,6 +33936,36 @@ Ember.View.reopen({ existingView.get('context') === view.get('context'); }, + /** + Removes an outlet from the view. + + Example + + ```javascript + var MyView = Ember.View.extend({ + template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') + }); + var myView = MyView.create(); + myView.appendTo('body'); + // myView's html: + // <div id="ember228" class="ember-view">Child view: </div> + + myView.connectOutlet('main', Ember.View.extend({ + template: Ember.Handlebars.compile('<h1>Foo</h1> ') + })); + // myView's html: + // <div id="ember228" class="ember-view">Child view: + // <div id="ember234" class="ember-view"><h1>Foo</h1> </div> + // </div> + + myView.disconnectOutlet('main'); + // myView's html: + // <div id="ember228" class="ember-view">Child view: </div> + ``` + + @method disconnectOutlet + @param {String} outletName The name of the outlet to be removed + */ disconnectOutlet: function(outletName) { if (!this._pendingDisconnections) { this._pendingDisconnections = {}; @@ -28450,6 +33974,14 @@ Ember.View.reopen({ Ember.run.once(this, '_finishDisconnections'); }, + /** + @private + + Gets an outlet that is pending disconnection and then + nullifys the object on the `_outlet` object. + + @method _finishDisconnections + */ _finishDisconnections: function() { var outlets = get(this, '_outlets'); var pendingDisconnections = this._pendingDisconnections; @@ -28473,7 +34005,7 @@ Ember.View.reopen({ // Add a new named queue after the 'actions' queue (where RSVP promises // resolve), which is used in router transitions to prevent unnecessary -// loading state entry if all context promises resolve on the +// loading state entry if all context promises resolve on the // 'actions' queue first. var queues = Ember.run.queues, @@ -28520,16 +34052,51 @@ var get = Ember.get, set = Ember.set; @static */ Ember.Location = { + /** + Create an instance of a an implementation of the `location` API. Requires + an options object with an `implementation` property. + + Example + + ```javascript + var hashLocation = Ember.Location.create({implementation: 'hash'}); + var historyLocation = Ember.Location.create({implementation: 'history'}); + var noneLocation = Ember.Location.create({implementation: 'none'}); + ``` + + @method create + @param {Object} options + @return {Object} an instance of an implementation of the `location` API + */ create: function(options) { var implementation = options && options.implementation; - + Ember.assert("Ember.Location.create: you must specify a 'implementation' option", !!implementation); var implementationClass = this.implementations[implementation]; - + Ember.assert("Ember.Location.create: " + implementation + " is not a valid implementation", !!implementationClass); return implementationClass.create.apply(implementationClass, arguments); }, + /** + Registers a class that implements the `location` API with an implementation + name. This implementation name can then be specified by the location property on + the application's router class. + + Example + + ```javascript + Ember.Location.registerImplementation('history', Ember.HistoryLocation); + + App.Router.reopen({ + location: 'history' + }); + ``` + + @method registerImplementation + @param {String} name + @param {Object} implementation of the `location` API + */ registerImplementation: function(name, implementation) { this.implementations[name] = implementation; }, @@ -28562,23 +34129,71 @@ var get = Ember.get, set = Ember.set; Ember.NoneLocation = Ember.Object.extend({ path: '', + /** + @private + + Returns the current path. + + @method getURL + @return {String} path + */ getURL: function() { return get(this, 'path'); }, + /** + @private + + Set the path and remembers what was set. Using this method + to change the path will not invoke the `updateURL` callback. + + @method setURL + @param path {String} + */ setURL: function(path) { set(this, 'path', path); }, + /** + @private + + Register a callback to be invoked when the path changes. These + callbacks will execute when the user presses the back or forward + button, but not after `setURL` is invoked. + + @method onUpdateURL + @param callback {Function} + */ onUpdateURL: function(callback) { this.updateCallback = callback; }, + /** + @private + + Sets the path and calls the `updateURL` callback. + + @method handleURL + @param callback {Function} + */ handleURL: function(url) { set(this, 'path', url); this.updateCallback(url); }, + /** + @private + + Given a URL, formats it to be placed into the page as part + of an element's `href` attribute. + + This is used, for example, when using the {{action}} helper + to generate a URL based on an event. + + @method formatURL + @param url {String} + @return {String} url + */ formatURL: function(url) { // The return value is not overly meaningful, but we do not want to throw // errors when test code renders templates containing {{action href=true}} @@ -28642,6 +34257,19 @@ Ember.HashLocation = Ember.Object.extend({ set(this, 'lastSetURL', path); }, + /** + @private + + Uses location.replace to update the url without a page reload + or history modification. + + @method replaceURL + @param path {String} + */ + replaceURL: function(path) { + get(this, 'location').replace('#' + path); + }, + /** @private @@ -28684,6 +34312,13 @@ Ember.HashLocation = Ember.Object.extend({ return '#'+url; }, + /** + @private + + Cleans up the HashLocation event listener. + + @method willDestroy + */ willDestroy: function() { var guid = Ember.guidFor(this); @@ -28748,6 +34383,7 @@ Ember.HistoryLocation = Ember.Object.extend({ Returns the current `location.pathname` without rootURL @method getURL + @return url {String} */ getURL: function() { var rootURL = get(this, 'rootURL'), @@ -28802,6 +34438,7 @@ Ember.HistoryLocation = Ember.Object.extend({ from a private _historyState variable @method getState + @return state {Object} */ getState: function() { return supportsHistoryState ? get(this, 'history').state : this._historyState; @@ -28881,6 +34518,7 @@ Ember.HistoryLocation = Ember.Object.extend({ @method formatURL @param url {String} + @return formatted url {String} */ formatURL: function(url) { var rootURL = get(this, 'rootURL'); @@ -28892,6 +34530,13 @@ Ember.HistoryLocation = Ember.Object.extend({ return rootURL + url; }, + /** + @private + + Cleans up the HistoryLocation event listener. + + @method willDestroy + */ willDestroy: function() { var guid = Ember.guidFor(this); @@ -28917,7 +34562,6 @@ Ember Routing @module ember @submodule ember-routing -@requires ember-states @requires ember-views */ @@ -29048,11 +34692,11 @@ var get = Ember.get, container lookups before consulting the container for registered items: - * templates are looked up on `Ember.TEMPLATES` - * other names are looked up on the application after converting - the name. For example, `controller:post` looks up - `App.PostController` by default. - * there are some nuances (see examples below) +* templates are looked up on `Ember.TEMPLATES` +* other names are looked up on the application after converting + the name. For example, `controller:post` looks up + `App.PostController` by default. +* there are some nuances (see examples below) ### How Resolving Works @@ -29074,7 +34718,7 @@ var get = Ember.get, ```javascript App = Ember.Application.create({ - resolver: Ember.DefaultResolver.extend({ + Resolver: Ember.DefaultResolver.extend({ resolveTemplate: function(parsedName) { var resolvedTemplate = this._super(parsedName); if (resolvedTemplate) { return resolvedTemplate; } @@ -29121,6 +34765,32 @@ Ember.DefaultResolver = Ember.Object.extend({ @property namespace */ namespace: null, + + normalize: function(fullName) { + var split = fullName.split(':', 2), + type = split[0], + name = split[1]; + + Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2); + + if (type !== 'template') { + var result = name; + + if (result.indexOf('.') > -1) { + result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } + + if (name.indexOf('_') > -1) { + result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } + + return type + ':' + result; + } else { + return fullName; + } + }, + + /** This method is called via the container's resolver method. It parses the provided `fullName` and then looks up and @@ -29150,6 +34820,7 @@ Ember.DefaultResolver = Ember.Object.extend({ broken out. @protected + @param {String} fullName the lookup string @method parseName */ parseName: function(fullName) { @@ -29165,6 +34836,7 @@ Ember.DefaultResolver = Ember.Object.extend({ var namespaceName = capitalize(parts.slice(0, -1).join('.')); root = Ember.Namespace.byName(namespaceName); + Ember.assert('You are looking for a ' + name + ' ' + type + ' in the ' + namespaceName + ' namespace, but the namespace could not be found', root); } return { @@ -29180,6 +34852,8 @@ Ember.DefaultResolver = Ember.Object.extend({ Look up the template in Ember.TEMPLATES @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveTemplate */ resolveTemplate: function(parsedName) { @@ -29199,6 +34873,8 @@ Ember.DefaultResolver = Ember.Object.extend({ the conventions expected by `Ember.Router` @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method useRouterNaming */ useRouterNaming: function(parsedName) { @@ -29208,7 +34884,11 @@ Ember.DefaultResolver = Ember.Object.extend({ } }, /** + Lookup the controller using `resolveOther` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveController */ resolveController: function(parsedName) { @@ -29216,7 +34896,11 @@ Ember.DefaultResolver = Ember.Object.extend({ return this.resolveOther(parsedName); }, /** + Lookup the route using `resolveOther` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveRoute */ resolveRoute: function(parsedName) { @@ -29224,7 +34908,11 @@ Ember.DefaultResolver = Ember.Object.extend({ return this.resolveOther(parsedName); }, /** + Lookup the view using `resolveOther` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveView */ resolveView: function(parsedName) { @@ -29233,7 +34921,11 @@ Ember.DefaultResolver = Ember.Object.extend({ }, /** + Lookup the model on the Application namespace + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveModel */ resolveModel: function(parsedName) { @@ -29247,6 +34939,8 @@ Ember.DefaultResolver = Ember.Object.extend({ namespace (usually on the Application) @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveOther */ resolveOther: function(parsedName) { @@ -29255,8 +34949,18 @@ Ember.DefaultResolver = Ember.Object.extend({ if (factory) { return factory; } }, - lookupDescription: function(name) { - var parsedName = this.parseName(name); + /** + Returns a human-readable description for a fullName. Used by the + Application namespace in assertions to describe the + precise name of the class that Ember is looking for, rather than + container keys. + + @protected + @param {String} fullName the lookup string + @method lookupDescription + */ + lookupDescription: function(fullName) { + var parsedName = this.parseName(fullName); if (parsedName.type === 'template') { return "template at " + parsedName.fullNameWithoutType.replace(/\./g, '/'); @@ -29266,6 +34970,10 @@ Ember.DefaultResolver = Ember.Object.extend({ if (parsedName.type !== 'model') { description += classify(parsedName.type); } return description; + }, + + makeToString: function(factory, fullName) { + return factory.toString(); } }); @@ -29289,6 +34997,7 @@ DeprecatedContainer.deprecate = function(method) { return function() { var container = this._container; + Ember.deprecate('Using the defaultContainer is no longer supported. [defaultContainer#' + method + '] see: http://git.io/EKPpnA', false); return container[method].apply(container, arguments); }; }; @@ -29411,9 +35120,9 @@ DeprecatedContainer.prototype = { In addition to creating your application's router, `Ember.Application` is also responsible for telling the router when to start routing. Transitions - between routes can be logged with the LOG_TRANSITIONS flag, and more + between routes can be logged with the `LOG_TRANSITIONS` flag, and more detailed intra-transition logging can be logged with - the LOG_TRANSITIONS_INTERNAL flag: + the `LOG_TRANSITIONS_INTERNAL` flag: ```javascript window.App = Ember.Application.create({ @@ -29514,13 +35223,19 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin this.scheduleInitialize(); - if (Ember.LOG_VERSION) { + Ember.libraries.registerCoreLibrary('Handlebars', Ember.Handlebars.VERSION); + Ember.libraries.registerCoreLibrary('jQuery', Ember.$().jquery); + + if ( Ember.LOG_VERSION ) { Ember.LOG_VERSION = false; // we only need to see this once per Application#init + var maxNameLength = Math.max.apply(this, Ember.A(Ember.libraries).mapBy("name.length")); - - - - + Ember.debug('-------------------------------'); + Ember.libraries.each(function(name, version) { + var spaces = new Array(maxNameLength - name.length + 1).join(" "); + Ember.debug([name, spaces, ' : ', version].join("")); + }); + Ember.debug('-------------------------------'); } }, @@ -29623,15 +35338,21 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin @method deferReadiness */ deferReadiness: function() { - + Ember.assert("You must call deferReadiness on an instance of Ember.Application", this instanceof Ember.Application); + Ember.assert("You cannot defer readiness since the `ready()` hook has already been called.", this._readinessDeferrals > 0); this._readinessDeferrals++; }, /** + Call `advanceReadiness` after any asynchronous setup logic has completed. + Each call to `deferReadiness` must be matched by a call to `advanceReadiness` + or the application will never become ready and routing will not begin. + @method advanceReadiness @see {Ember.Application#deferReadiness} */ advanceReadiness: function() { + Ember.assert("You must call advanceReadiness on an instance of Ember.Application", this instanceof Ember.Application); this._readinessDeferrals--; if (this._readinessDeferrals === 0) { @@ -29647,19 +35368,20 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin ```javascript App = Ember.Application.create(); - App.Person = Ember.Object.extend({}); - App.Orange = Ember.Object.extend({}); - App.Email = Ember.Object.extend({}); + App.Person = Ember.Object.extend({}); + App.Orange = Ember.Object.extend({}); + App.Email = Ember.Object.extend({}); + App.Session = Ember.Object.create({}); App.register('model:user', App.Person, {singleton: false }); App.register('fruit:favorite', App.Orange); App.register('communication:main', App.Email, {singleton: false}); + App.register('session', App.Session, {instantiate: false}); ``` @method register - @param type {String} - @param name {String} - @param factory {String} + @param fullName {String} type:name (e.g., 'model:user') + @param factory {Function} (e.g., App.Person) @param options {String} (optional) **/ register: function() { @@ -29699,7 +35421,7 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin @method initialize **/ initialize: function() { - + Ember.deprecate('Calling initialize manually is not supported. Please see Ember.Application#advanceReadiness and Ember.Application#deferReadiness'); }, /** @private @@ -29835,7 +35557,7 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin graph.topsort(function (vertex) { var initializer = vertex.value; - + Ember.assert("No application initializer named '"+vertex.name+"'", initializer); initializer(container, namespace); }); }, @@ -29879,8 +35601,8 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin /** @private - If the application has a router, use it to route to the current URL, and trigger a new call to `route` whenever the URL changes. + If the application has a router, use it to route to the current URL, and @method startRouting @property router {Ember.Router} @@ -29907,12 +35629,21 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin ready: Ember.K, /** + @deprecated Use 'Resolver' instead Set this to provide an alternate class to `Ember.DefaultResolver` + @property resolver */ resolver: null, + /** + Set this to provide an alternate class to `Ember.DefaultResolver` + + @property resolver + */ + Resolver: null, + willDestroy: function() { Ember.BOOTED = false; @@ -29930,8 +35661,9 @@ Ember.Application.reopenClass({ initializer: function(initializer) { var initializers = get(this, 'initializers'); - - + Ember.assert("The initializer '" + initializer.name + "' has already been registered", !initializers.findBy('name', initializers.name)); + Ember.assert("An injection cannot be registered with both a before and an after", !(initializer.before && initializer.after)); + Ember.assert("An injection cannot be registered without an injection function", Ember.canInvoke(initializer, 'initialize')); initializers.push(initializer); }, @@ -29968,9 +35700,12 @@ Ember.Application.reopenClass({ Ember.Container.defaultContainer = new DeprecatedContainer(container); container.set = Ember.set; - container.normalize = normalize; - container.resolver = resolverFor(namespace); - container.describe = container.resolver.describe; + container.resolver = resolverFor(namespace); + container.normalize = container.resolver.normalize; + container.describe = container.resolver.describe; + container.makeToString = container.resolver.makeToString; + + container.optionsForType('component', { singleton: false }); container.optionsForType('view', { singleton: false }); container.optionsForType('template', { instantiate: false }); container.register('application:main', namespace, { instantiate: false }); @@ -30010,8 +35745,12 @@ Ember.Application.reopenClass({ @return {*} the resolved value for a given lookup */ function resolverFor(namespace) { - var resolverClass = namespace.get('resolver') || Ember.DefaultResolver; - var resolver = resolverClass.create({ + if (namespace.get('resolver')) { + Ember.deprecate('Application.resolver is deprecated in favor of Application.Resolver', false); + } + + var ResolverClass = namespace.get('resolver') || namespace.get('Resolver') || Ember.DefaultResolver; + var resolver = ResolverClass.create({ namespace: namespace }); @@ -30023,32 +35762,22 @@ function resolverFor(namespace) { return resolver.lookupDescription(fullName); }; + resolve.makeToString = function(factory, fullName) { + return resolver.makeToString(factory, fullName); + }; + + resolve.normalize = function(fullName) { + if (resolver.normalize) { + return resolver.normalize(fullName); + } else { + Ember.deprecate('The Resolver should now provide a \'normalize\' function', false); + return fullName; + } + }; + return resolve; } -function normalize(fullName) { - var split = fullName.split(':', 2), - type = split[0], - name = split[1]; - - - if (type !== 'template') { - var result = name; - - if (result.indexOf('.') > -1) { - result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); - } - - if (name.indexOf('_') > -1) { - result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); - } - - return type + ':' + result; - } else { - return fullName; - } -} - Ember.runLoadHooks('Ember.Application', Ember.Application); })(); @@ -30068,44 +35797,27 @@ Ember.runLoadHooks('Ember.Application', Ember.Application); */ var get = Ember.get, set = Ember.set; -var ControllersProxy = Ember.Object.extend({ - controller: null, - unknownProperty: function(controllerName) { - var controller = get(this, 'controller'), - needs = get(controller, 'needs'), - container = controller.get('container'), - dependency; +function verifyNeedsDependencies(controller, container, needs) { + var dependency, i, l; - for (var i=0, l=needs.length; i<l; i++) { - dependency = needs[i]; - if (dependency === controllerName) { - return container.lookup('controller:' + controllerName); - } - } - } -}); - -function verifyDependencies(controller) { - var needs = get(controller, 'needs'), - container = get(controller, 'container'), - dependency, satisfied = true; - - for (var i=0, l=needs.length; i<l; i++) { + for (i=0, l=needs.length; i<l; i++) { dependency = needs[i]; if (dependency.indexOf(':') === -1) { dependency = "controller:" + dependency; } + // Structure assert to still do verification but not string concat in production if (!container.has(dependency)) { - satisfied = false; - + Ember.assert(Ember.inspect(controller) + " needs " + dependency + " but it does not exist", false); } } - - return satisfied; } +/** + @class ControllerMixin + @namespace Ember +*/ Ember.ControllerMixin.reopen({ concatenatedProperties: ['needs'], @@ -30138,22 +35850,64 @@ Ember.ControllerMixin.reopen({ needs: [], init: function() { - this._super.apply(this, arguments); + var needs = get(this, 'needs'), + length = get(needs, 'length'); - // Structure asserts to still do verification but not string concat in production - if (!verifyDependencies(this)) { + if (length > 0) { + verifyNeedsDependencies(this, this.container, needs); + // if needs then initialize controllers proxy + get(this, 'controllers'); } + + this._super.apply(this, arguments); }, controllerFor: function(controllerName) { - + Ember.deprecate("Controller#controllerFor is deprecated, please use Controller#needs instead"); return Ember.controllerFor(get(this, 'container'), controllerName); }, + /** + Stores the instances of other controllers available from within + this controller. Any controller listed by name in the `needs` + property will be accessible by name through this property. + + ```javascript + App.CommentsController = Ember.ArrayController.extend({ + needs: ['post'], + postTitle: function(){ + var currentPost = this.get('controllers.post'); // instance of App.PostController + return currentPost.get('title'); + }.property('controllers.post.title') + }); + ``` + + @see {Ember.ControllerMixin#needs} + @property {Object} controllers + @default null + */ controllers: Ember.computed(function() { - return ControllersProxy.create({ controller: this }); - }) + var controller = this; + + return { + needs: get(controller, 'needs'), + container: get(controller, 'container'), + unknownProperty: function(controllerName) { + var needs = this.needs, + dependency, i, l; + for (i=0, l=needs.length; i<l; i++) { + dependency = needs[i]; + if (dependency === controllerName) { + return this.container.lookup('controller:' + controllerName); + } + } + + var errorMessage = Ember.inspect(controller) + '#needs does not include `' + controllerName + '`. To access the ' + controllerName + ' controller from ' + Ember.inspect(controller) + ', ' + Ember.inspect(controller) + ' should have a `needs` property that is an array of the controllers it has access to.'; + throw new ReferenceError(errorMessage); + } + }; + }).readOnly() }); })(); @@ -30172,1257 +35926,706 @@ Ember Application @module ember @submodule ember-application -@requires ember-views, ember-states, ember-routing +@requires ember-views, ember-routing */ })(); (function() { -var get = Ember.get, set = Ember.set; - /** @module ember -@submodule ember-states +@submodule ember-extension-support */ - /** - @class State + The `DataAdapter` helps a data persistence library + interface with tools that debug Ember such + as the Chrome Ember Extension. + + This class will be extended by a persistence library + which will override some of the methods with + library-specific code. + + The methods likely to be overriden are + `getFilters`, `detect`, `columnsForType`, + `getRecords`, `getRecordColumnValues`, + `getRecordKeywords`, `getRecordFilterValues`, + `getRecordColor`, `observeRecord` + + The adapter will need to be registered + in the application's container as `dataAdapter:main` + + Example: + ```javascript + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DS.DataAdapter); + } + }); + ``` + + @class DataAdapter @namespace Ember @extends Ember.Object - @uses Ember.Evented */ -Ember.State = Ember.Object.extend(Ember.Evented, -/** @scope Ember.State.prototype */{ - /** - A reference to the parent state. - - @property parentState - @type Ember.State - */ - parentState: null, - start: null, - - /** - The name of this state. - - @property name - @type String - */ - name: null, - - /** - The full path to this state. - - @property path - @type String - */ - path: Ember.computed(function() { - var parentPath = get(this, 'parentState.path'), - path = get(this, 'name'); - - if (parentPath) { - path = parentPath + '.' + path; - } - - return path; - }), - - /** - @private - - Override the default event firing from `Ember.Evented` to - also call methods with the given name. - - @method trigger - @param name - */ - trigger: function(name) { - if (this[name]) { - this[name].apply(this, [].slice.call(arguments, 1)); - } - this._super.apply(this, arguments); - }, - - init: function() { - var states = get(this, 'states'); - set(this, 'childStates', Ember.A()); - set(this, 'eventTransitions', get(this, 'eventTransitions') || {}); - - var name, value, transitionTarget; - - // As a convenience, loop over the properties - // of this state and look for any that are other - // Ember.State instances or classes, and move them - // to the `states` hash. This avoids having to - // create an explicit separate hash. - - if (!states) { - states = {}; - - for (name in this) { - if (name === "constructor") { continue; } - - if (value = this[name]) { - if (transitionTarget = value.transitionTarget) { - this.eventTransitions[name] = transitionTarget; - } - - this.setupChild(states, name, value); - } - } - - set(this, 'states', states); - } else { - for (name in states) { - this.setupChild(states, name, states[name]); - } - } - - // pathsCaches is a nested hash of the form: - // pathsCaches[stateManagerTypeGuid][path] == transitions_hash - set(this, 'pathsCaches', {}); - }, - - setPathsCache: function(stateManager, path, transitions) { - var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), - pathsCaches = get(this, 'pathsCaches'), - pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; - - pathsCacheForManager[path] = transitions; - pathsCaches[stateManagerTypeGuid] = pathsCacheForManager; - }, - - getPathsCache: function(stateManager, path) { - var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), - pathsCaches = get(this, 'pathsCaches'), - pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; - - return pathsCacheForManager[path]; - }, - - setupChild: function(states, name, value) { - if (!value) { return false; } - var instance; - - if (value instanceof Ember.State) { - set(value, 'name', name); - instance = value; - instance.container = this.container; - } else if (Ember.State.detect(value)) { - instance = value.create({ - name: name, - container: this.container - }); - } - - if (instance instanceof Ember.State) { - set(instance, 'parentState', this); - get(this, 'childStates').pushObject(instance); - states[name] = instance; - return instance; - } - }, - - lookupEventTransition: function(name) { - var path, state = this; - - while(state && !path) { - path = state.eventTransitions[name]; - state = state.get('parentState'); - } - - return path; - }, - - /** - A Boolean value indicating whether the state is a leaf state - in the state hierarchy. This is `false` if the state has child - states; otherwise it is true. - - @property isLeaf - @type Boolean - */ - isLeaf: Ember.computed(function() { - return !get(this, 'childStates').length; - }), - - /** - A boolean value indicating whether the state takes a context. - By default we assume all states take contexts. - - @property hasContext - @default true - */ - hasContext: true, - - /** - This is the default transition event. - - @event setup - @param {Ember.StateManager} manager - @param context - @see Ember.StateManager#transitionEvent - */ - setup: Ember.K, - - /** - This event fires when the state is entered. - - @event enter - @param {Ember.StateManager} manager - */ - enter: Ember.K, - - /** - This event fires when the state is exited. - - @event exit - @param {Ember.StateManager} manager - */ - exit: Ember.K -}); - -Ember.State.reopenClass({ - - /** - Creates an action function for transitioning to the named state while - preserving context. - - The following example StateManagers are equivalent: - - ```javascript - aManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: Ember.State.transitionTo('stateTwo') - }), - stateTwo: Ember.State.create({}) - }) - - bManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: function(manager, context) { - manager.transitionTo('stateTwo', context) - } - }), - stateTwo: Ember.State.create({}) - }) - ``` - - @method transitionTo - @static - @param {String} target - */ - - transitionTo: function(target) { - - var transitionFunction = function(stateManager, contextOrEvent) { - var contexts = [], - Event = Ember.$ && Ember.$.Event; - - if (contextOrEvent && (Event && contextOrEvent instanceof Event)) { - if (contextOrEvent.hasOwnProperty('contexts')) { - contexts = contextOrEvent.contexts.slice(); - } - } - else { - contexts = [].slice.call(arguments, 1); - } - - contexts.unshift(target); - stateManager.transitionTo.apply(stateManager, contexts); - }; - - transitionFunction.transitionTarget = target; - - return transitionFunction; - } - -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-states -*/ - -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; -var arrayForEach = Ember.ArrayPolyfills.forEach; -/** - A Transition takes the enter, exit and resolve states and normalizes - them: - - * takes any passed in contexts into consideration - * adds in `initialState`s - - @class Transition - @private -*/ -var Transition = function(raw) { - this.enterStates = raw.enterStates.slice(); - this.exitStates = raw.exitStates.slice(); - this.resolveState = raw.resolveState; - - this.finalState = raw.enterStates[raw.enterStates.length - 1] || raw.resolveState; -}; - -Transition.prototype = { - /** - Normalize the passed in enter, exit and resolve states. - - This process also adds `finalState` and `contexts` to the Transition object. - - @method normalize - @param {Ember.StateManager} manager the state manager running the transition - @param {Array} contexts a list of contexts passed into `transitionTo` - */ - normalize: function(manager, contexts) { - this.matchContextsToStates(contexts); - this.addInitialStates(); - this.removeUnchangedContexts(manager); - return this; - }, - - /** - Match each of the contexts passed to `transitionTo` to a state. - This process may also require adding additional enter and exit - states if there are more contexts than enter states. - - @method matchContextsToStates - @param {Array} contexts a list of contexts passed into `transitionTo` - */ - matchContextsToStates: function(contexts) { - var stateIdx = this.enterStates.length - 1, - matchedContexts = [], - state, - context; - - // Next, we will match the passed in contexts to the states they - // represent. - // - // First, assign a context to each enter state in reverse order. If - // any contexts are left, add a parent state to the list of states - // to enter and exit, and assign a context to the parent state. - // - // If there are still contexts left when the state manager is - // reached, raise an exception. - // - // This allows the following: - // - // |- root - // | |- post - // | | |- comments - // | |- about (* current state) - // - // For `transitionTo('post.comments', post, post.get('comments')`, - // the first context (`post`) will be assigned to `root.post`, and - // the second context (`post.get('comments')`) will be assigned - // to `root.post.comments`. - // - // For the following: - // - // |- root - // | |- post - // | | |- index (* current state) - // | | |- comments - // - // For `transitionTo('post.comments', otherPost, otherPost.get('comments')`, - // the `<root.post>` state will be added to the list of enter and exit - // states because its context has changed. - - while (contexts.length > 0) { - if (stateIdx >= 0) { - state = this.enterStates[stateIdx--]; - } else { - if (this.enterStates.length) { - state = get(this.enterStates[0], 'parentState'); - if (!state) { throw "Cannot match all contexts to states"; } - } else { - // If re-entering the current state with a context, the resolve - // state will be the current state. - state = this.resolveState; - } - - this.enterStates.unshift(state); - this.exitStates.unshift(state); - } - - // in routers, only states with dynamic segments have a context - if (get(state, 'hasContext')) { - context = contexts.pop(); - } else { - context = null; - } - - matchedContexts.unshift(context); - } - - this.contexts = matchedContexts; - }, - - /** - Add any `initialState`s to the list of enter states. - - @method addInitialStates - */ - addInitialStates: function() { - var finalState = this.finalState, initialState; - - while(true) { - initialState = get(finalState, 'initialState') || 'start'; - finalState = get(finalState, 'states.' + initialState); - - if (!finalState) { break; } - - this.finalState = finalState; - this.enterStates.push(finalState); - this.contexts.push(undefined); - } - }, - - /** - Remove any states that were added because the number of contexts - exceeded the number of explicit enter states, but the context has - not changed since the last time the state was entered. - - @method removeUnchangedContexts - @param {Ember.StateManager} manager passed in to look up the last - context for a states - */ - removeUnchangedContexts: function(manager) { - // Start from the beginning of the enter states. If the state was added - // to the list during the context matching phase, make sure the context - // has actually changed since the last time the state was entered. - while (this.enterStates.length > 0) { - if (this.enterStates[0] !== this.exitStates[0]) { break; } - - if (this.enterStates.length === this.contexts.length) { - if (manager.getStateMeta(this.enterStates[0], 'context') !== this.contexts[0]) { break; } - this.contexts.shift(); - } - - this.resolveState = this.enterStates.shift(); - this.exitStates.shift(); - } - } -}; - -var sendRecursively = function(event, currentState, isUnhandledPass) { - var log = this.enableLogging, - eventName = isUnhandledPass ? 'unhandledEvent' : event, - action = currentState[eventName], - contexts, sendRecursiveArguments, actionArguments; - - contexts = [].slice.call(arguments, 3); - - // Test to see if the action is a method that - // can be invoked. Don't blindly check just for - // existence, because it is possible the state - // manager has a child state of the given name, - // and we should still raise an exception in that - // case. - if (typeof action === 'function') { - if (log) { - if (isUnhandledPass) { - Ember.Logger.log(fmt("STATEMANAGER: Unhandled event '%@' being sent to state %@.", [event, get(currentState, 'path')])); - } else { - Ember.Logger.log(fmt("STATEMANAGER: Sending event '%@' to state %@.", [event, get(currentState, 'path')])); - } - } - - actionArguments = contexts; - if (isUnhandledPass) { - actionArguments.unshift(event); - } - actionArguments.unshift(this); - - return action.apply(currentState, actionArguments); - } else { - var parentState = get(currentState, 'parentState'); - if (parentState) { - - sendRecursiveArguments = contexts; - sendRecursiveArguments.unshift(event, parentState, isUnhandledPass); - - return sendRecursively.apply(this, sendRecursiveArguments); - } else if (!isUnhandledPass) { - return sendEvent.call(this, event, contexts, true); - } - } -}; - -var sendEvent = function(eventName, sendRecursiveArguments, isUnhandledPass) { - sendRecursiveArguments.unshift(eventName, get(this, 'currentState'), isUnhandledPass); - return sendRecursively.apply(this, sendRecursiveArguments); -}; - -/** - StateManager is part of Ember's implementation of a finite state machine. A - StateManager instance manages a number of properties that are instances of - `Ember.State`, - tracks the current active state, and triggers callbacks when states have changed. - - ## Defining States - - The states of StateManager can be declared in one of two ways. First, you can - define a `states` property that contains all the states: - - ```javascript - managerA = Ember.StateManager.create({ - states: { - stateOne: Ember.State.create(), - stateTwo: Ember.State.create() - } - }) - - managerA.get('states') - // { - // stateOne: Ember.State.create(), - // stateTwo: Ember.State.create() - // } - ``` - - You can also add instances of `Ember.State` (or an `Ember.State` subclass) - directly as properties of a StateManager. These states will be collected into - the `states` property for you. - - ```javascript - managerA = Ember.StateManager.create({ - stateOne: Ember.State.create(), - stateTwo: Ember.State.create() - }) - - managerA.get('states') - // { - // stateOne: Ember.State.create(), - // stateTwo: Ember.State.create() - // } - ``` - - ## The Initial State - - When created a StateManager instance will immediately enter into the state - defined as its `start` property or the state referenced by name in its - `initialState` property: - - ```javascript - managerA = Ember.StateManager.create({ - start: Ember.State.create({}) - }) - - managerA.get('currentState.name') // 'start' - - managerB = Ember.StateManager.create({ - initialState: 'beginHere', - beginHere: Ember.State.create({}) - }) - - managerB.get('currentState.name') // 'beginHere' - ``` - - Because it is a property you may also provide a computed function if you wish - to derive an `initialState` programmatically: - - ```javascript - managerC = Ember.StateManager.create({ - initialState: function() { - if (someLogic) { - return 'active'; - } else { - return 'passive'; - } - }.property(), - active: Ember.State.create({}), - passive: Ember.State.create({}) - }) - ``` - - ## Moving Between States - - A StateManager can have any number of `Ember.State` objects as properties - and can have a single one of these states as its current state. - - Calling `transitionTo` transitions between states: - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({}), - poweredUp: Ember.State.create({}) - }) - - robotManager.get('currentState.name') // 'poweredDown' - robotManager.transitionTo('poweredUp') - robotManager.get('currentState.name') // 'poweredUp' - ``` - - Before transitioning into a new state the existing `currentState` will have - its `exit` method called with the StateManager instance as its first argument - and an object representing the transition as its second argument. - - After transitioning into a new state the new `currentState` will have its - `enter` method called with the StateManager instance as its first argument - and an object representing the transition as its second argument. - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - exit: function(stateManager) { - console.log("exiting the poweredDown state") - } - }), - poweredUp: Ember.State.create({ - enter: function(stateManager) { - console.log("entering the poweredUp state. Destroy all humans.") - } - }) - }) - - robotManager.get('currentState.name') // 'poweredDown' - robotManager.transitionTo('poweredUp') - - // will log - // 'exiting the poweredDown state' - // 'entering the poweredUp state. Destroy all humans.' - ``` - - Once a StateManager is already in a state, subsequent attempts to enter that - state will not trigger enter or exit method calls. Attempts to transition - into a state that the manager does not have will result in no changes in the - StateManager's current state: - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - exit: function(stateManager) { - console.log("exiting the poweredDown state") - } - }), - poweredUp: Ember.State.create({ - enter: function(stateManager) { - console.log("entering the poweredUp state. Destroy all humans.") - } - }) - }) - - robotManager.get('currentState.name') // 'poweredDown' - robotManager.transitionTo('poweredUp') - // will log - // 'exiting the poweredDown state' - // 'entering the poweredUp state. Destroy all humans.' - robotManager.transitionTo('poweredUp') // no logging, no state change - - robotManager.transitionTo('someUnknownState') // silently fails - robotManager.get('currentState.name') // 'poweredUp' - ``` - - Each state property may itself contain properties that are instances of - `Ember.State`. The StateManager can transition to specific sub-states in a - series of transitionTo method calls or via a single transitionTo with the - full path to the specific state. The StateManager will also keep track of the - full path to its currentState - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - charging: Ember.State.create(), - charged: Ember.State.create() - }), - poweredUp: Ember.State.create({ - mobile: Ember.State.create(), - stationary: Ember.State.create() - }) - }) - - robotManager.get('currentState.name') // 'poweredDown' - - robotManager.transitionTo('poweredUp') - robotManager.get('currentState.name') // 'poweredUp' - - robotManager.transitionTo('mobile') - robotManager.get('currentState.name') // 'mobile' - - // transition via a state path - robotManager.transitionTo('poweredDown.charging') - robotManager.get('currentState.name') // 'charging' - - robotManager.get('currentState.path') // 'poweredDown.charging' - ``` - - Enter transition methods will be called for each state and nested child state - in their hierarchical order. Exit methods will be called for each state and - its nested states in reverse hierarchical order. - - Exit transitions for a parent state are not called when entering into one of - its child states, only when transitioning to a new section of possible states - in the hierarchy. - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - enter: function() {}, - exit: function() { - console.log("exited poweredDown state") - }, - charging: Ember.State.create({ - enter: function() {}, - exit: function() {} - }), - charged: Ember.State.create({ - enter: function() { - console.log("entered charged state") - }, - exit: function() { - console.log("exited charged state") - } - }) - }), - poweredUp: Ember.State.create({ - enter: function() { - console.log("entered poweredUp state") - }, - exit: function() {}, - mobile: Ember.State.create({ - enter: function() { - console.log("entered mobile state") - }, - exit: function() {} - }), - stationary: Ember.State.create({ - enter: function() {}, - exit: function() {} - }) - }) - }) - - - robotManager.get('currentState.path') // 'poweredDown' - robotManager.transitionTo('charged') - // logs 'entered charged state' - // but does *not* log 'exited poweredDown state' - robotManager.get('currentState.name') // 'charged - - robotManager.transitionTo('poweredUp.mobile') - // logs - // 'exited charged state' - // 'exited poweredDown state' - // 'entered poweredUp state' - // 'entered mobile state' - ``` - - During development you can set a StateManager's `enableLogging` property to - `true` to receive console messages of state transitions. - - ```javascript - robotManager = Ember.StateManager.create({ - enableLogging: true - }) - ``` - - ## Managing currentState with Actions - - To control which transitions are possible for a given state, and - appropriately handle external events, the StateManager can receive and - route action messages to its states via the `send` method. Calling to - `send` with an action name will begin searching for a method with the same - name starting at the current state and moving up through the parent states - in a state hierarchy until an appropriate method is found or the StateManager - instance itself is reached. - - If an appropriately named method is found it will be called with the state - manager as the first argument and an optional `context` object as the second - argument. - - ```javascript - managerA = Ember.StateManager.create({ - initialState: 'stateOne.substateOne.subsubstateOne', - stateOne: Ember.State.create({ - substateOne: Ember.State.create({ - anAction: function(manager, context) { - console.log("an action was called") - }, - subsubstateOne: Ember.State.create({}) - }) - }) - }) - - managerA.get('currentState.name') // 'subsubstateOne' - managerA.send('anAction') - // 'stateOne.substateOne.subsubstateOne' has no anAction method - // so the 'anAction' method of 'stateOne.substateOne' is called - // and logs "an action was called" - // with managerA as the first argument - // and no second argument - - someObject = {} - managerA.send('anAction', someObject) - // the 'anAction' method of 'stateOne.substateOne' is called again - // with managerA as the first argument and - // someObject as the second argument. - ``` - - If the StateManager attempts to send an action but does not find an appropriately named - method in the current state or while moving upwards through the state hierarchy, it will - repeat the process looking for a `unhandledEvent` method. If an `unhandledEvent` method is - found, it will be called with the original event name as the second argument. If an - `unhandledEvent` method is not found, the StateManager will throw a new Ember.Error. - - ```javascript - managerB = Ember.StateManager.create({ - initialState: 'stateOne.substateOne.subsubstateOne', - stateOne: Ember.State.create({ - substateOne: Ember.State.create({ - subsubstateOne: Ember.State.create({}), - unhandledEvent: function(manager, eventName, context) { - console.log("got an unhandledEvent with name " + eventName); - } - }) - }) - }) - - managerB.get('currentState.name') // 'subsubstateOne' - managerB.send('anAction') - // neither `stateOne.substateOne.subsubstateOne` nor any of it's - // parent states have a handler for `anAction`. `subsubstateOne` - // also does not have a `unhandledEvent` method, but its parent - // state, `substateOne`, does, and it gets fired. It will log - // "got an unhandledEvent with name anAction" - ``` - - Action detection only moves upwards through the state hierarchy from the current state. - It does not search in other portions of the hierarchy. - - ```javascript - managerC = Ember.StateManager.create({ - initialState: 'stateOne.substateOne.subsubstateOne', - stateOne: Ember.State.create({ - substateOne: Ember.State.create({ - subsubstateOne: Ember.State.create({}) - }) - }), - stateTwo: Ember.State.create({ - anAction: function(manager, context) { - // will not be called below because it is - // not a parent of the current state - } - }) - }) - - managerC.get('currentState.name') // 'subsubstateOne' - managerC.send('anAction') - // Error: <Ember.StateManager:ember132> could not - // respond to event anAction in state stateOne.substateOne.subsubstateOne. - ``` - - Inside of an action method the given state should delegate `transitionTo` calls on its - StateManager. - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown.charging', - poweredDown: Ember.State.create({ - charging: Ember.State.create({ - chargeComplete: function(manager, context) { - manager.transitionTo('charged') - } - }), - charged: Ember.State.create({ - boot: function(manager, context) { - manager.transitionTo('poweredUp') - } - }) - }), - poweredUp: Ember.State.create({ - beginExtermination: function(manager, context) { - manager.transitionTo('rampaging') - }, - rampaging: Ember.State.create() - }) - }) - - robotManager.get('currentState.name') // 'charging' - robotManager.send('boot') // throws error, no boot action - // in current hierarchy - robotManager.get('currentState.name') // remains 'charging' - - robotManager.send('beginExtermination') // throws error, no beginExtermination - // action in current hierarchy - robotManager.get('currentState.name') // remains 'charging' - - robotManager.send('chargeComplete') - robotManager.get('currentState.name') // 'charged' - - robotManager.send('boot') - robotManager.get('currentState.name') // 'poweredUp' - - robotManager.send('beginExtermination', allHumans) - robotManager.get('currentState.name') // 'rampaging' - ``` - - Transition actions can also be created using the `transitionTo` method of the `Ember.State` class. The - following example StateManagers are equivalent: - - ```javascript - aManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: Ember.State.transitionTo('stateTwo') - }), - stateTwo: Ember.State.create({}) - }) - - bManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: function(manager, context) { - manager.transitionTo('stateTwo', context) - } - }), - stateTwo: Ember.State.create({}) - }) - ``` - - @class StateManager - @namespace Ember - @extends Ember.State -**/ -Ember.StateManager = Ember.State.extend({ - /** - @private - - When creating a new statemanager, look for a default state to transition - into. This state can either be named `start`, or can be specified using the - `initialState` property. - - @method init - */ +Ember.DataAdapter = Ember.Object.extend({ init: function() { this._super(); - - set(this, 'stateMeta', Ember.Map.create()); - - var initialState = get(this, 'initialState'); - - if (!initialState && get(this, 'states.start')) { - initialState = 'start'; - } - - if (initialState) { - this.transitionTo(initialState); - - } - }, - - stateMetaFor: function(state) { - var meta = get(this, 'stateMeta'), - stateMeta = meta.get(state); - - if (!stateMeta) { - stateMeta = {}; - meta.set(state, stateMeta); - } - - return stateMeta; - }, - - setStateMeta: function(state, key, value) { - return set(this.stateMetaFor(state), key, value); - }, - - getStateMeta: function(state, key) { - return get(this.stateMetaFor(state), key); + this.releaseMethods = Ember.A(); }, /** - The current state from among the manager's possible states. This property should - not be set directly. Use `transitionTo` to move between states by name. - - @property currentState - @type Ember.State + The container of the application being debugged. + This property will be injected + on creation. */ - currentState: null, + container: null, /** - The path of the current state. Returns a string representation of the current - state. + @private - @property currentPath - @type String + Number of attributes to send + as columns. (Enough to make the record + identifiable). */ - currentPath: Ember.computed.alias('currentState.path'), + attributeLimit: 3, /** - The name of transitionEvent that this stateManager will dispatch + @private - @property transitionEvent - @type String - @default 'setup' + Stores all methods that clear observers. + These methods will be called on destruction. */ - transitionEvent: 'setup', + releaseMethods: Ember.A(), /** - If set to true, `errorOnUnhandledEvents` will cause an exception to be - raised if you attempt to send an event to a state manager that is not - handled by the current state or any of its parent states. + @public - @property errorOnUnhandledEvents - @type Boolean - @default true + Specifies how records can be filtered. + Records returned will need to have a `filterValues` + property with a key for every name in the returned array. + + @method getFilters + @return {Array} List of objects defining filters. + The object should have a `name` and `desc` property. */ - errorOnUnhandledEvent: true, - - send: function(event) { - var contexts = [].slice.call(arguments, 1); - - return sendEvent.call(this, event, contexts, false); - }, - unhandledEvent: function(manager, event) { - if (get(this, 'errorOnUnhandledEvent')) { - throw new Ember.Error(this.toString() + " could not respond to event " + event + " in state " + get(this, 'currentState.path') + "."); - } + getFilters: function() { + return Ember.A(); }, /** - Finds a state by its state path. + @public - Example: + Fetch the model types and observe them for changes. - ```javascript - manager = Ember.StateManager.create({ - root: Ember.State.create({ - dashboard: Ember.State.create() - }) + @method watchModelTypes + + @param {Function} typesAdded Callback to call to add types. + Takes an array of objects containing wrapped types (returned from `wrapModelType`). + + @param {Function} typesUpdated Callback to call when a type has changed. + Takes an array of objects containing wrapped types. + + @return {Function} Method to call to remove all observers + */ + watchModelTypes: function(typesAdded, typesUpdated) { + var modelTypes = this.getModelTypes(), + self = this, typesToSend, releaseMethods = Ember.A(); + + typesToSend = modelTypes.map(function(type) { + var wrapped = self.wrapModelType(type); + releaseMethods.push(self.observeModelType(type, typesUpdated)); + return wrapped; }); - manager.getStateByPath(manager, "root.dashboard") + typesAdded(typesToSend); - // returns the dashboard state - ``` - - @method getStateByPath - @param {Ember.State} root the state to start searching from - @param {String} path the state path to follow - @return {Ember.State} the state at the end of the path - */ - getStateByPath: function(root, path) { - var parts = path.split('.'), - state = root; - - for (var i=0, len=parts.length; i<len; i++) { - state = get(get(state, 'states'), parts[i]); - if (!state) { break; } - } - - return state; - }, - - findStateByPath: function(state, path) { - var possible; - - while (!possible && state) { - possible = this.getStateByPath(state, path); - state = get(state, 'parentState'); - } - - return possible; + var release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + self.releaseMethods.removeObject(release); + }; + this.releaseMethods.pushObject(release); + return release; }, /** - A state stores its child states in its `states` hash. - This code takes a path like `posts.show` and looks - up `root.states.posts.states.show`. + @public - It returns a list of all of the states from the - root, which is the list of states to call `enter` - on. + Fetch the records of a given type and observe them for changes. - @method getStatesInPath - @param root - @param path + @method watchRecords + + @param {Function} recordsAdded Callback to call to add records. + Takes an array of objects containing wrapped records. + The object should have the following properties: + columnValues: {Object} key and value of a table cell + object: {Object} the actual record object + + @param {Function} recordsUpdated Callback to call when a record has changed. + Takes an array of objects containing wrapped records. + + @param {Function} recordsRemoved Callback to call when a record has removed. + Takes the following parameters: + index: the array index where the records were removed + count: the number of records removed + + @return {Function} Method to call to remove all observers */ - getStatesInPath: function(root, path) { - if (!path || path === "") { return undefined; } - var parts = path.split('.'), - result = [], - states, - state; + watchRecords: function(type, recordsAdded, recordsUpdated, recordsRemoved) { + var self = this, releaseMethods = Ember.A(), records = this.getRecords(type), release; - for (var i=0, len=parts.length; i<len; i++) { - states = get(root, 'states'); - if (!states) { return undefined; } - state = get(states, parts[i]); - if (state) { root = state; result.push(state); } - else { return undefined; } - } - - return result; - }, - - goToState: function() { - // not deprecating this yet so people don't constantly need to - // make trivial changes for little reason. - return this.transitionTo.apply(this, arguments); - }, - - transitionTo: function(path, context) { - // XXX When is transitionTo called with no path - if (Ember.isEmpty(path)) { return; } - - // The ES6 signature of this function is `path, ...contexts` - var contexts = context ? Array.prototype.slice.call(arguments, 1) : [], - currentState = get(this, 'currentState') || this; - - // First, get the enter, exit and resolve states for the current state - // and specified path. If possible, use an existing cache. - var hash = this.contextFreeTransition(currentState, path); - - // Next, process the raw state information for the contexts passed in. - var transition = new Transition(hash).normalize(this, contexts); - - this.enterState(transition); - this.triggerSetupContext(transition); - }, - - contextFreeTransition: function(currentState, path) { - var cache = currentState.getPathsCache(this, path); - if (cache) { return cache; } - - var enterStates = this.getStatesInPath(currentState, path), - exitStates = [], - resolveState = currentState; - - // Walk up the states. For each state, check whether a state matching - // the `path` is nested underneath. This will find the closest - // parent state containing `path`. - // - // This allows the user to pass in a relative path. For example, for - // the following state hierarchy: - // - // | |root - // | |- posts - // | | |- show (* current) - // | |- comments - // | | |- show - // - // If the current state is `<root.posts.show>`, an attempt to - // transition to `comments.show` will match `<root.comments.show>`. - // - // First, this code will look for root.posts.show.comments.show. - // Next, it will look for root.posts.comments.show. Finally, - // it will look for `root.comments.show`, and find the state. - // - // After this process, the following variables will exist: - // - // * resolveState: a common parent state between the current - // and target state. In the above example, `<root>` is the - // `resolveState`. - // * enterStates: a list of all of the states represented - // by the path from the `resolveState`. For example, for - // the path `root.comments.show`, `enterStates` would have - // `[<root.comments>, <root.comments.show>]` - // * exitStates: a list of all of the states from the - // `resolveState` to the `currentState`. In the above - // example, `exitStates` would have - // `[<root.posts>`, `<root.posts.show>]`. - while (resolveState && !enterStates) { - exitStates.unshift(resolveState); - - resolveState = get(resolveState, 'parentState'); - if (!resolveState) { - enterStates = this.getStatesInPath(this, path); - if (!enterStates) { - - return; - } - } - enterStates = this.getStatesInPath(resolveState, path); - } - - // If the path contains some states that are parents of both the - // current state and the target state, remove them. - // - // For example, in the following hierarchy: - // - // |- root - // | |- post - // | | |- index (* current) - // | | |- show - // - // If the `path` is `root.post.show`, the three variables will - // be: - // - // * resolveState: `<state manager>` - // * enterStates: `[<root>, <root.post>, <root.post.show>]` - // * exitStates: `[<root>, <root.post>, <root.post.index>]` - // - // The goal of this code is to remove the common states, so we - // have: - // - // * resolveState: `<root.post>` - // * enterStates: `[<root.post.show>]` - // * exitStates: `[<root.post.index>]` - // - // This avoid unnecessary calls to the enter and exit transitions. - while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { - resolveState = enterStates.shift(); - exitStates.shift(); - } - - // Cache the enterStates, exitStates, and resolveState for the - // current state and the `path`. - var transitions = { - exitStates: exitStates, - enterStates: enterStates, - resolveState: resolveState + var recordUpdated = function(updatedRecord) { + recordsUpdated([updatedRecord]); }; - currentState.setPathsCache(this, path, transitions); + var recordsToSend = records.map(function(record) { + releaseMethods.push(self.observeRecord(record, recordUpdated)); + return self.wrapRecord(record); + }); - return transitions; + + var contentDidChange = function(array, idx, removedCount, addedCount) { + for (var i = idx; i < idx + addedCount; i++) { + var record = array.objectAt(i); + var wrapped = self.wrapRecord(record); + releaseMethods.push(self.observeRecord(record, recordUpdated)); + recordsAdded([wrapped]); + } + + if (removedCount) { + recordsRemoved(idx, removedCount); + } + }; + + var observer = { didChange: contentDidChange, willChange: Ember.K }; + records.addArrayObserver(self, observer); + + release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + records.removeArrayObserver(self, observer); + self.releaseMethods.removeObject(release); + }; + + recordsAdded(recordsToSend); + + this.releaseMethods.pushObject(release); + return release; }, - triggerSetupContext: function(transitions) { - var contexts = transitions.contexts, - offset = transitions.enterStates.length - contexts.length, - enterStates = transitions.enterStates, - transitionEvent = get(this, 'transitionEvent'); + /** + @private - - arrayForEach.call(enterStates, function(state, idx) { - state.trigger(transitionEvent, this, contexts[idx-offset]); - }, this); + Clear all observers before destruction + */ + willDestroy: function() { + this._super(); + this.releaseMethods.forEach(function(fn) { + fn(); + }); }, - getState: function(name) { - var state = get(this, name), - parentState = get(this, 'parentState'); + /** + @private - if (state) { - return state; - } else if (parentState) { - return parentState.getState(name); + Detect whether a class is a model. + + Test that against the model class + of your persistence library + + @method detect + @param {Class} klass The class to test + @return boolean Whether the class is a model class or not + */ + detect: function(klass) { + return false; + }, + + /** + @private + + Get the columns for a given model type. + + @method columnsForType + @param {Class} type The model type + @return {Array} An array of columns of the following format: + name: {String} name of the column + desc: {String} Humanized description (what would show in a table column name) + */ + columnsForType: function(type) { + return Ember.A(); + }, + + /** + @private + + Adds observers to a model type class. + + @method observeModelType + @param {Class} type The model type class + @param {Function} typesUpdated Called when a type is modified. + @return {Function} The function to call to remove observers + */ + + observeModelType: function(type, typesUpdated) { + var self = this, records = this.getRecords(type); + + var onChange = function() { + typesUpdated([self.wrapModelType(type)]); + }; + var observer = { + didChange: function() { + Ember.run.scheduleOnce('actions', this, onChange); + }, + willChange: Ember.K + }; + + records.addArrayObserver(this, observer); + + var release = function() { + records.removeArrayObserver(self, observer); + }; + + return release; + }, + + + /** + @private + + Wraps a given model type and observes changes to it. + + @method wrapModelType + @param {Class} type A model class + @param {Function} typesUpdated callback to call when the type changes + @return {Object} contains the wrapped type and the function to remove observers + Format: + type: {Object} the wrapped type + The wrapped type has the following format: + name: {String} name of the type + count: {Integer} number of records available + columns: {Columns} array of columns to describe the record + object: {Class} the actual Model type class + release: {Function} The function to remove observers + */ + wrapModelType: function(type, typesUpdated) { + var release, records = this.getRecords(type), + typeToSend, self = this; + + typeToSend = { + name: type.toString(), + count: Ember.get(records, 'length'), + columns: this.columnsForType(type), + object: type + }; + + + return typeToSend; + }, + + + /** + @private + + Fetches all models defined in the application. + TODO: Use the resolver instead of looping over namespaces. + + @method getModelTypes + @return {Array} Array of model types + */ + getModelTypes: function() { + var namespaces = Ember.A(Ember.Namespace.NAMESPACES), types = Ember.A(), self = this; + + namespaces.forEach(function(namespace) { + for (var key in namespace) { + if (!namespace.hasOwnProperty(key)) { continue; } + var klass = namespace[key]; + if (self.detect(klass)) { + types.push(klass); + } + } + }); + return types; + }, + + /** + @private + + Fetches all loaded records for a given type. + + @method getRecords + @return {Array} array of records. + This array will be observed for changes, + so it should update when new records are added/removed. + */ + getRecords: function(type) { + return Ember.A(); + }, + + /** + @private + + Wraps a record and observers changes to it + + @method wrapRecord + @param {Object} record The record instance + @return {Object} the wrapped record. Format: + columnValues: {Array} + searchKeywords: {Array} + */ + wrapRecord: function(record) { + var recordToSend = { object: record }, columnValues = {}, self = this; + + recordToSend.columnValues = this.getRecordColumnValues(record); + recordToSend.searchKeywords = this.getRecordKeywords(record); + recordToSend.filterValues = this.getRecordFilterValues(record); + recordToSend.color = this.getRecordColor(record); + + return recordToSend; + }, + + /** + @private + + Gets the values for each column. + + @method getRecordColumnValues + @return {Object} Keys should match column names defined + by the model type. + */ + getRecordColumnValues: function(record) { + return {}; + }, + + /** + @private + + Returns keywords to match when searching records. + + @method getRecordKeywords + @return {Array} Relevant keywords for search. + */ + getRecordKeywords: function(record) { + return Ember.A(); + }, + + /** + @private + + Returns the values of filters defined by `getFilters`. + + @method getRecordFilterValues + @param {Object} record The record instance + @return {Object} The filter values + */ + getRecordFilterValues: function(record) { + return {}; + }, + + /** + @private + + Each record can have a color that represents its state. + + @method getRecordColor + @param {Object} record The record instance + @return {String} The record's color + Possible options: black, red, blue, green + */ + getRecordColor: function(record) { + return null; + }, + + /** + @private + + Observes all relevant properties and re-sends the wrapped record + when a change occurs. + + @method observerRecord + @param {Object} record The record instance + @param {Function} recordUpdated The callback to call when a record is updated. + @return {Function} The function to call to remove all observers. + */ + observeRecord: function(record, recordUpdated) { + return function(){}; + } + +}); + + +})(); + + + +(function() { +/** +Ember Extension Support + +@module ember +@submodule ember-extension-support +@requires ember-application +*/ + +})(); + +(function() { +/** + @module ember + @submodule ember-testing + */ +var slice = [].slice, + helpers = {}, + originalMethods = {}, + injectHelpersCallbacks = []; + +/** + This is a container for an assortment of testing related functionality: + + * Choose your default test adapter (for your framework of choice). + * Register/Unregister additional test helpers. + * Setup callbacks to be fired when the test helpers are injected into + your application. + + @class Test + @namespace Ember +*/ +Ember.Test = { + + /** + `registerHelper` is used to register a test helper that will be injected + when `App.injectTestHelpers` is called. + + The helper method will always be called with the current Application as + the first parameter. + + For example: + ```javascript + Ember.Test.registerHelper('boot', function(app) { + Ember.run(app, app.deferReadiness); + }); + ``` + + This helper can later be called without arguments because it will be + called with `app` as the first parameter. + + ```javascript + App = Ember.Application.create(); + App.injectTestHelpers(); + boot(); + ``` + + Whenever you register a helper that performs async operations, make sure + you `return wait();` at the end of the helper. + + If an async helper also needs to return a value, pass it to the `wait` + helper as a first argument: + `return wait(val);` + + @public + @method registerHelper + @param {String} name The name of the helper method to add. + @param {Function} helperMethod + */ + registerHelper: function(name, helperMethod) { + helpers[name] = helperMethod; + }, + /** + Remove a previously added helper method. + + Example: + ``` + Ember.Test.unregisterHelper('wait'); + ``` + + @public + @method unregisterHelper + @param {String} name The helper to remove. + */ + unregisterHelper: function(name) { + delete helpers[name]; + if (originalMethods[name]) { + window[name] = originalMethods[name]; + } + delete originalMethods[name]; + }, + + /** + Used to register callbacks to be fired whenever `App.injectTestHelpers` + is called. + + The callback will receive the current application as an argument. + + Example: + ``` + Ember.Test.onInjectHelpers(function() { + Ember.$(document).ajaxStart(function() { + Test.pendingAjaxRequests++; + }); + + Ember.$(document).ajaxStop(function() { + Test.pendingAjaxRequests--; + }); + }); + ``` + + @public + @method onInjectHelpers + @param {Function} callback The function to be called. + */ + onInjectHelpers: function(callback) { + injectHelpersCallbacks.push(callback); + }, + + /** + This returns a thenable tailored for testing. It catches failed + `onSuccess` callbacks and invokes the `Ember.Test.adapter.exception` + callback in the last chained then. + + This method should be returned by async helpers such as `wait`. + + @public + @method promise + @param {Function} resolver The function used to resolve the promise. + */ + promise: function(resolver) { + var promise = new Ember.RSVP.Promise(resolver); + var thenable = { + chained: false + }; + thenable.then = function(onSuccess, onFailure) { + var thenPromise, nextPromise; + thenable.chained = true; + thenPromise = promise.then(onSuccess, onFailure); + // this is to ensure all downstream fulfillment + // handlers are wrapped in the error handling + nextPromise = Ember.Test.promise(function(resolve) { + resolve(thenPromise); + }); + thenPromise.then(null, function(reason) { + // ensure this is the last promise in the chain + // if not, ignore and the exception will propagate + // this prevents the same error from being fired multiple times + if (!nextPromise.chained) { + Ember.Test.adapter.exception(reason); + } + }); + return nextPromise; + }; + return thenable; + }, + + /** + Used to allow ember-testing to communicate with a specific testing + framework. + + You can manually set it before calling `App.setupForTesting()`. + + Example: + ``` + Ember.Test.adapter = MyCustomAdapter.create() + ``` + + If you do not set it, ember-testing will default to `Ember.Test.QUnitAdapter`. + + @public + @property adapter + @type {Class} The adapter to be used. + @default Ember.Test.QUnitAdapter + */ + adapter: null +}; + +function curry(app, fn) { + return function() { + var args = slice.call(arguments); + args.unshift(app); + return fn.apply(app, args); + }; +} + +Ember.Application.reopen({ + /** + @property testHelpers + @type {Object} + @default {} + */ + testHelpers: {}, + + /** + This hook defers the readiness of the application, so that you can start + the app when your tests are ready to run. It also sets the router's + location to 'none', so that the window's location will not be modified + (preventing both accidental leaking of state between tests and interference + with your testing framework). + + Example: + ``` + App.setupForTesting(); + ``` + + @method setupForTesting + */ + setupForTesting: function() { + Ember.testing = true; + + this.deferReadiness(); + + this.Router.reopen({ + location: 'none' + }); + + // if adapter is not manually set default to QUnit + if (!Ember.Test.adapter) { + Ember.Test.adapter = Ember.Test.QUnitAdapter.create(); } }, - enterState: function(transition) { - var log = this.enableLogging; + /** + This injects the test helpers into the window's scope. If a function of the + same name has already been defined it will be cached (so that it can be reset + if the helper is removed with `unregisterHelper` or `removeTestHelpers`). - var exitStates = transition.exitStates.slice(0).reverse(); - arrayForEach.call(exitStates, function(state) { - state.trigger('exit', this); - }, this); + Any callbacks registered with `onInjectHelpers` will be called once the + helpers have been injected. - arrayForEach.call(transition.enterStates, function(state) { - if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); } - state.trigger('enter', this); - }, this); + Example: + ``` + App.injectTestHelpers(); + ``` - set(this, 'currentState', transition.finalState); + @method injectTestHelpers + */ + injectTestHelpers: function() { + this.testHelpers = {}; + for (var name in helpers) { + originalMethods[name] = window[name]; + this.testHelpers[name] = window[name] = curry(this, helpers[name]); + } + + for(var i = 0, l = injectHelpersCallbacks.length; i < l; i++) { + injectHelpersCallbacks[i](this); + } + }, + + /** + This removes all helpers that have been registered, and resets and functions + that were overridden by the helpers. + + Example: + ``` + App.removeTestHelpers(); + ``` + + @public + @method removeTestHelpers + */ + removeTestHelpers: function() { + for (var name in helpers) { + window[name] = originalMethods[name]; + delete this.testHelpers[name]; + delete originalMethods[name]; + } } }); @@ -31432,20 +36635,509 @@ Ember.StateManager = Ember.State.extend({ (function() { /** -Ember States + @module ember + @submodule ember-testing + */ + +var $ = Ember.$; + +/** + This method creates a checkbox and triggers the click event to fire the + passed in handler. It is used to correct for a bug in older versions + of jQuery (e.g 1.8.3). + + @private + @method testCheckboxClick +*/ +function testCheckboxClick(handler) { + $('<input type="checkbox">') + .css({ position: 'absolute', left: '-1000px', top: '-1000px' }) + .appendTo('body') + .on('click', handler) + .trigger('click') + .remove(); +} + +$(function() { + /* + Determine whether a checkbox checked using jQuery's "click" method will have + the correct value for its checked property. + + If we determine that the current jQuery version exhibits this behavior, + patch it to work correctly as in the commit for the actual fix: + https://github.com/jquery/jquery/commit/1fb2f92. + */ + testCheckboxClick(function() { + if (!this.checked && !$.event.special.click) { + $.event.special.click = { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ($.nodeName( this, "input" ) && this.type === "checkbox" && this.click) { + this.click(); + return false; + } + } + }; + } + }); + + // Try again to verify that the patch took effect or blow up. + testCheckboxClick(function() { + Ember.warn("clicked checkboxes should be checked! the jQuery patch didn't work", this.checked); + }); +}); + +})(); + + + +(function() { +/** + @module ember + @submodule ember-testing +*/ + +var Test = Ember.Test; + +/** + The primary purpose of this class is to create hooks that can be implemented + by an adapter for various test frameworks. + + @class Adapter + @namespace Ember.Test +*/ +Test.Adapter = Ember.Object.extend({ + /** + This callback will be called whenever an async operation is about to start. + + Override this to call your framework's methods that handle async + operations. + + @public + @method asyncStart + */ + asyncStart: Ember.K, + + /** + This callback will be called whenever an async operation has completed. + + @public + @method asyncEnd + */ + asyncEnd: Ember.K, + + /** + Override this method with your testing framework's false assertion. + This function is called whenever an exception occurs causing the testing + promise to fail. + + QUnit example: + + ```javascript + exception: function(error) { + ok(false, error); + }; + ``` + + @public + @method exception + @param {String} error The exception to be raised. + */ + exception: function(error) { + setTimeout(function() { + throw error; + }); + } +}); + +/** + This class implements the methods defined by Ember.Test.Adapter for the + QUnit testing framework. + + @class QUnitAdapter + @namespace Ember.Test + @extends Ember.Test.Adapter +*/ +Test.QUnitAdapter = Test.Adapter.extend({ + asyncStart: function() { + stop(); + }, + asyncEnd: function() { + start(); + }, + exception: function(error) { + ok(false, Ember.inspect(error)); + } +}); + +})(); + + + +(function() { +/** +* @module ember +* @submodule ember-testing +*/ + +var get = Ember.get, + Test = Ember.Test, + helper = Test.registerHelper, + countAsync = 0; + +Test.pendingAjaxRequests = 0; + +Test.onInjectHelpers(function() { + Ember.$(document).ajaxStart(function() { + Test.pendingAjaxRequests++; + }); + + Ember.$(document).ajaxStop(function() { + Test.pendingAjaxRequests--; + }); +}); + + +function visit(app, url) { + app.__container__.lookup('router:main').location.setURL(url); + Ember.run(app, app.handleURL, url); + return wait(app); +} + +function click(app, selector, context) { + var $el = findWithAssert(app, selector, context); + Ember.run($el, 'mousedown'); + + if ($el.is(':input')) { + var type = $el.prop('type'); + if (type !== 'checkbox' && type !== 'radio' && type !== 'hidden') { + Ember.run($el, 'focus'); + } + } + + Ember.run($el, 'mouseup'); + Ember.run($el, 'click'); + + return wait(app); +} + +function keyEvent(app, selector, context, type, keyCode) { + var $el; + if (typeof keyCode === 'undefined') { + keyCode = type; + type = context; + context = null; + } + $el = findWithAssert(app, selector, context); + var event = Ember.$.Event(type, { keyCode: keyCode }); + Ember.run($el, 'trigger', event); + return wait(app); +} + +function fillIn(app, selector, context, text) { + var $el; + if (typeof text === 'undefined') { + text = context; + context = null; + } + $el = findWithAssert(app, selector, context); + Ember.run(function() { + $el.val(text).change(); + }); + return wait(app); +} + +function findWithAssert(app, selector, context) { + var $el = find(app, selector, context); + if ($el.length === 0) { + throw new Error("Element " + selector + " not found."); + } + return $el; +} + +function find(app, selector, context) { + var $el; + context = context || get(app, 'rootElement'); + $el = app.$(selector, context); + + return $el; +} + +function wait(app, value) { + var promise; + + promise = Test.promise(function(resolve) { + if (++countAsync === 1) { + Test.adapter.asyncStart(); + } + var watcher = setInterval(function() { + var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; + if (routerIsLoading) { return; } + if (Test.pendingAjaxRequests) { return; } + if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + + clearInterval(watcher); + + if (--countAsync === 0) { + Test.adapter.asyncEnd(); + } + + Ember.run(null, resolve, value); + }, 10); + }); + + return buildChainObject(app, promise); +} + +/* + Builds an object that contains all helper methods. This object will be + returned by helpers and then-promises. + + This allows us to chain helpers: + + ```javascript + visit('posts/new') + .click('.add-btn') + .fillIn('.title', 'Post') + .click('.submit') + .then(function() { + equal('.post-title', 'Post'); + }) + .visit('comments') + .then(function() { + equal(find('.comments'),length, 0); + }); + ``` + + @method buildChainObject + @param {Ember.Application} app + @param {Ember.RSVP.Promise} promise + @return {Object} A new object with properties for each + of app's helpers to be used for continued + method chaining (using promises). +*/ +function buildChainObject(app, promise) { + var helperName, obj = {}; + for(helperName in app.testHelpers) { + obj[helperName] = chain(app, promise, app.testHelpers[helperName]); + } + obj.then = function(fn) { + var thenPromise = promise.then(fn); + return buildChainObject(app, thenPromise); + }; + return obj; +} + +/* + Used in conjunction with buildChainObject to setup a + continued chain of method calls (with promises) + + @method chain + @param {Ember.Application} app + @param {Ember.RSVP.Promise} promise + @param {Function} fn +*/ +function chain(app, promise, fn) { + return function() { + var args = arguments, chainedPromise; + chainedPromise = promise.then(function() { + return fn.apply(null, args); + }); + return buildChainObject(app, chainedPromise); + }; +} + +/** +* Loads a route, sets up any controllers, and renders any templates associated +* with the route as though a real user had triggered the route change while +* using your app. +* +* Example: +* +* ``` +* visit('posts/index').then(function() { +* // assert something +* }); +* ``` +* +* @method visit +* @param {String} url the name of the route +* @return {RSVP.Promise} +*/ +helper('visit', visit); + +/** +* Clicks an element and triggers any actions triggered by the element's `click` +* event. +* +* Example: +* +* ``` +* click('.some-jQuery-selector').then(function() { +* // assert something +* }); +* ``` +* +* @method click +* @param {String} selector jQuery selector for finding element on the DOM +* @return {RSVP.Promise} +*/ +helper('click', click); + +/** +* Simulates a key event, e.g. `keypress`, `keydown`, `keyup` with the desired keyCode +* +* Example: +* +* ``` +* keyEvent('.some-jQuery-selector', 'keypress', 13).then(function() { +* // assert something +* }); +* ``` +* +* @method keyEvent +* @param {String} selector jQuery selector for finding element on the DOM +* @param {String} the type of key event, e.g. `keypress`, `keydown`, `keyup` +* @param {Number} the keyCode of the simulated key event +* @return {RSVP.Promise} +*/ +helper('keyEvent', keyEvent); + +/** +* Fills in an input element with some text. +* +* Example: +* +* ``` +* fillIn('#email', 'you@example.com').then(function() { +* // assert something +* }); +* ``` +* +* @method fillIn +* @param {String} selector jQuery selector finding an input element on the DOM +* to fill text with +* @param {String} text text to place inside the input element +* @return {RSVP.Promise} +*/ +helper('fillIn', fillIn); + +/** +* Finds an element in the context of the app's container element. A simple alias +* for `app.$(selector)`. +* +* Example: +* +* ``` +* var $el = find('.my-selector); +* ``` +* +* @method find +* @param {String} selector jQuery string selector for element lookup +* @return {Object} jQuery object representing the results of the query +*/ +helper('find', find); + +/** +* +* Like `find`, but throws an error if the element selector returns no results +* +* Example: +* +* ``` +* var $el = findWithAssert('.doesnt-exist'); // throws error +* ``` +* +* @method findWithAssert +* @param {String} selector jQuery selector string for finding an element within +* the DOM +* @return {Object} jQuery object representing the results of the query +* @throws {Error} throws error if jQuery object returned has a length of 0 +*/ +helper('findWithAssert', findWithAssert); + +/** + Causes the run loop to process any pending events. This is used to ensure that + any async operations from other helpers (or your assertions) have been processed. + + This is most often used as the return value for the helper functions (see 'click', + 'fillIn','visit',etc). + + Example: + + ``` + Ember.Test.registerHelper('loginUser', function(app, username, password) { + visit('secured/path/here') + .fillIn('#username', username) + .fillIn('#password', username) + .click('.submit') + + return wait(app); + }); + + @method wait + @param {Object} value The value to be returned. + @return {RSVP.Promise} + ``` +*/ +helper('wait', wait); + +})(); + + + +(function() { +/** + Ember Testing + + @module ember + @submodule ember-testing + @requires ember-application +*/ + +})(); + +(function() { +/** +Ember @module ember -@submodule ember-states -@requires ember-runtime +*/ + +function throwWithMessage(msg) { + return function() { + throw new Error(msg); + }; +} + +function generateRemovedClass(className) { + var msg = " has been moved into a plugin: https://github.com/emberjs/ember-states"; + + return { + extend: throwWithMessage(className + msg), + create: throwWithMessage(className + msg) + }; +} + +Ember.StateManager = generateRemovedClass("Ember.StateManager"); + +/** + This was exported to ember-states plugin for v 1.0.0 release. See: https://github.com/emberjs/ember-states + + @class StateManager + @namespace Ember +*/ + +Ember.State = generateRemovedClass("Ember.State"); + +/** + This was exported to ember-states plugin for v 1.0.0 release. See: https://github.com/emberjs/ember-states + + @class State + @namespace Ember */ })(); })(); - - -if (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) { - Ember.Logger.warn("You are running a production build of Ember on localhost and won't receive detailed error messages. "+ - "If you want full error messages please use the non-minified build provided on the Ember website."); -} diff --git a/app/assets/javascripts/external_development/jquery-2.0.3.js b/vendor/assets/javascripts/development/jquery-2.0.3.js similarity index 100% rename from app/assets/javascripts/external_development/jquery-2.0.3.js rename to vendor/assets/javascripts/development/jquery-2.0.3.js diff --git a/vendor/assets/javascripts/list_view.js b/vendor/assets/javascripts/development/list-view.js old mode 100755 new mode 100644 similarity index 82% rename from vendor/assets/javascripts/list_view.js rename to vendor/assets/javascripts/development/list-view.js index af923982d..6c4df148c --- a/vendor/assets/javascripts/list_view.js +++ b/vendor/assets/javascripts/development/list-view.js @@ -1,3 +1,6 @@ +// Last commit: 1f0c355 (2013-09-18 11:01:11 -0400) + + (function() { var get = Ember.get, set = Ember.set; @@ -18,7 +21,7 @@ function positionElement() { // TODO: avoid needing this by avoiding unnecessary // calls to this method in the first place if (samePosition(position, _position)) { return; } - this._parentView.applyTransform(element, position); + this._parentView.applyTransform(element, position.x, position.y); this._position = position; }, this); @@ -186,23 +189,54 @@ Ember.ReusableListItemView = Ember.View.extend(Ember.ListItemViewMixin, { (function() { +var el = document.createElement('div'), style = el.style; + +var propPrefixes = ['Webkit', 'Moz', 'O', 'ms']; + +function testProp(prop) { + if (prop in style) return prop; + var uppercaseProp = prop.charAt(0).toUpperCase() + prop.slice(1); + for (var i=0; i<propPrefixes.length; i++) { + var prefixedProp = propPrefixes[i] + uppercaseProp; + if (prefixedProp in style) { + return prefixedProp; + } + } + return null; +} + +var transformProp = testProp('transform'); +var perspectiveProp = testProp('perspective'); + +var supports2D = transformProp !== null; +var supports3D = perspectiveProp !== null; + Ember.ListViewHelper = { + transformProp: transformProp, applyTransform: (function(){ - var element = document.createElement('div'); - - if ('webkitTransform' in element.style){ - return function(element, position){ - var x = position.x, - y = position.y; - - element.style.webkitTransform = 'translate3d(' + x + 'px, ' + y + 'px, 0)'; + if (supports2D) { + return function(element, x, y){ + element.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px)'; }; - }else{ - return function(element, position){ - var x = position.x, - y = position.y; - - element.style.top = y + 'px'; + } else { + return function(element, x, y){ + element.style.top = y + 'px'; + element.style.left = x + 'px'; + }; + } + })(), + apply3DTransform: (function(){ + if (supports3D) { + return function(element, x, y){ + element.style[transformProp] = 'translate3d(' + x + 'px, ' + y + 'px, 0)'; + }; + } else if (supports2D) { + return function(element, x, y){ + element.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px)'; + }; + } else { + return function(element, x, y){ + element.style.top = y + 'px'; element.style.left = x + 'px'; }; } @@ -239,10 +273,6 @@ function sortByContentIndex (viewOne, viewTwo){ return get(viewOne, 'contentIndex') - get(viewTwo, 'contentIndex'); } -function detectListItemViews(childView) { - return Ember.ListItemViewMixin.detect(childView); -} - function notifyMutationListeners() { if (Ember.View.notifyMutationListeners) { Ember.run.once(Ember.View, 'notifyMutationListeners'); @@ -296,6 +326,7 @@ function enableProfilingOutput() { */ Ember.ListViewMixin = Ember.Mixin.create({ itemViewClass: Ember.ListItemView, + emptyViewClass: Ember.View, classNames: ['ember-list-view'], attributeBindings: ['style'], domManager: domManager, @@ -315,13 +346,16 @@ Ember.ListViewMixin = Ember.Mixin.create({ */ init: function() { this._super(); - enableProfilingOutput(); - addContentArrayObserver.call(this); - this._syncChildViews(); - this.columnCountDidChange(); this.on('didInsertElement', syncListContainerWidth); + this.columnCountDidChange(); + this._syncChildViews(); + this._addContentArrayObserver(); }, + _addContentArrayObserver: Ember.beforeObserver(function() { + addContentArrayObserver.call(this); + }, 'content'), + /** Called on your view when it should push strings of HTML into a `Ember.RenderBuffer`. @@ -599,7 +633,7 @@ Ember.ListViewMixin = Ember.Mixin.create({ maxScrollTop: Ember.computed('height', 'totalHeight', function(){ var totalHeight, viewportHeight; - totalHeight = get(this, 'totalHeight'), + totalHeight = get(this, 'totalHeight'); viewportHeight = get(this, 'height'); return max(0, totalHeight - viewportHeight); @@ -665,7 +699,7 @@ Ember.ListViewMixin = Ember.Mixin.create({ } }, 'content'), - /** + /**), @private @event contentDidChange */ @@ -768,7 +802,7 @@ Ember.ListViewMixin = Ember.Mixin.create({ scrollTop = get(this, 'scrollTop'); contentLength = get(this, 'content.length'); maxContentIndex = max(contentLength - 1, 0); - childViews = get(this, 'listItemViews'); + childViews = this._childViews; childViewsLength = childViews.length; startingIndex = this._startingIndex(); @@ -786,24 +820,12 @@ Ember.ListViewMixin = Ember.Mixin.create({ } }, - /** - @private - - Returns an array of current ListItemView views in the visible area - when you start to scroll. - - @property {Ember.ComputedProperty} listItemViews - */ - listItemViews: Ember.computed('[]', function(){ - return this.filter(detectListItemViews); - }), - /** @private @method positionOrderedChildViews */ positionOrderedChildViews: function() { - return get(this, 'listItemViews').sort(sortByContentIndex); + return this._childViews.sort(sortByContentIndex); }, arrayWillChange: Ember.K, @@ -944,13 +966,7 @@ Ember.ListView = Ember.ContainerView.extend(Ember.ListViewMixin, { 'overflow-scrolling': 'touch' }, - applyTransform: function(element, position){ - var x = position.x, - y = position.y; - - element.style.top = y + 'px'; - element.style.left = x + 'px'; - }, + applyTransform: Ember.ListViewHelper.applyTransform, _scrollTo: function(scrollTop) { var element = get(this, 'element'); @@ -1006,6 +1022,148 @@ Ember.ListView = Ember.ContainerView.extend(Ember.ListViewMixin, { +(function() { +var fieldRegex = /input|textarea|select/i, + hasTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch, + handleStart, handleMove, handleEnd, handleCancel, + startEvent, moveEvent, endEvent, cancelEvent; +if (hasTouch) { + startEvent = 'touchstart'; + handleStart = function (e) { + var touch = e.touches[0], + target = touch && touch.target; + // avoid e.preventDefault() on fields + if (target && fieldRegex.test(target.tagName)) { + return; + } + bindWindow(this.scrollerEventHandlers); + this.willBeginScroll(e.touches, e.timeStamp); + e.preventDefault(); + }; + moveEvent = 'touchmove'; + handleMove = function (e) { + this.continueScroll(e.touches, e.timeStamp); + }; + endEvent = 'touchend'; + handleEnd = function (e) { + // if we didn't end up scrolling we need to + // synthesize click since we did e.preventDefault() + // on touchstart + if (!this._isScrolling) { + synthesizeClick(e); + } + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; + cancelEvent = 'touchcancel'; + handleCancel = function (e) { + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; +} else { + startEvent = 'mousedown'; + handleStart = function (e) { + if (e.which !== 1) return; + var target = e.target; + // avoid e.preventDefault() on fields + if (target && fieldRegex.test(target.tagName)) { + return; + } + bindWindow(this.scrollerEventHandlers); + this.willBeginScroll([e], e.timeStamp); + e.preventDefault(); + }; + moveEvent = 'mousemove'; + handleMove = function (e) { + this.continueScroll([e], e.timeStamp); + }; + endEvent = 'mouseup'; + handleEnd = function (e) { + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; + cancelEvent = 'mouseout'; + handleCancel = function (e) { + if (e.relatedTarget) return; + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; +} + +function handleWheel(e) { + this.mouseWheel(e); + e.preventDefault(); +} + +function bindElement(el, handlers) { + el.addEventListener(startEvent, handlers.start, false); + el.addEventListener('mousewheel', handlers.wheel, false); +} + +function unbindElement(el, handlers) { + el.removeEventListener(startEvent, handlers.start, false); + el.removeEventListener('mousewheel', handlers.wheel, false); +} + +function bindWindow(handlers) { + window.addEventListener(moveEvent, handlers.move, true); + window.addEventListener(endEvent, handlers.end, true); + window.addEventListener(cancelEvent, handlers.cancel, true); +} + +function unbindWindow(handlers) { + window.removeEventListener(moveEvent, handlers.move, true); + window.removeEventListener(endEvent, handlers.end, true); + window.removeEventListener(cancelEvent, handlers.cancel, true); +} + +Ember.VirtualListScrollerEvents = Ember.Mixin.create({ + init: function() { + this.on('didInsertElement', this, 'bindScrollerEvents'); + this.on('willDestroyElement', this, 'unbindScrollerEvents'); + this.scrollerEventHandlers = { + start: bind(this, handleStart), + move: bind(this, handleMove), + end: bind(this, handleEnd), + cancel: bind(this, handleCancel), + wheel: bind(this, handleWheel) + }; + return this._super(); + }, + bindScrollerEvents: function() { + var el = this.get('element'), + handlers = this.scrollerEventHandlers; + bindElement(el, handlers); + }, + unbindScrollerEvents: function() { + var el = this.get('element'), + handlers = this.scrollerEventHandlers; + unbindElement(el, handlers); + unbindWindow(handlers); + } +}); + +function bind(view, handler) { + return function (evt) { + handler.call(view, evt); + }; +} + +function synthesizeClick(e) { + var point = e.changedTouches[0], + target = point.target, + ev; + if (target && fieldRegex.test(target.tagName)) { + ev = document.createEvent('MouseEvents'); + ev.initMouseEvent('click', true, true, e.view, 1, point.screenX, point.screenY, point.clientX, point.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null); + return target.dispatchEvent(ev); + } +} + +})(); + + + (function() { /*global Scroller*/ var max = Math.max, get = Ember.get, set = Ember.set; @@ -1029,8 +1187,9 @@ function updateScrollerDimensions(target) { @class VirtualListView @namespace Ember */ -Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { +Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, Ember.VirtualListScrollerEvents, { _isScrolling: false, + _mouseWheel: null, css: { position: 'relative', overflow: 'hidden' @@ -1041,7 +1200,7 @@ Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { this.setupScroller(); }, _scrollerTop: 0, - applyTransform: Ember.ListViewHelper.applyTransform, + applyTransform: Ember.ListViewHelper.apply3DTransform, setupScroller: function(){ var view, y; @@ -1052,7 +1211,7 @@ Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { if (view.state !== 'inDOM') { return; } if (view.listContainerElement) { - view.applyTransform(view.listContainerElement, {x: 0, y: -top}); + view.applyTransform(view.listContainerElement, 0, -top); view._scrollerTop = top; view._scrollContentTo(top); } @@ -1072,17 +1231,7 @@ Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { }, 'width', 'height', 'totalHeight'), didInsertElement: function() { - var that, listContainerElement; - - that = this; this.listContainerElement = this.$('> .ember-list-container')[0]; - - this._mouseWheel = function(e) { that.mouseWheel(e); }; - this.$().on('mousewheel', this._mouseWheel); - }, - - willDestroyElement: function() { - this.$().off('mousewheel', this._mouseWheel); }, willBeginScroll: function(touches, timeStamp) { @@ -1113,6 +1262,10 @@ Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { } }, + endScroll: function(timeStamp) { + this.scroller.doTouchEnd(timeStamp); + }, + // api scrollTo: function(y, animate) { if (animate === undefined) { @@ -1134,48 +1287,6 @@ Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { this.scroller.scrollBy(0, delta, true); } - return false; - }, - - endScroll: function(timeStamp) { - this.scroller.doTouchEnd(timeStamp); - }, - - touchStart: function(e){ - e = e.originalEvent || e; - this.willBeginScroll(e.touches, e.timeStamp); - return false; - }, - - touchMove: function(e){ - e = e.originalEvent || e; - this.continueScroll(e.touches, e.timeStamp); - return false; - }, - - touchEnd: function(e){ - e = e.originalEvent || e; - this.endScroll(e.timeStamp); - return false; - }, - - mouseDown: function(e){ - this.willBeginScroll([e], e.timeStamp); - return false; - }, - - mouseMove: function(e){ - this.continueScroll([e], e.timeStamp); - return false; - }, - - mouseUp: function(e){ - this.endScroll(e.timeStamp); - return false; - }, - - mouseLeave: function(e){ - this.endScroll(e.timeStamp); return false; } }); @@ -1187,3 +1298,4 @@ Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, { (function() { })(); + diff --git a/app/assets/javascripts/external/ember-renderspeed.js b/vendor/assets/javascripts/ember-renderspeed.js similarity index 100% rename from app/assets/javascripts/external/ember-renderspeed.js rename to vendor/assets/javascripts/ember-renderspeed.js diff --git a/app/assets/javascripts/external/favcount.js b/vendor/assets/javascripts/favcount.js similarity index 100% rename from app/assets/javascripts/external/favcount.js rename to vendor/assets/javascripts/favcount.js diff --git a/app/assets/javascripts/external/handlebars.js b/vendor/assets/javascripts/handlebars.js similarity index 100% rename from app/assets/javascripts/external/handlebars.js rename to vendor/assets/javascripts/handlebars.js diff --git a/app/assets/javascripts/external/jquery.ba-replacetext.js b/vendor/assets/javascripts/jquery.ba-replacetext.js similarity index 100% rename from app/assets/javascripts/external/jquery.ba-replacetext.js rename to vendor/assets/javascripts/jquery.ba-replacetext.js diff --git a/app/assets/javascripts/external/jquery.ba-resize.min.js b/vendor/assets/javascripts/jquery.ba-resize.min.js similarity index 100% rename from app/assets/javascripts/external/jquery.ba-resize.min.js rename to vendor/assets/javascripts/jquery.ba-resize.min.js diff --git a/app/assets/javascripts/external/jquery.color.js b/vendor/assets/javascripts/jquery.color.js similarity index 100% rename from app/assets/javascripts/external/jquery.color.js rename to vendor/assets/javascripts/jquery.color.js diff --git a/app/assets/javascripts/external/jquery.cookie.js b/vendor/assets/javascripts/jquery.cookie.js similarity index 100% rename from app/assets/javascripts/external/jquery.cookie.js rename to vendor/assets/javascripts/jquery.cookie.js diff --git a/app/assets/javascripts/external/jquery.fileupload.js b/vendor/assets/javascripts/jquery.fileupload.js similarity index 100% rename from app/assets/javascripts/external/jquery.fileupload.js rename to vendor/assets/javascripts/jquery.fileupload.js diff --git a/app/assets/javascripts/external/jquery.iframe-transport.js b/vendor/assets/javascripts/jquery.iframe-transport.js similarity index 100% rename from app/assets/javascripts/external/jquery.iframe-transport.js rename to vendor/assets/javascripts/jquery.iframe-transport.js diff --git a/app/assets/javascripts/external/jquery.putcursoratend.js b/vendor/assets/javascripts/jquery.putcursoratend.js similarity index 100% rename from app/assets/javascripts/external/jquery.putcursoratend.js rename to vendor/assets/javascripts/jquery.putcursoratend.js diff --git a/app/assets/javascripts/external/jquery.tagsinput.js b/vendor/assets/javascripts/jquery.tagsinput.js similarity index 100% rename from app/assets/javascripts/external/jquery.tagsinput.js rename to vendor/assets/javascripts/jquery.tagsinput.js diff --git a/app/assets/javascripts/external/jquery.ui.widget.js b/vendor/assets/javascripts/jquery.ui.widget.js similarity index 100% rename from app/assets/javascripts/external/jquery.ui.widget.js rename to vendor/assets/javascripts/jquery.ui.widget.js diff --git a/app/assets/javascripts/external/lodash.js b/vendor/assets/javascripts/lodash.js similarity index 100% rename from app/assets/javascripts/external/lodash.js rename to vendor/assets/javascripts/lodash.js diff --git a/app/assets/javascripts/external/md5.js b/vendor/assets/javascripts/md5.js similarity index 100% rename from app/assets/javascripts/external/md5.js rename to vendor/assets/javascripts/md5.js diff --git a/app/assets/javascripts/external/modernizr.custom.95264.js b/vendor/assets/javascripts/modernizr.custom.95264.js similarity index 100% rename from app/assets/javascripts/external/modernizr.custom.95264.js rename to vendor/assets/javascripts/modernizr.custom.95264.js diff --git a/app/assets/javascripts/external/mousetrap.js b/vendor/assets/javascripts/mousetrap.js similarity index 100% rename from app/assets/javascripts/external/mousetrap.js rename to vendor/assets/javascripts/mousetrap.js diff --git a/app/assets/javascripts/external_development/ember.js b/vendor/assets/javascripts/production/ember.js old mode 100755 new mode 100644 similarity index 78% rename from app/assets/javascripts/external_development/ember.js rename to vendor/assets/javascripts/production/ember.js index 8f571c2af..209ece8f0 --- a/app/assets/javascripts/external_development/ember.js +++ b/vendor/assets/javascripts/production/ember.js @@ -1,163 +1,15 @@ -// Version: v1.0.0-pre.2-1839-ge87d164 -// Last commit: e87d164 (2013-07-29 18:30:35 -0400) +// ========================================================================== +// Project: Ember - JavaScript Application Framework +// Copyright: ©2011-2013 Tilde Inc. and contributors +// Portions ©2006-2011 Strobe Inc. +// Portions ©2008-2011 Apple Inc. All rights reserved. +// License: Licensed under MIT license +// See https://raw.github.com/emberjs/ember.js/master/LICENSE +// ========================================================================== -(function() { -/*global __fail__*/ - -/** -Ember Debug - -@module ember -@submodule ember-debug -*/ - -/** -@class Ember -*/ - -if ('undefined' === typeof Ember) { - Ember = {}; - - if ('undefined' !== typeof window) { - window.Em = window.Ember = Em = Ember; - } -} - -Ember.ENV = 'undefined' === typeof ENV ? {} : ENV; - -if (!('MANDATORY_SETTER' in Ember.ENV)) { - Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist -} - -/** - Define an assertion that will throw an exception if the condition is not - met. Ember build tools will remove any calls to `Ember.assert()` when - doing a production build. Example: - - ```javascript - // Test for truthiness - Ember.assert('Must pass a valid object', obj); - // Fail unconditionally - Ember.assert('This code path should never be run') - ``` - - @method assert - @param {String} desc A description of the assertion. This will become - the text of the Error thrown if the assertion fails. - @param {Boolean} test Must be truthy for the assertion to pass. If - falsy, an exception will be thrown. -*/ -Ember.assert = function(desc, test) { - Ember.Logger.assert(test, desc); - - if (Ember.testing && !test) { - // when testing, ensure test failures when assertions fail - throw new Error("Assertion Failed: " + desc); - } -}; - - -/** - Display a warning with the provided message. Ember build tools will - remove any calls to `Ember.warn()` when doing a production build. - - @method warn - @param {String} message A warning to display. - @param {Boolean} test An optional boolean. If falsy, the warning - will be displayed. -*/ -Ember.warn = function(message, test) { - if (!test) { - Ember.Logger.warn("WARNING: "+message); - if ('trace' in Ember.Logger) Ember.Logger.trace(); - } -}; - -/** - Display a debug notice. Ember build tools will remove any calls to - `Ember.debug()` when doing a production build. - - ```javascript - Ember.debug("I'm a debug notice!"); - ``` - - @method debug - @param {String} message A debug message to display. -*/ -Ember.debug = function(message) { - Ember.Logger.debug("DEBUG: "+message); -}; - -/** - Display a deprecation warning with the provided message and a stack trace - (Chrome and Firefox only). Ember build tools will remove any calls to - `Ember.deprecate()` when doing a production build. - - @method deprecate - @param {String} message A description of the deprecation. - @param {Boolean} test An optional boolean. If falsy, the deprecation - will be displayed. -*/ -Ember.deprecate = function(message, test) { - if (Ember.TESTING_DEPRECATION) { return; } - - if (arguments.length === 1) { test = false; } - if (test) { return; } - - if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } - - var error; - - // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome - try { __fail__.fail(); } catch (e) { error = e; } - - if (Ember.LOG_STACKTRACE_ON_DEPRECATION && error.stack) { - var stack, stackStr = ''; - if (error['arguments']) { - // Chrome - stack = error.stack.replace(/^\s+at\s+/gm, ''). - replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2'). - replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); - stack.shift(); - } else { - // Firefox - stack = error.stack.replace(/(?:\n@:0)?\s+$/m, ''). - replace(/^\(/gm, '{anonymous}(').split('\n'); - } - - stackStr = "\n " + stack.slice(2).join("\n "); - message = message + stackStr; - } - - Ember.Logger.warn("DEPRECATION: "+message); -}; - - - -/** - Display a deprecation warning with the provided message and a stack trace - (Chrome and Firefox only) when the wrapped method is called. - - Ember build tools will not remove calls to `Ember.deprecateFunc()`, though - no warnings will be shown in production. - - @method deprecateFunc - @param {String} message A description of the deprecation. - @param {Function} func The function to be deprecated. - @return {Function} a new function that wrapped the original function with a deprecation warning -*/ -Ember.deprecateFunc = function(message, func) { - return function() { - Ember.deprecate(message); - return func.apply(this, arguments); - }; -}; - -})(); - -// Version: v1.0.0-pre.2-1839-ge87d164 -// Last commit: e87d164 (2013-07-29 18:30:35 -0400) +// Version: v1.0.0-rc.6-733-gd034d11 +// Last commit: d034d11 (2013-09-16 00:44:21 -0700) (function() { @@ -185,7 +37,6 @@ var define, requireModule; deps = mod.deps; callback = mod.callback; reified = []; - exports; for (var i=0, l=deps.length; i<l; i++) { if (deps[i] === 'exports') { @@ -224,7 +75,7 @@ var define, requireModule; @class Ember @static - @version 1.0.0-rc.6 + @version 1.0.0 */ if ('undefined' === typeof Ember) { @@ -251,10 +102,10 @@ Ember.toString = function() { return "Ember"; }; /** @property VERSION @type String - @default '1.0.0-rc.6.1' + @default '1.0.0' @final */ -Ember.VERSION = '1.0.0-rc.6.1'; +Ember.VERSION = '1.0.0'; /** Standard environmental variables. You can define these in a global `ENV` @@ -264,10 +115,42 @@ Ember.VERSION = '1.0.0-rc.6.1'; @property ENV @type Hash */ -Ember.ENV = Ember.ENV || ('undefined' === typeof ENV ? {} : ENV); + +if ('undefined' === typeof ENV) { + exports.ENV = {}; +} + +// We disable the RANGE API by default for performance reasons +if ('undefined' === typeof ENV.DISABLE_RANGE_API) { + ENV.DISABLE_RANGE_API = true; +} + + +Ember.ENV = Ember.ENV || ENV; Ember.config = Ember.config || {}; +/** + Hash of enabled Canary features. Add to before creating your application. + + @property FEATURES + @type Hash +*/ + +Ember.FEATURES = {}; + +/** + Test that a feature is enabled. Parsed by Ember's build tools to leave + experimental features out of beta/stable builds. + + @method isEnabled + @param {string} feature +*/ + +Ember.FEATURES.isEnabled = function(feature) { + return Ember.FEATURES[feature]; +}; + // .......................................................... // BOOTSTRAP // @@ -320,7 +203,7 @@ Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPE Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; /** - Empty function. Useful for some operations. + Empty function. Useful for some operations. Always returns `this`. @method K @private @@ -354,14 +237,20 @@ Ember.uuid = 0; // function consoleMethod(name) { - var console = imports.console, - method = typeof console === 'object' ? console[name] : null; + var consoleObj; + if (imports.console) { + consoleObj = imports.console; + } else if (typeof console !== 'undefined') { + consoleObj = console; + } + + var method = typeof consoleObj === 'object' ? consoleObj[name] : null; if (method) { // Older IE doesn't support apply, but Chrome needs it if (method.apply) { return function() { - method.apply(console, arguments); + method.apply(consoleObj, arguments); }; } else { return function() { @@ -393,11 +282,87 @@ function assertPolyfill(test, message) { @namespace Ember */ Ember.Logger = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method log + @for Ember.Logger + @param {*} arguments + */ log: consoleMethod('log') || Ember.K, + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.warn('Something happened!'); // "Something happened!" will be printed to the console with a warning icon. + ``` + + @method warn + @for Ember.Logger + @param {*} arguments + */ warn: consoleMethod('warn') || Ember.K, + /** + Prints the arguments to the console with an error icon, red text and a stack race. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + Ember.Logger.error('Danger! Danger!'); // "Danger! Danger!" will be printed to the console in red text. + ``` + + @method error + @for Ember.Logger + @param {*} arguments + */ error: consoleMethod('error') || Ember.K, + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method info + @for Ember.Logger + @param {*} arguments + */ info: consoleMethod('info') || Ember.K, + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); // "log value of foo: 1" will be printed to the console + ``` + + @method debug + @for Ember.Logger + @param {*} arguments + */ debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + /** + + If the value passed into Ember.Logger.assert is not truthy it will throw an error with a stack trace. + + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ assert: consoleMethod('assert') || assertPolyfill }; @@ -411,6 +376,15 @@ Ember.Logger = { internals encounter an error. This is useful for specialized error handling and reporting code. + ```javascript + Ember.onerror = function(error) { + Em.$.ajax('/report-error', 'POST', { + stack: error.stack, + otherInformation: 'whatever app state you want to provide' + }); + }; + ``` + @event onerror @for Ember @param {Exception} error the error object @@ -441,6 +415,21 @@ Ember.handleErrors = function(func, context) { } }; +/** + Merge the contents of two objects together into the first object. + + ```javascript + Ember.merge({first: 'Tom'}, {last: 'Dale'}); // {first: 'Tom', last: 'Dale'} + var a = {first: 'Yehuda'}, b = {last: 'Katz'}; + Ember.merge(a, b); // a == {first: 'Yehuda', last: 'Katz'}, b == {last: 'Katz'} + ``` + + @method merge + @for Ember + @param {Object} original The object to merge into + @param {Object} updates The object to copy properties from + @return {Object} +*/ Ember.merge = function(original, updates) { for (var prop in updates) { if (!updates.hasOwnProperty(prop)) { continue; } @@ -743,6 +732,12 @@ var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.index return -1; }; +/** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills +*/ Ember.ArrayPolyfills = { map: arrayMap, forEach: arrayForEach, @@ -1038,7 +1033,7 @@ Ember.setMeta = function setMeta(obj, property, value) { shared with its constructor */ Ember.metaPath = function metaPath(obj, path, writable) { - Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); + var meta = Ember.meta(obj, writable), keyName, value; for (var i=0, l=path.length; i<l; i++) { @@ -1087,6 +1082,7 @@ Ember.wrap = function(func, superFunc) { superWrapper.wrappedFunction = func; superWrapper.__ember_observes__ = func.__ember_observes__; superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__; + superWrapper.__ember_listens__ = func.__ember_listens__; return superWrapper; }; @@ -1155,10 +1151,18 @@ function canInvoke(obj, methodName) { /** Checks to see if the `methodName` exists on the `obj`. + ```javascript + var foo = {bar: Ember.K, baz: null}; + Ember.canInvoke(foo, 'bar'); // true + Ember.canInvoke(foo, 'baz'); // false + Ember.canInvoke(foo, 'bat'); // false + ``` + @method canInvoke @for Ember @param {Object} obj The object to check for the method @param {String} methodName The method name to check for + @return {Boolean} */ Ember.canInvoke = canInvoke; @@ -1166,6 +1170,13 @@ Ember.canInvoke = canInvoke; Checks to see if the `methodName` exists on the `obj`, and if it does, invokes it with the arguments passed. + ```javascript + var d = new Date('03/15/2013'); + Ember.tryInvoke(d, 'getTime'); // 1363320000000 + Ember.tryInvoke(d, 'setFullYear', [2014]); // 1394856000000 + Ember.tryInvoke(d, 'noSuchMethod', [2014]); // undefined + ``` + @method tryInvoke @for Ember @param {Object} obj The object to check for the method @@ -1197,6 +1208,17 @@ var needsFinallyFix = (function() { Provides try { } finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + someResource.lock(); + runCallback(); // May throw error. + }; + var finalizer = function() { + someResource.unlock(); + }; + Ember.tryFinally(tryable, finalizer); + ``` + @method tryFinally @for Ember @param {Function} tryable The function to run the try callback @@ -1247,6 +1269,30 @@ if (needsFinallyFix) { Provides try { } catch finally { } functionality, while working around Safari's double finally bug. + ```javascript + var tryable = function() { + for (i=0, l=listeners.length; i<l; i++) { + listener = listeners[i]; + beforeValues[i] = listener.before(name, time(), payload); + } + + return callback.call(binding); + }; + + var catchable = function(e) { + payload = payload || {}; + payload.exception = e; + }; + + var finalizer = function() { + for (i=0, l=listeners.length; i<l; i++) { + listener = listeners[i]; + listener.after(name, time(), payload, beforeValues[i]); + } + }; + Ember.tryCatchFinally(tryable, catchable, finalizer); + ``` + @method tryCatchFinally @for Ember @param {Function} tryable The function to run the try callback @@ -1442,13 +1488,23 @@ var populateListeners = function(name) { }; var time = (function() { - var perf = 'undefined' !== typeof window ? window.performance || {} : {}; - var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow; - // fn.bind will be available in all the browsers that support the advanced window.performance... ;-) - return fn ? fn.bind(perf) : function() { return +new Date(); }; + var perf = 'undefined' !== typeof window ? window.performance || {} : {}; + var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow; + // fn.bind will be available in all the browsers that support the advanced window.performance... ;-) + return fn ? fn.bind(perf) : function() { return +new Date(); }; })(); +/** + Notifies event's subscribers, calls `before` and `after` hooks. + @method instrument + @namespace Ember.Instrumentation + + @param {String} [name] Namespaced event name. + @param {Object} payload + @param {Function} callback Function that you're instrumenting. + @param {Object} binding Context that instrument function is called with. +*/ Ember.Instrumentation.instrument = function(name, payload, callback, binding) { var listeners = cache[name], timeName, ret; @@ -1497,6 +1553,15 @@ Ember.Instrumentation.instrument = function(name, payload, callback, binding) { return Ember.tryCatchFinally(tryable, catchable, finalizer); }; +/** + Subscribes to a particular event or instrumented block of code. + + @method subscribe + @namespace Ember.Instrumentation + + @param {String} [pattern] Namespaced event name. + @param {Object} [object] Before and After hooks. +*/ Ember.Instrumentation.subscribe = function(pattern, object) { var paths = pattern.split("."), path, regex = []; @@ -1524,6 +1589,14 @@ Ember.Instrumentation.subscribe = function(pattern, object) { return subscriber; }; +/** + Unsubscribes from a particular event or instrumented block of code. + + @method unsubscribe + @namespace Ember.Instrumentation + + @param {Object} [subscriber] +*/ Ember.Instrumentation.unsubscribe = function(subscriber) { var index; @@ -1537,6 +1610,12 @@ Ember.Instrumentation.unsubscribe = function(subscriber) { cache = {}; }; +/** + Resets `Ember.Instrumentation` by flushing list of subscribers. + + @method reset + @namespace Ember.Instrumentation +*/ Ember.Instrumentation.reset = function() { subscribers = []; cache = {}; @@ -1544,17 +1623,16 @@ Ember.Instrumentation.reset = function() { Ember.instrument = Ember.Instrumentation.instrument; Ember.subscribe = Ember.Instrumentation.subscribe; - })(); (function() { -var map, forEach, indexOf, concat; -concat = Array.prototype.concat; +var map, forEach, indexOf, splice; map = Array.prototype.map || Ember.ArrayPolyfills.map; forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; +splice = Array.prototype.splice; var utils = Ember.EnumerableUtils = { map: function(obj, callback, thisArg) { @@ -1585,12 +1663,31 @@ var utils = Ember.EnumerableUtils = { if (index !== -1) { array.splice(index, 1); } }, + _replace: function(array, idx, amt, objects) { + var args = [].concat(objects), chunk, ret = [], + // https://code.google.com/p/chromium/issues/detail?id=56588 + size = 60000, start = idx, ends = amt, count; + + while (args.length) { + count = ends > size ? size : ends; + if (count <= 0) { count = 0; } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + }, + replace: function(array, idx, amt, objects) { if (array.replace) { return array.replace(idx, amt, objects); } else { - var args = concat.apply([idx, amt], objects); - return array.splice.apply(array, args); + return utils._replace(array, idx, amt, objects); } }, @@ -1666,8 +1763,7 @@ get = function get(obj, keyName) { obj = null; } - Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); - Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); + if (obj === null || keyName.indexOf('.') !== -1) { return getPath(obj, keyName); @@ -1769,7 +1865,6 @@ Ember.getWithDefault = function(root, key, defaultValue) { Ember.get = get; -Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now supports paths', Ember.get); })(); @@ -1783,6 +1878,7 @@ Ember.getPath = Ember.deprecateFunc('getPath is deprecated since get now support var o_create = Ember.create, metaFor = Ember.meta, META_KEY = Ember.META_KEY, + a_slice = [].slice, /* listener flags */ ONCE = 1, SUSPENDED = 2; @@ -1797,7 +1893,7 @@ var o_create = Ember.create, { listeners: { // variable name: `listenerSet` "foo:changed": [ // variable name: `actions` - [target, method, flags] + target, method, flags ] } } @@ -1806,8 +1902,8 @@ var o_create = Ember.create, function indexOf(array, target, method) { var index = -1; - for (var i = 0, l = array.length; i < l; i++) { - if (target === array[i][0] && method === array[i][1]) { index = i; break; } + for (var i = 0, l = array.length; i < l; i += 3) { + if (target === array[i] && method === array[i+1]) { index = i; break; } } return index; } @@ -1840,14 +1936,14 @@ function actionsUnion(obj, eventName, otherActions) { actions = meta && meta.listeners && meta.listeners[eventName]; 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) { - otherActions.push([target, method, flags]); + otherActions.push(target, method, flags); } } } @@ -1858,16 +1954,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; @@ -1885,7 +1981,7 @@ function actionsDiff(obj, eventName, otherActions) { @param {Boolean} once A flag whether a function should only be called once */ function addListener(obj, eventName, target, method, once) { - Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); + if (!method && 'function' === typeof target) { method = target; @@ -1900,7 +1996,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); @@ -1920,7 +2016,7 @@ function addListener(obj, eventName, target, method, once) { @param {Function|String} method A function or the name of a function to be called on `target` */ function removeListener(obj, eventName, target, method) { - Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); + if (!method && 'function' === typeof target) { method = target; @@ -1934,7 +2030,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); @@ -1948,8 +2044,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]); } } } @@ -1979,17 +2075,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); } @@ -1999,7 +2092,7 @@ function suspendListener(obj, eventName, target, method, callback) { Suspends multiple listeners during a callback. - + @method suspendListeners @for Ember @param obj @@ -2015,7 +2108,8 @@ function suspendListeners(obj, eventNames, target, method, callback) { } var suspendedActions = [], - eventName, actions, action, i, l; + actionsList = [], + eventName, actions, i, l; for (i=0, l=eventNames.length; i<l; i++) { eventName = eventNames[i]; @@ -2023,18 +2117,18 @@ function suspendListeners(obj, eventNames, target, method, callback) { var actionIndex = indexOf(actions, target, method); if (actionIndex !== -1) { - action = actions[actionIndex].slice(); - action[2] |= SUSPENDED; - actions[actionIndex] = action; - suspendedActions.push(action); + actions[actionIndex+2] |= SUSPENDED; + suspendedActions.push(actionIndex); + actionsList.push(actions); } } function tryable() { return callback.call(target); } function finalizer() { - for (i = 0, l = suspendedActions.length; i < l; i++) { - suspendedActions[i][2] &= ~SUSPENDED; + for (var i = 0, l = suspendedActions.length; i < l; i++) { + var actionIndex = suspendedActions[i]; + actionsList[i][actionIndex+2] &= ~SUSPENDED; } } @@ -2066,7 +2160,7 @@ function watchedEvents(obj) { is skipped, and once listeners are removed. A listener without a target is executed on the passed object. If an array of actions is not passed, the actions stored on the passed object are invoked. - + @method sendEvent @for Ember @param obj @@ -2088,10 +2182,9 @@ function sendEvent(obj, eventName, params, actions) { if (!actions) { return; } - for (var i = actions.length - 1; 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; } @@ -2133,15 +2226,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; @@ -2256,7 +2374,7 @@ var metaFor = Ember.meta, @param {String} keyName The property key (or path) that will change. @return {void} */ -var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { +function propertyWillChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -2268,7 +2386,8 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { dependentKeysWillChange(obj, keyName, m); chainsWillChange(obj, keyName, m); notifyBeforeObservers(obj, keyName); -}; +} +Ember.propertyWillChange = propertyWillChange; /** This function is called just after an object property has changed. @@ -2276,7 +2395,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { Normally you will not need to call this method directly but if for some reason you can't directly watch a property you can invoke this method - manually along with `Ember.propertyWilLChange()` which you should call just + manually along with `Ember.propertyWillChange()` which you should call just before the property value changes. @method propertyDidChange @@ -2285,7 +2404,7 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { @param {String} keyName The property key (or path) that will change. @return {void} */ -var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { +function propertyDidChange(obj, keyName) { var m = metaFor(obj, false), watching = m.watching[keyName] > 0 || keyName === 'length', proto = m.proto, @@ -2298,9 +2417,10 @@ var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { if (!watching && keyName !== 'length') { return; } dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName, m); + chainsDidChange(obj, keyName, m, false); notifyObservers(obj, keyName); -}; +} +Ember.propertyDidChange = propertyDidChange; var WILL_SEEN, DID_SEEN; @@ -2341,32 +2461,47 @@ function iterDeps(method, obj, depKey, seen, meta) { } } -var chainsWillChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do - - var nodes = m.chainWatchers; - - nodes = nodes[keyName]; - if (!nodes) { return; } - - for(var i = 0, l = nodes.length; i < l; i++) { - nodes[i].willChange(arg); +function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; } -}; -var chainsDidChange = function(obj, keyName, m, arg) { - if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do + var nodes = m.chainWatchers[keyName], + events = [], + i, l; - var nodes = m.chainWatchers; - - 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[i].didChange(arg); + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); } -}; + + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i+1]); + } +} + +function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m.hasOwnProperty('chainWatchers') && + m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName], + events = suppressEvents ? null : [], + i, l; + + for(i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } + + if (suppressEvents) { + return; + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i+1]); + } +} Ember.overrideChains = function(obj, keyName, m) { chainsDidChange(obj, keyName, m, true); @@ -2376,20 +2511,24 @@ Ember.overrideChains = function(obj, keyName, m) { @method beginPropertyChanges @chainable */ -var beginPropertyChanges = Ember.beginPropertyChanges = function() { +function beginPropertyChanges() { deferred++; -}; +} + +Ember.beginPropertyChanges = beginPropertyChanges; /** @method endPropertyChanges */ -var endPropertyChanges = Ember.endPropertyChanges = function() { +function endPropertyChanges() { deferred--; if (deferred<=0) { beforeObserverSet.clear(); observerSet.flush(); } -}; +} + +Ember.endPropertyChanges = endPropertyChanges; /** Make a series of property changes together in an @@ -2411,7 +2550,7 @@ Ember.changeProperties = function(cb, binding) { tryFinally(cb, endPropertyChanges, binding); }; -var notifyBeforeObservers = function(obj, keyName) { +function notifyBeforeObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':before', listeners, diff; @@ -2422,9 +2561,9 @@ var notifyBeforeObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} -var notifyObservers = function(obj, keyName) { +function notifyObservers(obj, keyName) { if (obj.isDestroying) { return; } var eventName = keyName + ':change', listeners; @@ -2434,7 +2573,7 @@ var notifyObservers = function(obj, keyName) { } else { sendEvent(obj, eventName, [obj, keyName]); } -}; +} })(); @@ -2453,7 +2592,7 @@ var META_KEY = Ember.META_KEY, /** Sets the value of a property on an object, respecting computed properties and notifying observers and other listeners of the change. If the - property is not defined but the object implements the `unknownProperty` + property is not defined but the object implements the `setUnknownProperty` method then that will be invoked as well. If you plan to run on IE8 and older browsers then you should use this @@ -2463,7 +2602,7 @@ var META_KEY = Ember.META_KEY, On all newer browsers, you only need to use this method to set properties if the property might not be defined on the object and you want - to respect the `unknownProperty` handler. Otherwise you can ignore this + to respect the `setUnknownProperty` handler. Otherwise you can ignore this method. @method set @@ -2475,20 +2614,18 @@ var META_KEY = Ember.META_KEY, */ var set = function set(obj, keyName, value, tolerant) { if (typeof obj === 'string') { - Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); + value = keyName; keyName = obj; obj = null; } - Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); if (!obj || keyName.indexOf('.') !== -1) { return setPath(obj, keyName, value, tolerant); } - Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); - Ember.assert('calling set on destroyed object', !obj.isDestroyed); + var meta = obj[META_KEY], desc = meta && meta.descs[keyName], isUnknown, currentValue; @@ -2564,7 +2701,6 @@ function setPath(root, path, value, tolerant) { } Ember.set = set; -Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now supports paths', Ember.set); /** Error-tolerant form of `Ember.set`. Will not blow up if any part of the @@ -2582,7 +2718,6 @@ Ember.setPath = Ember.deprecateFunc('setPath is deprecated since set now support Ember.trySet = function(root, path, value) { return set(root, path, value, true); }; -Ember.trySetPath = Ember.deprecateFunc('trySetPath has been renamed to trySet', Ember.trySet); })(); @@ -2794,14 +2929,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. @@ -3000,7 +3135,7 @@ Ember.Descriptor = function() {}; // var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) { - Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false); + }; var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { @@ -3082,7 +3217,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) { @@ -3123,6 +3257,47 @@ Ember.defineProperty = function(obj, keyName, desc, data, meta) { +(function() { +var get = Ember.get; + +/** + To get multiple properties at once, call `Ember.getProperties` + with an object followed by a list of strings or an array: + + ```javascript + Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + is equivalent to: + + ```javascript + Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } + ``` + + @method getProperties + @param obj + @param {String...|Array} list of keys to get + @return {Hash} +*/ +Ember.getProperties = function(obj) { + var ret = {}, + propertyNames = arguments, + i = 1; + + if (arguments.length === 2 && Ember.typeOf(arguments[1]) === 'array') { + i = 0; + propertyNames = arguments[1]; + } + for(var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = get(obj, propertyNames[i]); + } + return ret; +}; + +})(); + + + (function() { var changeProperties = Ember.changeProperties, set = Ember.set; @@ -3132,6 +3307,14 @@ var changeProperties = Ember.changeProperties, a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered. + ```javascript + anObject.setProperties({ + firstName: "Stanley", + lastName: "Stuart", + age: "21" + }) + ``` + @method setProperties @param self @param {Object} hash @@ -3160,13 +3343,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); @@ -3188,13 +3369,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); @@ -3213,6 +3391,7 @@ Ember.unwatchKey = function(obj, keyName) { watching[keyName]--; } }; + })(); @@ -3225,8 +3404,6 @@ var metaFor = Ember.meta, // utils.js warn = Ember.warn, watchKey = Ember.watchKey, unwatchKey = Ember.unwatchKey, - propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange, FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { @@ -3281,10 +3458,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. @@ -3319,10 +3492,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; }; @@ -3442,42 +3637,50 @@ ChainNodePrototype.unchain = function(key, path) { }; -ChainNodePrototype.willChange = function() { +ChainNodePrototype.willChange = function(events) { var chains = this._chains; if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].willChange(); + chains[key].willChange(events); } } - if (this._parent) { this._parent.chainWillChange(this, this._key, 1); } + if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } }; -ChainNodePrototype.chainWillChange = function(chain, path, depth) { +ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainWillChange(this, path, depth+1); + this._parent.chainWillChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyWillChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyWillChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.chainDidChange = function(chain, path, depth) { +ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { if (this._key) { path = this._key + '.' + path; } if (this._parent) { - this._parent.chainDidChange(this, path, depth+1); + this._parent.chainDidChange(this, path, depth+1, events); } else { - if (depth > 1) { propertyDidChange(this.value(), path); } + if (depth > 1) { + events.push(this.value(), path); + } path = 'this.' + path; - if (this._paths[path] > 0) { propertyDidChange(this.value(), path); } + if (this._paths[path] > 0) { + events.push(this.value(), path); + } } }; -ChainNodePrototype.didChange = function(suppressEvent) { +ChainNodePrototype.didChange = function(events) { // invalidate my own value first. if (this._watching) { var obj = this._parent.value(); @@ -3499,14 +3702,25 @@ ChainNodePrototype.didChange = function(suppressEvent) { if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) { continue; } - chains[key].didChange(suppressEvent); + chains[key].didChange(events); } } - if (suppressEvent) { return; } + // if no events are passed in then we only care about the above wiring update + if (events === null) { return; } // and finally tell parent about my path changing... - if (this._parent) { this._parent.chainDidChange(this, this._key, 1); } + if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } +}; + +Ember.finishChains = function(obj) { + var m = metaFor(obj, false), chains = m.chains; + if (chains) { + if (chains.value() !== obj) { + m.chains = chains = chains.copy(obj); + } + chains.didChange(null); + } }; })(); @@ -3701,7 +3915,6 @@ Ember.destroy = function (obj) { @module ember-metal */ -Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false); var get = Ember.get, @@ -3789,6 +4002,81 @@ function removeDependentKeys(desc, obj, keyName, meta) { // /** + A computed property transforms an objects function into a property. + + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property is dependent on. This will force the cached + result to be recomputed if the dependencies are modified. + + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the properties + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as it's dependencies have not been changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. + + ```javascript + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); + + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); + + tom.get('fullName') // "Tom Dale" + ``` + + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. + + ```javascript + + Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(" "); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + person.set('fullName', "Peter Wagenet"); + person.get('firstName') // Peter + person.get('lastName') // Wagenet + ``` + @class ComputedProperty @namespace Ember @extends Ember.Descriptor @@ -3807,7 +4095,7 @@ ComputedProperty.prototype = new Ember.Descriptor(); var ComputedPropertyPrototype = ComputedProperty.prototype; -/* +/** Properties are cacheable by default. Computed property will automatically cache the return value of your function until one of the dependent keys changes. @@ -3933,25 +4221,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 @@ -3960,31 +4229,76 @@ 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); } } }; -/* impl descriptor API */ +function finishChains(chainNodes) +{ + for (var i=0, l=chainNodes.length; i<l; i++) { + chainNodes[i].didChange(null); + } +} + +/** + Access the value of the function backing the computed property. + If this property has already been cached, return the cached result. + Otherwise, call the function passing the property name as an argument. + + ```javascript + Person = Ember.Object.extend({ + fullName: function(keyName) { + // the keyName parameter is 'fullName' in this case. + + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + }); + + + var tom = Person.create({ + firstName: "Tom", + lastName: "Dale" + }); + + tom.get('fullName') // "Tom Dale" + ``` + + @method get + @param {String} keyName The key being accessed. + @return {Object} The return value of the function backing the CP. +*/ ComputedPropertyPrototype.get = function(obj, keyName) { - var ret, cache, meta; + var ret, cache, meta, chainNodes; if (this._cacheable) { meta = metaFor(obj); cache = meta.cache; if (keyName in cache) { return cache[keyName]; } ret = cache[keyName] = this.func.call(obj, keyName); - if (!meta.watching[keyName]) { - addDependentKeys(this, obj, keyName, meta); - } + chainNodes = meta.chainWatchers && meta.chainWatchers[keyName]; + if (chainNodes) { finishChains(chainNodes); } + addDependentKeys(this, obj, keyName, meta); } else { ret = this.func.call(obj, keyName); } return ret; }; -/* impl descriptor API */ +/** + Set the value of a computed property. If the function that backs your + computed property does not accept arguments then the default action for + setting would be to define the property on the current object, and set + the value of the property to the value being set. + + Generally speaking if you intend for your computed property to be set + your backing function should accept either two or three arguments. + + @method set + @param {String} keyName The key being accessed. + @param {Object} newValue The new value being assigned. + @param {String} oldValue The old value being replaced. + @return {Object} The return value of the function backing the CP. +*/ ComputedPropertyPrototype.set = function(obj, keyName, value) { var cacheable = this._cacheable, func = this.func, @@ -4036,7 +4350,7 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { } if (cacheable) { - if (!watched && !hadCachedValue) { + if (!hadCachedValue) { addDependentKeys(this, obj, keyName, meta); } cache[keyName] = ret; @@ -4049,19 +4363,11 @@ ComputedPropertyPrototype.set = function(obj, keyName, value) { return ret; }; -/* called when property is defined */ -ComputedPropertyPrototype.setup = function(obj, keyName) { - var meta = obj[META_KEY]; - if (meta && meta.watching[keyName]) { - addDependentKeys(this, obj, keyName, metaFor(obj)); - } -}; - /* called before property is overridden */ ComputedPropertyPrototype.teardown = function(obj, keyName) { var meta = metaFor(obj); - if (meta.watching[keyName] || keyName in meta.cache) { + if (keyName in meta.cache) { removeDependentKeys(this, obj, keyName, meta); } @@ -4157,6 +4463,25 @@ function registerComputedWithProperties(name, macro) { } /** + A computed property that returns true if the value of the dependent + property is null, an empty string, empty array, or empty function. + + Note: When using `Ember.computed.empty` to watch an array make sure to + use the `array.length` syntax so the computed can subscribe to transitions + from empty to non-empty states. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + done: Ember.computed.empty('todos.length') + }); + var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); + todoList.get('done'); // false + todoList.get('todos').clear(); // [] + todoList.get('done'); // true + ``` + @method computed.empty @for Ember @param {String} dependentKey @@ -4168,6 +4493,21 @@ registerComputed('empty', function(dependentKey) { }); /** + A computed property that returns true if the value of the dependent + property is NOT null, an empty string, empty array, or empty function. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasStuff: Ember.computed.notEmpty('backpack') + }); + var hampster = Hampster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); + hampster.get('hasStuff'); // true + hampster.get('backpack').clear(); // [] + hampster.get('hasStuff'); // false + ``` + @method computed.notEmpty @for Ember @param {String} dependentKey @@ -4179,6 +4519,24 @@ registerComputed('notEmpty', function(dependentKey) { }); /** + A computed property that returns true if the value of the dependent + property is null or undefined. This avoids errors from JSLint complaining + about use of ==, which can be technically confusing. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + isHungry: Ember.computed.none('food') + }); + var hampster = Hampster.create(); + hampster.get('isHungry'); // true + hampster.set('food', 'Banana'); + hampster.get('isHungry'); // false + hampster.set('food', null); + hampster.get('isHungry'); // true + ``` + @method computed.none @for Ember @param {String} dependentKey @@ -4190,6 +4548,21 @@ registerComputed('none', function(dependentKey) { }); /** + A computed property that returns the inverse boolean value + of the original value for the dependent property. + + Example + + ```javascript + var User = Ember.Object.extend({ + isAnonymous: Ember.computed.not('loggedIn') + }); + var user = User.create({loggedIn: false}); + user.get('isAnonymous'); // true + user.set('loggedIn', true); + user.get('isAnonymous'); // false + ``` + @method computed.not @for Ember @param {String} dependentKey @@ -4201,6 +4574,23 @@ registerComputed('not', function(dependentKey) { }); /** + A computed property that converts the provided dependent property + into a boolean value. + + ```javascript + var Hampster = Ember.Object.extend({ + hasBananas: Ember.computed.bool('numBananas') + }); + var hampster = Hampster.create(); + hampster.get('hasBananas'); // false + hampster.set('numBananas', 0); + hampster.get('hasBananas'); // false + hampster.set('numBananas', 1); + hampster.get('hasBananas'); // true + hampster.set('numBananas', null); + hampster.get('hasBananas'); // false + ``` + @method computed.bool @for Ember @param {String} dependentKey @@ -4212,6 +4602,24 @@ registerComputed('bool', function(dependentKey) { }); /** + A computed property which matches the original value for the + dependent property against a given RegExp, returning `true` + if they values matches the RegExp and `false` if it does not. + + Example + + ```javascript + var User = Ember.Object.extend({ + hasValidEmail: Ember.computed.match('email', /^.+@.+\..+$/) + }); + var user = User.create({loggedIn: false}); + user.get('hasValidEmail'); // false + user.set('email', ''); + user.get('hasValidEmail'); // false + user.set('email', 'ember_hampster@example.com'); + user.get('hasValidEmail'); // true + ``` + @method computed.match @for Ember @param {String} dependentKey @@ -4225,6 +4633,23 @@ registerComputed('match', function(dependentKey, regexp) { }); /** + A computed property that returns true if the provided dependent property + is equal to the given value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + napTime: Ember.computed.equal('state', 'sleepy') + }); + var hampster = Hampster.create(); + hampster.get('napTime'); // false + hampster.set('state', 'sleepy'); + hampster.get('napTime'); // true + hampster.set('state', 'hungry'); + hampster.get('napTime'); // false + ``` + @method computed.equal @for Ember @param {String} dependentKey @@ -4237,6 +4662,23 @@ registerComputed('equal', function(dependentKey, value) { }); /** + A computed property that returns true if the provied dependent property + is greater than the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gt('numBananas', 10) + }); + var hampster = Hampster.create(); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 3); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 11); + hampster.get('hasTooManyBananas'); // true + ``` + @method computed.gt @for Ember @param {String} dependentKey @@ -4249,6 +4691,23 @@ registerComputed('gt', function(dependentKey, value) { }); /** + A computed property that returns true if the provided dependent property + is greater than or equal to the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gte('numBananas', 10) + }); + var hampster = Hampster.create(); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 3); + hampster.get('hasTooManyBananas'); // false + hampster.set('numBananas', 10); + hampster.get('hasTooManyBananas'); // true + ``` + @method computed.gte @for Ember @param {String} dependentKey @@ -4261,6 +4720,23 @@ registerComputed('gte', function(dependentKey, value) { }); /** + A computed property that returns true if the provided dependent property + is less than the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lt('numBananas', 3) + }); + var hampster = Hampster.create(); + hampster.get('needsMoreBananas'); // true + hampster.set('numBananas', 3); + hampster.get('needsMoreBananas'); // false + hampster.set('numBananas', 2); + hampster.get('needsMoreBananas'); // true + ``` + @method computed.lt @for Ember @param {String} dependentKey @@ -4273,6 +4749,23 @@ registerComputed('lt', function(dependentKey, value) { }); /** + A computed property that returns true if the provided dependent property + is less than or equal to the provided value. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lte('numBananas', 3) + }); + var hampster = Hampster.create(); + hampster.get('needsMoreBananas'); // true + hampster.set('numBananas', 5); + hampster.get('needsMoreBananas'); // false + hampster.set('numBananas', 3); + hampster.get('needsMoreBananas'); // true + ``` + @method computed.lte @for Ember @param {String} dependentKey @@ -4285,10 +4778,28 @@ registerComputed('lte', function(dependentKey, value) { }); /** + A computed property that performs a logical `and` on the + original values for the provided dependent properties. + + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') + }); + var hampster = Hampster.create(); + hampster.get('readyForCamp'); // false + hampster.set('hasTent', true); + hampster.get('readyForCamp'); // false + hampster.set('hasBackpack', true); + hampster.get('readyForCamp'); // true + ``` + @method computed.and @for Ember @param {String} dependentKey, [dependentKey...] - @return {Ember.ComputedProperty} computed property which peforms + @return {Ember.ComputedProperty} computed property which performs a logical `and` on the values of all the original values for properties. */ registerComputedWithProperties('and', function(properties) { @@ -4301,10 +4812,25 @@ registerComputedWithProperties('and', function(properties) { }); /** + A computed property that which performs a logical `or` on the + original values for the provided dependent properties. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + readyForRain: Ember.computed.or('hasJacket', 'hasUmbrella') + }); + var hampster = Hampster.create(); + hampster.get('readyForRain'); // false + hampster.set('hasJacket', true); + hampster.get('readyForRain'); // true + ``` + @method computed.or @for Ember @param {String} dependentKey, [dependentKey...] - @return {Ember.ComputedProperty} computed property which peforms + @return {Ember.ComputedProperty} computed property which performs a logical `or` on the values of all the original values for properties. */ registerComputedWithProperties('or', function(properties) { @@ -4317,6 +4843,21 @@ registerComputedWithProperties('or', function(properties) { }); /** + A computed property that returns the first truthy value + from a list of dependent properties. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + hasClothes: Ember.computed.any('hat', 'shirt') + }); + var hampster = Hampster.create(); + hampster.get('hasClothes'); // null + hampster.set('shirt', 'Hawaiian Shirt'); + hampster.get('hasClothes'); // 'Hawaiian Shirt' + ``` + @method computed.any @for Ember @param {String} dependentKey, [dependentKey...] @@ -4333,13 +4874,29 @@ registerComputedWithProperties('any', function(properties) { }); /** + A computed property that returns the array of values + for the provided dependent properties. + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + clothes: Ember.computed.map('hat', 'shirt') + }); + var hampster = Hampster.create(); + hampster.get('clothes'); // [null, null] + hampster.set('hat', 'Camp Hat'); + hampster.set('shirt', 'Camp Shirt'); + hampster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] + ``` + @method computed.map @for Ember @param {String} dependentKey, [dependentKey...] @return {Ember.ComputedProperty} computed property which maps values of all passed properties in to an array. */ -registerComputedWithProperties('map', function(properties) { +registerComputedWithProperties('collect', function(properties) { var res = []; for (var key in properties) { if (properties.hasOwnProperty(key)) { @@ -4389,18 +4946,14 @@ Ember.computed.alias = function(dependentKey) { }; /** - @method computed.oneWay - @for Ember - @param {String} 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 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 diverge from the upstream property. + Example + ```javascript User = Ember.Object.extend({ firstName: null, @@ -4422,6 +4975,12 @@ Ember.computed.alias = function(dependentKey) { user.get('firstName'); # 'Teddy' ``` + + @method computed.oneWay + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + one way computed property to the original value for property. */ Ember.computed.oneWay = function(dependentKey) { return Ember.computed(dependentKey, function() { @@ -4431,6 +4990,23 @@ Ember.computed.oneWay = function(dependentKey) { /** + A computed property that acts like a standard getter and setter, + but retruns the value at the provided `defaultPath` if the + property itself has not been set to a value + + Example + + ```javascript + var Hampster = Ember.Object.extend({ + wishList: Ember.computed.defaultTo('favoriteFood') + }); + var hampster = Hampster.create({favoriteFood: 'Banana'}); + hampster.get('wishList'); // 'Banana' + hampster.set('wishList', 'More Unit Tests'); + hampster.get('wishList'); // 'More Unit Tests' + hampster.get('favoriteFood'); // 'Banana' + ``` + @method computed.defaultTo @for Ember @param {String} defaultPath @@ -4446,6 +5022,8 @@ Ember.computed.defaultTo = function(defaultPath) { }); }; + + })(); @@ -4835,9 +5413,6 @@ define("backburner", run: function(target, method /*, args */) { var ret; - - var t2 = new Date().getTime(); - this.begin(); if (!method) { @@ -4863,15 +5438,6 @@ define("backburner", this.end(); } } - - // make sure we can output to the console and we're not running QUnit tests - if ((typeof console !== 'undefined') && console.log && !window.QUnit) { - var diff = new Date().getTime() - t2; - if (diff > 10) { - console.log("Backburner: " + (new Date() - t2) + "ms"); - } - } - return ret; }, @@ -5014,7 +5580,7 @@ define("backburner", clearTimeout(debouncee[2]); } - var timer = window.setTimeout(function() { + var timer = global.setTimeout(function() { if (!immediate) { self.run.apply(self, args); } @@ -5083,8 +5649,8 @@ define("backburner", function createAutorun(backburner) { backburner.begin(); autorun = global.setTimeout(function() { - backburner.end(); autorun = null; + backburner.end(); }); } @@ -5652,7 +6218,7 @@ Ember.run.throttle = function() { // Make sure it's not an autorun during testing function checkAutoRun() { if (!Ember.run.currentRunLoop) { - Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); + } } @@ -5731,7 +6297,7 @@ Binding.prototype = { This copies the Binding so it can be connected to another object. @method copy - @return {Ember.Binding} + @return {Ember.Binding} `this` */ copy: function () { var copy = new Binding(this._to, this._from); @@ -5816,7 +6382,7 @@ Binding.prototype = { @return {Ember.Binding} `this` */ connect: function(obj) { - Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj); + var fromPath = this._from, toPath = this._to; Ember.trySet(obj, toPath, getWithGlobals(obj, fromPath)); @@ -5841,7 +6407,7 @@ Binding.prototype = { @return {Ember.Binding} `this` */ disconnect: function(obj) { - Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); + var twoWay = !this._oneWay; @@ -5939,7 +6505,7 @@ function mixinProperties(to, from) { mixinProperties(Binding, { - /** + /* See `Ember.Binding.from`. @method from @@ -5950,7 +6516,7 @@ mixinProperties(Binding, { return binding.from.apply(binding, arguments); }, - /** + /* See `Ember.Binding.to`. @method to @@ -5975,6 +6541,7 @@ mixinProperties(Binding, { @param {Boolean} [flag] (Optional) passing nothing here will make the binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the binding two way again. + @return {Ember.Binding} `this` */ oneWay: function(from, flag) { var C = this, binding = new C(null, from); @@ -6199,13 +6766,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; @@ -6272,7 +6839,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; } @@ -6285,11 +6873,14 @@ function addNormalizedProperty(base, key, value, meta, descs, values, concats) { descs[key] = value; values[key] = undefined; } else { - // impl super if needed... - if (isMethod(value)) { - value = giveMethodSuper(base, key, value, values, descs); - } else if ((concats && a_indexOf.call(concats, key) >= 0) || key === 'concatenatedProperties') { + 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); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); } descs[key] = undefined; @@ -6298,7 +6889,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]; @@ -6307,19 +6898,21 @@ function mergeMixins(mixins, m, descs, values, base, keys) { for(var i=0, l=mixins.length; i<l; i++) { mixin = mixins[i]; - Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); + props = mixinProperties(m, mixin); if (props === CONTINUE) { continue; } if (props) { meta = Ember.meta(base); - concats = concatenatedProperties(props, values, base); + if (base.willMergeMixin) { base.willMergeMixin(props); } + 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 @@ -6389,26 +6982,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<l; i++) { - Ember[method](obj, paths[i], null, key); + Ember[updateMethod](obj, paths[i], null, key); } } } -function replaceObservers(obj, key, observer) { - var prevObserver = obj[key]; +function replaceObserversAndListeners(obj, key, observerOrListener) { + var prev = obj[key]; - updateObservers(obj, key, prevObserver, '__ember_observesBefore__', 'removeBeforeObserver'); - updateObservers(obj, key, prevObserver, '__ember_observes__', 'removeObserver'); + if ('function' === typeof prev) { + updateObserversAndListeners(obj, key, prev, '__ember_observesBefore__', 'removeBeforeObserver'); + updateObserversAndListeners(obj, key, prev, '__ember_observes__', 'removeObserver'); + updateObserversAndListeners(obj, key, prev, '__ember_listens__', 'removeListener'); + } - updateObservers(obj, key, observer, '__ember_observesBefore__', 'addBeforeObserver'); - updateObservers(obj, key, observer, '__ember_observes__', 'addObserver'); + if ('function' === typeof observerOrListener) { + updateObserversAndListeners(obj, key, observerOrListener, '__ember_observesBefore__', 'addBeforeObserver'); + updateObserversAndListeners(obj, key, observerOrListener, '__ember_observes__', 'addObserver'); + updateObserversAndListeners(obj, key, observerOrListener, '__ember_listens__', 'addListener'); + } } function applyMixin(obj, mixins, partial) { @@ -6418,6 +7015,7 @@ function applyMixin(obj, mixins, partial) { // Go through all mixins and hashes passed in, and: // // * Handle concatenated properties + // * Handle merged properties // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers @@ -6440,7 +7038,7 @@ function applyMixin(obj, mixins, partial) { if (desc === undefined && value === undefined) { continue; } - replaceObservers(obj, key, value); + replaceObserversAndListeners(obj, key, value); detectBinding(obj, key, value, m); defineProperty(obj, key, desc, value, m); } @@ -6481,7 +7079,7 @@ Ember.mixin = function(obj) { // Mix mixins into classes by passing them as the first arguments to // .extend. App.CommentView = Ember.View.extend(App.Editable, { - template: Ember.Handlebars.compile('{{#if isEditing}}...{{else}}...{{/if}}') + template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') }); commentView = App.CommentView.create(); @@ -6491,6 +7089,31 @@ Ember.mixin = function(obj) { Note that Mixins are created with `Ember.Mixin.create`, not `Ember.Mixin.extend`. + Note that mixins extend a constructor's prototype so arrays and object literals + defined as properties will be shared amongst objects that implement the mixin. + If you want to define an property in a mixin that is not shared, you can define + it either as a computed property or have it be created on initialization of the object. + + ```javascript + //filters array will be shared amongst any object implementing mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.A() + }); + + //filters will be a separate array for every object implementing the mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.computed(function(){return Ember.A();}) + }); + + //filters will be created as a separate array during the object's initialization + App.Filterable = Ember.Mixin.create({ + init: function() { + this._super(); + this.set("filters", Ember.A()); + } + }); + ``` + @class Mixin @namespace Ember */ @@ -6548,7 +7171,7 @@ MixinPrototype.reopen = function() { for(idx=0; idx < len; idx++) { mixin = arguments[idx]; - Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); + if (mixin instanceof Mixin) { mixins.push(mixin); @@ -6726,6 +7349,22 @@ Ember.aliasMethod = function(methodName) { // /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. + + Also available as `Function.prototype.observes` if prototype extensions are + enabled. + @method observer @for Ember @param {Function} func @@ -6738,9 +7377,23 @@ Ember.observer = function(func) { return func; }; -// If observers ever become asynchronous, Ember.immediateObserver -// must remain synchronous. /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver(function() { + // Executes whenever the "value" property changes + }, 'value') + }); + ``` + + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. + + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. + @method immediateObserver @for Ember @param {Function} func @@ -6750,36 +7403,49 @@ Ember.observer = function(func) { Ember.immediateObserver = function() { for (var i=0, l=arguments.length; i<l; i++) { var arg = arguments[i]; - Ember.assert("Immediate observers must observe internal properties only, not properties on other objects.", typeof arg !== "string" || arg.indexOf('.') === -1); + } return Ember.observer.apply(this, arguments); }; /** - When observers fire, they are called with the arguments `obj`, `keyName` - and `value`. In a typical observer, value is the new, post-change value. + When observers fire, they are called with the arguments `obj`, `keyName`. - A `beforeObserver` fires before a property changes. The `value` argument contains - the pre-change value. + Note, `@each.property` observer is called per each add or replace of an element + and it's not called with a specific enumeration item. + + A `beforeObserver` fires before a property changes. A `beforeObserver` is an alternative form of `.observesBefore()`. ```javascript App.PersonView = Ember.View.extend({ - valueWillChange: function (obj, keyName, value) { - this.changingFrom = value; - }.observesBefore('content.value'), - valueDidChange: function(obj, keyName, value) { + + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], + + valueWillChange: Ember.beforeObserver(function(obj, keyName) { + this.changingFrom = obj.get(keyName); + }, 'content.value'), + + valueDidChange: Ember.observer(function(obj, keyName) { // only run if updating a value already in the DOM if (this.get('state') === 'inDOM') { - var color = value > this.changingFrom ? 'green' : 'red'; - // logic + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; + // logic } - }.observes('content.value') + }, 'content.value'), + + friendsDidChange: Ember.observer(function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }, 'friends.@each.name') }); ``` + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. + @method beforeObserver @for Ember @param {Function} func @@ -6796,6 +7462,52 @@ Ember.beforeObserver = function(func) { +(function() { +// Provides a way to register library versions with ember. + +Ember.libraries = function() { + var libraries = []; + var coreLibIndex = 0; + + var getLibrary = function(name) { + for (var i = 0; i < libraries.length; i++) { + if (libraries[i].name === name) { + return libraries[i]; + } + } + }; + + libraries.register = function(name, version) { + if (!getLibrary(name)) { + libraries.push({name: name, version: version}); + } + }; + + libraries.registerCoreLibrary = function(name, version) { + if (!getLibrary(name)) { + libraries.splice(coreLibIndex++, 0, {name: name, version: version}); + } + }; + + libraries.deRegister = function(name) { + var lib = getLibrary(name); + if (lib) libraries.splice(libraries.indexOf(lib), 1); + }; + + libraries.each = function (callback) { + libraries.forEach(function(lib) { + callback(lib.name, lib.version); + }); + }; + return libraries; +}(); + +Ember.libraries.registerCoreLibrary('Ember', Ember.VERSION); + +})(); + + + (function() { /** Ember Metal @@ -6863,6 +7575,7 @@ define("rsvp/async", var browserGlobal = (typeof window !== 'undefined') ? window : {}; var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var async; + var local = (typeof global !== 'undefined') ? global : this; // old node function useNextTick() { @@ -6913,7 +7626,7 @@ define("rsvp/async", function useSetTimeout() { return function(callback, arg) { - setTimeout(function() { + local.setTimeout(function() { callback(arg); }, 1); }; @@ -7296,6 +8009,10 @@ define("rsvp/promise", }); return thenPromise; + }, + + fail: function(fail) { + return this.then(null, fail); } }; @@ -7398,19 +8115,36 @@ define("rsvp/resolve", __exports__.resolve = resolve; }); +define("rsvp/rethrow", + ["exports"], + function(__exports__) { + "use strict"; + var local = (typeof global === "undefined") ? this : global; + + function rethrow(reason) { + local.setTimeout(function() { + throw reason; + }); + throw reason; + } + + + __exports__.rethrow = rethrow; + }); 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__) { + ["rsvp/events","rsvp/promise","rsvp/node","rsvp/all","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __exports__) { "use strict"; var EventTarget = __dependency1__.EventTarget; var Promise = __dependency2__.Promise; var denodeify = __dependency3__.denodeify; var all = __dependency4__.all; var hash = __dependency5__.hash; - var defer = __dependency6__.defer; - var config = __dependency7__.config; - var resolve = __dependency8__.resolve; - var reject = __dependency9__.reject; + var rethrow = __dependency6__.rethrow; + var defer = __dependency7__.defer; + var config = __dependency8__.config; + var resolve = __dependency9__.resolve; + var reject = __dependency10__.reject; function configure(name, value) { config[name] = value; @@ -7421,25 +8155,35 @@ define("rsvp", __exports__.EventTarget = EventTarget; __exports__.all = all; __exports__.hash = hash; + __exports__.rethrow = rethrow; __exports__.defer = defer; __exports__.denodeify = denodeify; __exports__.configure = configure; __exports__.resolve = resolve; __exports__.reject = reject; }); - })(); (function() { +/** +@private +Public api for the container is still in flux. +The public api, specified on the application namespace should be considered the stable api. +// @module container +*/ + +/* + Flag to enable/disable model factory injections (disabled by default) + If model factory injections are enabled, models should not be + accessed globally (only through `container.lookupFactory('model:modelName'))`); +*/ +Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; + define("container", [], function() { - /** - A safe and simple inheriting object. - - @class InheritingDict - */ + // A safe and simple inheriting object. function InheritingDict(parent) { this.parent = parent; this.dict = {}; @@ -7512,7 +8256,7 @@ define("container", @method has @param {String} key - @returns {Boolean} + @return {Boolean} */ has: function(key) { var dict = this.dict; @@ -7546,20 +8290,25 @@ define("container", } }; - /** - A lightweight container that helps to assemble and decouple components. - @class Container - */ + // A lightweight container that helps to assemble and decouple components. + // Public api for the container is still in flux. + // The public api, specified on the application namespace should be considered the stable api. 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); } @@ -7633,7 +8382,7 @@ define("container", to correctly inherit from the current container. @method child - @returns {Container} + @return {Container} */ child: function() { var container = new Container(this); @@ -7647,7 +8396,7 @@ define("container", as expected. @method set - @param {Object} obkect + @param {Object} object @param {String} key @param {any} value */ @@ -7682,7 +8431,7 @@ define("container", factory = name; fullName = type; } else { - Ember.deprecate('register("'+type +'", "'+ name+'") is now deprecated in-favour of register("'+type+':'+name+'");', false); + fullName = type + ":" + name; } @@ -7703,6 +8452,7 @@ define("container", container.unregister('model:user') container.lookup('model:user') === undefined //=> true + ``` @method unregister @param {String} fullName @@ -7712,6 +8462,7 @@ define("container", this.registry.remove(normalizedName); this.cache.remove(normalizedName); + this.factoryCache.remove(normalizedName); this._options.remove(normalizedName); }, @@ -7745,7 +8496,7 @@ define("container", @method resolve @param {String} fullName - @returns {Function} fullName's factory + @return {Function} fullName's factory */ resolve: function(fullName) { return this.resolver(fullName) || this.registry.get(fullName); @@ -7776,12 +8527,23 @@ define("container", return fullName; }, + /** + @method makeToString + + @param {any} factory + @param {string} fullName + @return {function} toString function + */ + makeToString: function(factory, fullName) { + return factory.toString(); + }, + /** 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 there own locally scoped singletons. + to all have their own locally scoped singletons. ```javascript var container = new Container(); @@ -7826,7 +8588,7 @@ define("container", var value = instantiate(this, fullName); - if (!value) { return; } + if (value === undefined) { return; } if (isSingleton(this, fullName) && options.singleton !== false) { this.cache.set(fullName, value); @@ -7863,7 +8625,7 @@ define("container", }, /** - Allow registerying options for all factories of a type. + Allow registering options for all factories of a type. ```javascript var container = new Container(); @@ -7904,7 +8666,7 @@ define("container", this.optionsForType(type, options); }, - /* + /** @private Used only via `injection`. @@ -7943,20 +8705,10 @@ define("container", 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 - }); + addTypeInjection(this.typeInjections, type, property, fullName); }, - /* + /** Defines injection rules. These rules are used to inject dependencies onto objects when they @@ -7964,8 +8716,8 @@ define("container", Two forms of injections are possible: - * Injecting one fullName on another fullName - * Injecting one fullName on a type + * Injecting one fullName on another fullName + * Injecting one fullName on a type Example: @@ -7974,7 +8726,7 @@ define("container", container.register('source:main', Source); container.register('model:user', User); - container.register('model:post', PostController); + container.register('model:post', Post); // injecting one fullName on another fullName // eg. each user model gets a post model @@ -8007,8 +8759,104 @@ 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); }, /** @@ -8030,7 +8878,7 @@ define("container", item.destroy(); }); - delete this.parent; + this.parent = undefined; this.isDestroyed = true; }, @@ -8066,7 +8914,7 @@ define("container", injection = injections[i]; lookup = container.lookup(injection.fullName); - if (lookup) { + if (lookup !== undefined) { hash[injection.property] = lookup; } else { throw new Error('Attempting to inject an unknown injection: `' + injection.fullName + '`'); @@ -8093,32 +8941,83 @@ define("container", function factoryFor(container, fullName) { var name = container.normalize(fullName); - return container.resolve(name); + var factory = container.resolve(name); + var injectedFactory; + var cache = container.factoryCache; + var type = fullName.split(":")[0]; + + if (factory === undefined) { return; } + + if (cache.has(fullName)) { + return cache.get(fullName); + } + + if (!factory || typeof factory.extend !== 'function' || (!Ember.MODEL_FACTORY_INJECTIONS && type === 'model')) { + // TODO: think about a 'safe' merge style extension + // for now just fallback to create time injection + return factory; + } else { + + var injections = injectionsFor(container, fullName); + var factoryInjections = factoryInjectionsFor(container, fullName); + + factoryInjections._toString = container.makeToString(factory, fullName); + + injectedFactory = factory.extend(injections); + injectedFactory.reopenClass(factoryInjections); + + cache.set(fullName, injectedFactory); + + return injectedFactory; + } + } + + function injectionsFor(container ,fullName) { + var splitName = fullName.split(":"), + type = splitName[0], + injections = []; + + injections = injections.concat(container.typeInjections.get(type) || []); + injections = injections.concat(container.injections[fullName] || []); + + injections = buildInjections(container, injections); + injections._debugContainerKey = fullName; + injections.container = container; + + return injections; + } + + function factoryInjectionsFor(container, fullName) { + var splitName = fullName.split(":"), + type = splitName[0], + factoryInjections = []; + + factoryInjections = factoryInjections.concat(container.factoryTypeInjections.get(type) || []); + factoryInjections = factoryInjections.concat(container.factoryInjections[fullName] || []); + + factoryInjections = buildInjections(container, factoryInjections); + factoryInjections._debugContainerKey = fullName; + + return factoryInjections; } function instantiate(container, fullName) { var factory = factoryFor(container, fullName); - var splitName = fullName.split(":"), - type = splitName[0], - value; - if (option(container, fullName, 'instantiate') === false) { return factory; } if (factory) { - var injections = []; - injections = injections.concat(container.typeInjections.get(type) || []); - injections = injections.concat(container.injections[fullName] || []); - - var hash = buildInjections(container, injections); - hash.container = container; - hash._debugContainerKey = fullName; - - value = factory.create(hash); - - return value; + if (typeof factory.extend === 'function') { + // assume the factory was extendable and is already injected + return factory.create(); + } else { + // assume the factory was extendable + // to create time injections + // TODO: support new'ing for instantiation and merge injections for pure JS Functions + return factory.create(injectionsFor(container, fullName)); + } } } @@ -8137,6 +9036,25 @@ define("container", container.cache.dict = {}; } + function addTypeInjection(rules, type, property, fullName) { + var injections = rules.get(type); + + if (!injections) { + injections = []; + rules.set(type, injections); + } + + injections.push({ + property: property, + fullName: fullName + }); + } + + function addInjection(rules, factoryName, property, injectionName) { + var injections = rules[factoryName] = rules[factoryName] || []; + injections.push({ property: property, fullName: injectionName }); + } + return Container; }); @@ -8272,7 +9190,6 @@ function _copy(obj, deep, seen, copies) { // avoid cyclical loops if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; - Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); // IMPORTANT: this specific test will detect a native array only. Any other // object will need to implement Copyable. @@ -8453,529 +9370,6 @@ Ember.Error.prototype = Ember.create(Error.prototype); -(function() { -/** - Expose RSVP implementation - - @class RSVP - @namespace Ember - @constructor -*/ -Ember.RSVP = requireModule('rsvp'); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var STRING_DASHERIZE_REGEXP = (/[ _]/g); -var STRING_DASHERIZE_CACHE = {}; -var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); -var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); -var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); -var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); - -/** - Defines the hash of localized strings for the current language. Used by - the `Ember.String.loc()` helper. To localize, add string values to this - hash. - - @property STRINGS - @for Ember - @type Hash -*/ -Ember.STRINGS = {}; - -/** - Defines string helper methods including string formatting and localization. - Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be - added to the `String.prototype` as well. - - @class String - @namespace Ember - @static -*/ -Ember.String = { - - /** - Apply formatting options to the string. This will look for occurrences - of "%@" in your string and substitute them with the arguments you pass into - this method. If you want to control the specific order of replacement, - you can add a number after the key as well to indicate which argument - you want to insert. - - Ordered insertions are most useful when building loc strings where values - you need to insert may appear in different orders. - - ```javascript - "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" - "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" - ``` - - @method fmt - @param {String} str The string to format - @param {Array} formats An array of parameters to interpolate into string. - @return {String} formatted string - */ - fmt: function(str, formats) { - // first, replace any ORDERED replacements. - var idx = 0; // the current index for non-numerical replacements - return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { - argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; - s = formats[argIndex]; - return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); - }) ; - }, - - /** - Formats the passed string, but first looks up the string in the localized - strings hash. This is a convenient way to localize text. See - `Ember.String.fmt()` for more information on formatting. - - Note that it is traditional but not required to prefix localized string - keys with an underscore or other character so you can easily identify - localized strings. - - ```javascript - Ember.STRINGS = { - '_Hello World': 'Bonjour le monde', - '_Hello %@ %@': 'Bonjour %@ %@' - }; - - Ember.String.loc("_Hello World"); // 'Bonjour le monde'; - Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; - ``` - - @method loc - @param {String} str The string to format - @param {Array} formats Optional array of parameters to interpolate into string. - @return {String} formatted string - */ - loc: function(str, formats) { - str = Ember.STRINGS[str] || str; - return Ember.String.fmt(str, formats) ; - }, - - /** - Splits a string into separate units separated by spaces, eliminating any - empty strings in the process. This is a convenience method for split that - is mostly useful when applied to the `String.prototype`. - - ```javascript - Ember.String.w("alpha beta gamma").forEach(function(key) { - console.log(key); - }); - - // > alpha - // > beta - // > gamma - ``` - - @method w - @param {String} str The string to split - @return {String} split string - */ - w: function(str) { return str.split(/\s+/); }, - - /** - Converts a camelized string into all lower case separated by underscores. - - ```javascript - 'innerHTML'.decamelize(); // 'inner_html' - 'action_name'.decamelize(); // 'action_name' - 'css-class-name'.decamelize(); // 'css-class-name' - 'my favorite items'.decamelize(); // 'my favorite items' - ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. - */ - decamelize: function(str) { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); - }, - - /** - Replaces underscores or spaces with dashes. - - ```javascript - 'innerHTML'.dasherize(); // 'inner-html' - 'action_name'.dasherize(); // 'action-name' - 'css-class-name'.dasherize(); // 'css-class-name' - 'my favorite items'.dasherize(); // 'my-favorite-items' - ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ - dasherize: function(str) { - var cache = STRING_DASHERIZE_CACHE, - hit = cache.hasOwnProperty(str), - ret; - - if (hit) { - return cache[str]; - } else { - ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); - cache[str] = ret; - } - - return ret; - }, - - /** - Returns the lowerCamelCase form of a string. - - ```javascript - 'innerHTML'.camelize(); // 'innerHTML' - 'action_name'.camelize(); // 'actionName' - 'css-class-name'.camelize(); // 'cssClassName' - 'my favorite items'.camelize(); // 'myFavoriteItems' - 'My Favorite Items'.camelize(); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ - camelize: function(str) { - return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { - return chr ? chr.toUpperCase() : ''; - }).replace(/^([A-Z])/, function(match, separator, chr) { - return match.toLowerCase(); - }); - }, - - /** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ - classify: function(str) { - var parts = str.split("."), - out = []; - - for (var i=0, l=parts.length; i<l; i++) { - var camelized = Ember.String.camelize(parts[i]); - out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1)); - } - - return out.join("."); - }, - - /** - More general than decamelize. Returns the lower\_case\_and\_underscored - form of a string. - - ```javascript - 'innerHTML'.underscore(); // 'inner_html' - 'action_name'.underscore(); // 'action_name' - 'css-class-name'.underscore(); // 'css_class_name' - 'my favorite items'.underscore(); // 'my_favorite_items' - ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. - */ - underscore: function(str) { - return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). - replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); - }, - - /** - Returns the Capitalized form of a string - - ```javascript - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' - ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. - */ - capitalize: function(str) { - return str.charAt(0).toUpperCase() + str.substr(1); - } - -}; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - - - -var fmt = Ember.String.fmt, - w = Ember.String.w, - loc = Ember.String.loc, - camelize = Ember.String.camelize, - decamelize = Ember.String.decamelize, - dasherize = Ember.String.dasherize, - underscore = Ember.String.underscore, - capitalize = Ember.String.capitalize, - classify = Ember.String.classify; - -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { - - /** - See `Ember.String.fmt`. - - @method fmt - @for String - */ - String.prototype.fmt = function() { - return fmt(this, arguments); - }; - - /** - See `Ember.String.w`. - - @method w - @for String - */ - String.prototype.w = function() { - return w(this); - }; - - /** - See `Ember.String.loc`. - - @method loc - @for String - */ - String.prototype.loc = function() { - return loc(this, arguments); - }; - - /** - See `Ember.String.camelize`. - - @method camelize - @for String - */ - String.prototype.camelize = function() { - return camelize(this); - }; - - /** - See `Ember.String.decamelize`. - - @method decamelize - @for String - */ - String.prototype.decamelize = function() { - return decamelize(this); - }; - - /** - See `Ember.String.dasherize`. - - @method dasherize - @for String - */ - String.prototype.dasherize = function() { - return dasherize(this); - }; - - /** - See `Ember.String.underscore`. - - @method underscore - @for String - */ - String.prototype.underscore = function() { - return underscore(this); - }; - - /** - See `Ember.String.classify`. - - @method classify - @for String - */ - String.prototype.classify = function() { - return classify(this); - }; - - /** - See `Ember.String.capitalize`. - - @method capitalize - @for String - */ - String.prototype.capitalize = function() { - return capitalize(this); - }; - -} - - -})(); - - - -(function() { -/** -@module ember -@submodule ember-runtime -*/ - -var a_slice = Array.prototype.slice; - -if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { - - /** - The `property` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - `true`, which is the default. - - Computed properties allow you to treat a function like a property: - - ```javascript - MyApp.president = Ember.Object.create({ - firstName: "Barack", - lastName: "Obama", - - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Call this flag to mark the function as a property - }.property() - }); - - MyApp.president.get('fullName'); // "Barack Obama" - ``` - - Treating a function like a property is useful because they can work with - bindings, just like any other property. - - Many computed properties have dependencies on other properties. For - example, in the above example, the `fullName` property depends on - `firstName` and `lastName` to determine its value. You can tell Ember - about these dependencies like this: - - ```javascript - MyApp.president = Ember.Object.create({ - firstName: "Barack", - lastName: "Obama", - - fullName: function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Tell Ember.js that this computed property depends on firstName - // and lastName - }.property('firstName', 'lastName') - }); - ``` - - Make sure you list these dependencies so Ember knows when to update - bindings that connect to a computed property. Changing a dependency - will not immediately trigger an update of the computed property, but - will instead clear the cache so that it is updated when the next `get` - is called on the property. - - See `Ember.ComputedProperty`, `Ember.computed`. - - @method property - @for Function - */ - Function.prototype.property = function() { - var ret = Ember.computed(this); - return ret.property.apply(ret, arguments); - }; - - /** - The `observes` extension of Javascript's Function prototype is available - when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is - true, which is the default. - - You can observe property changes simply by adding the `observes` - call to the end of your method declarations in classes that you write. - For example: - - ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property changes - }.observes('value') - }); - ``` - - See `Ember.Observable.observes`. - - @method observes - @for Function - */ - Function.prototype.observes = function() { - this.__ember_observes__ = a_slice.call(arguments); - return this; - }; - - /** - The `observesBefore` extension of Javascript's Function prototype is - available when `Ember.EXTEND_PROTOTYPES` or - `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. - - You can get notified when a property changes is about to happen by - by adding the `observesBefore` call to the end of your method - declarations in classes that you write. For example: - - ```javascript - Ember.Object.create({ - valueObserver: function() { - // Executes whenever the "value" property is about to change - }.observesBefore('value') - }); - ``` - - See `Ember.Observable.observesBefore`. - - @method observesBefore - @for Function - */ - Function.prototype.observesBefore = function() { - this.__ember_observesBefore__ = a_slice.call(arguments); - return this; - }; - -} - - -})(); - - - -(function() { - -})(); - - - (function() { /** @module ember @@ -9205,14 +9599,14 @@ Ember.Enumerable = Ember.Mixin.create({ }, /** - Alias for `mapProperty` + Alias for `mapBy` @method getEach @param {String} key name of the property @return {Array} The mapped array. */ getEach: function(key) { - return this.mapProperty(key); + return this.mapBy(key); }, /** @@ -9270,16 +9664,28 @@ Ember.Enumerable = Ember.Mixin.create({ Similar to map, this specialized function returns the value of the named property on all items in the enumeration. - @method mapProperty + @method mapBy @param {String} key name of the property @return {Array} The mapped array. */ - mapProperty: function(key) { + mapBy: function(key) { return this.map(function(next) { return get(next, key); }); }, + /** + Similar to map, this specialized function returns the value of the named + property on all items in the enumeration. + + @method mapProperty + @param {String} key name of the property + @return {Array} The mapped array. + @deprecated Use `mapBy` instead + */ + + mapProperty: Ember.aliasMethod('mapBy'), + /** Returns an array with all of the items in the enumeration that the passed function returns true for. This method corresponds to `filter()` defined in @@ -9351,15 +9757,46 @@ Ember.Enumerable = Ember.Mixin.create({ can pass an optional second argument with the target value. Otherwise this will match any property that evaluates to `true`. - @method filterProperty + @method filterBy @param {String} key the property to test @param {String} [value] optional value to test against. @return {Array} filtered array */ - filterProperty: function(key, value) { + filterBy: function(key, value) { return this.filter(iter.apply(this, arguments)); }, + /** + Returns an array with just the items with the matched property. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to `true`. + + @method filterProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} filtered array + @deprecated Use `filterBy` instead + */ + filterProperty: Ember.aliasMethod('filterBy'), + + /** + Returns an array with the items that do not have truthy values for + key. You can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to false. + + @method rejectBy + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Array} rejected array + */ + rejectBy: function(key, value) { + var exactValue = function(item) { return get(item, key) === value; }, + hasValue = function(item) { return !!get(item, key); }, + use = (arguments.length === 2 ? exactValue : hasValue); + + return this.reject(use); + }, + /** Returns an array with the items that do not have truthy values for key. You can pass an optional second argument with the target value. Otherwise @@ -9369,14 +9806,9 @@ Ember.Enumerable = Ember.Mixin.create({ @param {String} key the property to test @param {String} [value] optional value to test against. @return {Array} rejected array + @deprecated Use `rejectBy` instead */ - rejectProperty: function(key, value) { - var exactValue = function(item) { return get(item, key) === value; }, - hasValue = function(item) { return !!get(item, key); }, - use = (arguments.length === 2 ? exactValue : hasValue); - - return this.reject(use); - }, + rejectProperty: Ember.aliasMethod('rejectBy'), /** Returns the first item in the array for which the callback returns true. @@ -9429,15 +9861,30 @@ Ember.Enumerable = Ember.Mixin.create({ This method works much like the more generic `find()` method. - @method findProperty + @method findBy @param {String} key the property to test @param {String} [value] optional value to test against. @return {Object} found item or `undefined` */ - findProperty: function(key, value) { + findBy: function(key, value) { return this.find(iter.apply(this, arguments)); }, + /** + Returns the first item with a property matching the passed value. You + can pass an optional second argument with the target value. Otherwise + this will match any property that evaluates to `true`. + + This method works much like the more generic `find()` method. + + @method findProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Object} found item or `undefined` + @deprecated Use `findBy` instead + */ + findProperty: Ember.aliasMethod('findBy'), + /** Returns `true` if the passed function returns true for every item in the enumeration. This corresponds with the `every()` method in JavaScript 1.6. @@ -9480,15 +9927,65 @@ Ember.Enumerable = Ember.Mixin.create({ Returns `true` if the passed property resolves to `true` for all items in the enumerable. This method is often simpler/faster than using a callback. - @method everyProperty + @method everyBy @param {String} key the property to test @param {String} [value] optional value to test against. @return {Boolean} */ - everyProperty: function(key, value) { + everyBy: function(key, value) { return this.every(iter.apply(this, arguments)); }, + /** + Returns `true` if the passed property resolves to `true` for all items in + the enumerable. This method is often simpler/faster than using a callback. + + @method everyProperty + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Boolean} + @deprecated Use `everyBy` instead + */ + everyProperty: Ember.aliasMethod('everyBy'), + + /** + Returns `true` if the passed function returns true for any item in the + enumeration. This corresponds with the `some()` method in JavaScript 1.6. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(item, index, enumerable); + ``` + + - `item` is the current item in the iteration. + - `index` is the current index in the iteration. + - `enumerable` is the enumerable object itself. + + It should return the `true` to include the item in the results, `false` + otherwise. + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. This is a good way + to give your iterator function access to the current object. + + Usage Example: + + ```javascript + if (people.any(isManager)) { Paychecks.addBiggerBonus(); } + ``` + + @method any + @param {Function} callback The callback to execute + @param {Object} [target] The target object to use + @return {Boolean} `true` if the passed function returns `true` for any item + */ + any: function(callback, target) { + return !!this.find(function(x, idx, i) { + return !!callback.call(target, x, idx, i); + }); + }, /** Returns `true` if the passed function returns true for any item in the @@ -9522,11 +10019,21 @@ Ember.Enumerable = Ember.Mixin.create({ @param {Function} callback The callback to execute @param {Object} [target] The target object to use @return {Boolean} `true` if the passed function returns `true` for any item + @deprecated Use `any` instead */ - some: function(callback, target) { - return !!this.find(function(x, idx, i) { - return !!callback.call(target, x, idx, i); - }); + some: Ember.aliasMethod('any'), + + /** + Returns `true` if the passed property resolves to `true` for any item in + the enumerable. This method is often simpler/faster than using a callback. + + @method anyBy + @param {String} key the property to test + @param {String} [value] optional value to test against. + @return {Boolean} `true` if the passed function returns `true` for any item + */ + anyBy: function(key, value) { + return this.any(iter.apply(this, arguments)); }, /** @@ -9537,10 +10044,9 @@ Ember.Enumerable = Ember.Mixin.create({ @param {String} key the property to test @param {String} [value] optional value to test against. @return {Boolean} `true` if the passed function returns `true` for any item + @deprecated Use `anyBy` instead */ - someProperty: function(key, value) { - return this.some(iter.apply(this, arguments)); - }, + someProperty: Ember.aliasMethod('anyBy'), /** This will combine the values of the enumerator into a single value. It @@ -10249,6 +10755,9 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot return an enumerable that maps automatically to the named key on the member objects. + If you merely want to watch for any items being added or removed to the array, + use the `[]` property instead of `@each`. + @property @each */ '@each': Ember.computed(function() { @@ -10262,6 +10771,2207 @@ Ember.Array = Ember.Mixin.create(Ember.Enumerable, /** @scope Ember.Array.protot +(function() { +var e_get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + metaFor = Ember.meta, + addBeforeObserver = Ember.addBeforeObserver, + removeBeforeObserver = Ember.removeBeforeObserver, + addObserver = Ember.addObserver, + removeObserver = Ember.removeObserver, + ComputedProperty = Ember.ComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach, + // Here we explicitly don't allow `@each.foo`; it would require some special + // testing, but there's no particular reason why it should be disallowed. + eachPropertyPattern = /^(.*)\.@each\.(.*)/, + doubleEachPropertyPattern = /(.*\.@each){2,}/; + +function get(obj, key) { + if (Ember.FEATURES.isEnabled('reduceComputedSelf')) { + if (key === '@self') { + return obj; + } + } + + return e_get(obj, key); +} + +/* + Tracks changes to dependent arrays, as well as to properties of items in + dependent arrays. + + @class DependentArraysObserver +*/ +function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { + // user specified callbacks for `addedItem` and `removedItem` + this.callbacks = callbacks; + + // the computed property: remember these are shared across instances + this.cp = cp; + + // the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is + // associated with + this.instanceMeta = instanceMeta; + + // A map of array guids to dependentKeys, for the given context. We track + // this because we want to set up the computed property potentially before the + // dependent array even exists, but when the array observer fires, we lack + // enough context to know what to update: we can recover that context by + // getting the dependentKey. + this.dependentKeysByGuid = {}; + + // a map of dependent array guids -> Ember.TrackedArray instances. We use + // this to lazily recompute indexes for item property observers. + this.trackedArraysByGuid = {}; + + // This is used to coalesce item changes from property observers. + this.changedItems = {}; +} + +function ItemPropertyObserverContext (dependentArray, index, trackedArray) { + + + this.dependentArray = dependentArray; + this.index = index; + this.item = dependentArray.objectAt(index); + this.trackedArray = trackedArray; + this.beforeObserver = null; + this.observer = null; + + this.destroyed = false; +} + +DependentArraysObserver.prototype = { + setValue: function (newValue) { + this.instanceMeta.setValue(newValue); + }, + getValue: function () { + return this.instanceMeta.getValue(); + }, + + setupObservers: function (dependentArray, dependentKey) { + + + this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; + + dependentArray.addArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + + if (this.cp._itemPropertyKeys[dependentKey]) { + this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); + } + }, + + teardownObservers: function (dependentArray, dependentKey) { + var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; + + delete this.dependentKeysByGuid[guidFor(dependentArray)]; + + this.teardownPropertyObservers(dependentKey, itemPropertyKeys); + + dependentArray.removeArrayObserver(this, { + willChange: 'dependentArrayWillChange', + didChange: 'dependentArrayDidChange' + }); + }, + + setupPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArray = get(this.instanceMeta.context, dependentKey), + length = get(dependentArray, 'length'), + observerContexts = new Array(length); + + this.resetTransformations(dependentKey, observerContexts); + + forEach(dependentArray, function (item, index) { + var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); + observerContexts[index] = observerContext; + + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + }, this); + }, + + teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { + var dependentArrayObserver = this, + trackedArray = this.trackedArraysByGuid[dependentKey], + beforeObserver, + observer, + item; + + if (!trackedArray) { return; } + + trackedArray.apply(function (observerContexts, offset, operation) { + if (operation === Ember.TrackedArray.DELETE) { return; } + + forEach(observerContexts, function (observerContext) { + observerContext.destroyed = true; + beforeObserver = observerContext.beforeObserver; + observer = observerContext.observer; + item = observerContext.item; + + forEach(itemPropertyKeys, function (propertyKey) { + removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); + removeObserver(item, propertyKey, dependentArrayObserver, observer); + }); + }); + }); + }, + + createPropertyObserverContext: function (dependentArray, index, trackedArray) { + var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); + + this.createPropertyObserver(observerContext); + + return observerContext; + }, + + createPropertyObserver: function (observerContext) { + var dependentArrayObserver = this; + + observerContext.beforeObserver = function (obj, keyName) { + return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + observerContext.observer = function (obj, keyName) { + return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); + }; + }, + + resetTransformations: function (dependentKey, observerContexts) { + this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); + }, + + addTransformation: function (dependentKey, index, newItems) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + if (trackedArray) { + trackedArray.addItems(index, newItems); + } + }, + + removeTransformation: function (dependentKey, index, removedCount) { + var trackedArray = this.trackedArraysByGuid[dependentKey]; + + if (trackedArray) { + return trackedArray.removeItems(index, removedCount); + } + + return []; + }, + + updateIndexes: function (trackedArray, array) { + var length = get(array, 'length'); + // OPTIMIZE: we could stop updating once we hit the object whose observer + // fired; ie partially apply the transformations + trackedArray.apply(function (observerContexts, offset, operation) { + // we don't even have observer contexts for removed items, even if we did, + // they no longer have any index in the array + if (operation === Ember.TrackedArray.DELETE) { return; } + if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { + // If we update many items we don't want to walk the array each time: we + // only need to update the indexes at most once per run loop. + return; + } + + forEach(observerContexts, function (context, index) { + context.index = index + offset; + }); + }); + }, + + dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { + var removedItem = this.callbacks.removedItem, + changeMeta, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], + item, + itemIndex, + sliceIndex, + observerContexts; + + observerContexts = this.removeTransformation(dependentKey, index, removedCount); + + function removeObservers(propertyKey) { + observerContexts[sliceIndex].destroyed = true; + removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); + removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); + } + + for (sliceIndex = removedCount - 1; sliceIndex >= 0; --sliceIndex) { + itemIndex = index + sliceIndex; + item = dependentArray.objectAt(itemIndex); + + forEach(itemPropertyKeys, removeObservers, this); + + changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( removedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + } + }, + + dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { + var addedItem = this.callbacks.addedItem, + guid = guidFor(dependentArray), + dependentKey = this.dependentKeysByGuid[guid], + observerContexts = new Array(addedCount), + itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], + changeMeta, + observerContext; + + forEach(dependentArray.slice(index, index + addedCount), function (item, sliceIndex) { + if (itemPropertyKeys) { + observerContext = + observerContexts[sliceIndex] = + this.createPropertyObserverContext(dependentArray, index + sliceIndex, this.trackedArraysByGuid[dependentKey]); + forEach(itemPropertyKeys, function (propertyKey) { + addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); + addObserver(item, propertyKey, this, observerContext.observer); + }, this); + } + + changeMeta = createChangeMeta(dependentArray, item, index + sliceIndex, this.instanceMeta.propertyName, this.cp); + this.setValue( addedItem.call( + this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); + }, this); + + this.addTransformation(dependentKey, index, observerContexts); + }, + + itemPropertyWillChange: function (obj, keyName, array, observerContext) { + var guid = guidFor(obj); + + if (!this.changedItems[guid]) { + this.changedItems[guid] = { + array: array, + observerContext: observerContext, + obj: obj, + previousValues: {} + }; + } + + this.changedItems[guid].previousValues[keyName] = get(obj, keyName); + }, + + itemPropertyDidChange: function(obj, keyName, array, observerContext) { + Ember.run.once(this, 'flushChanges'); + }, + + flushChanges: function() { + var changedItems = this.changedItems, key, c, changeMeta; + + for (key in changedItems) { + c = changedItems[key]; + if (c.observerContext.destroyed) { continue; } + + this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); + + changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); + this.setValue( + this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + this.setValue( + this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); + } + this.changedItems = {}; + } +}; + +function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { + var meta = { + arrayChanged: dependentArray, + index: index, + item: item, + propertyName: propertyName, + property: property + }; + + if (previousValues) { + // previous values only available for item property changes + meta.previousValues = previousValues; + } + + return meta; +} + +function addItems (dependentArray, callbacks, cp, propertyName, meta) { + forEach(dependentArray, function (item, index) { + meta.setValue( callbacks.addedItem.call( + this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); + }, this); +} + +function reset(cp, propertyName) { + var callbacks = cp._callbacks(), + meta; + + if (cp._hasInstanceMeta(this, propertyName)) { + meta = cp._instanceMeta(this, propertyName); + meta.setValue(cp.resetValue(meta.getValue())); + } else { + meta = cp._instanceMeta(this, propertyName); + } + + if (cp.options.initialize) { + cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); + } +} + +function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { + this.context = context; + this.propertyName = propertyName; + this.cache = metaFor(context).cache; + + this.dependentArrays = {}; + this.sugarMeta = {}; + + this.initialValue = initialValue; +} + +ReduceComputedPropertyInstanceMeta.prototype = { + getValue: function () { + if (this.propertyName in this.cache) { + return this.cache[this.propertyName]; + } else { + return this.initialValue; + } + }, + + setValue: function(newValue) { + // This lets sugars force a recomputation, handy for very simple + // implementations of eg max. + if (newValue !== undefined) { + this.cache[this.propertyName] = newValue; + } else { + delete this.cache[this.propertyName]; + } + } +}; + +/** + A computed property whose dependent keys are arrays and which is updated with + "one at a time" semantics. + + @class ReduceComputedProperty + @namespace Ember + @extends Ember.ComputedProperty + @constructor +*/ +function ReduceComputedProperty(options) { + var cp = this; + + this.options = options; + this._instanceMetas = {}; + + this._dependentKeys = null; + // A map of dependentKey -> [itemProperty, ...] that tracks what properties of + // items in the array we must track to update this property. + this._itemPropertyKeys = {}; + this._previousItemPropertyKeys = {}; + + this.readOnly(); + this.cacheable(); + + this.recomputeOnce = function(propertyName) { + // What we really want to do is coalesce by <cp, propertyName>. + // We need a form of `scheduleOnce` that accepts an arbitrary token to + // coalesce by, in addition to the target and method. + Ember.run.once(this, recompute, propertyName); + }; + var recompute = function(propertyName) { + var dependentKeys = cp._dependentKeys, + meta = cp._instanceMeta(this, propertyName), + callbacks = cp._callbacks(); + + reset.call(this, cp, propertyName); + + forEach(cp._dependentKeys, function (dependentKey) { + var dependentArray = get(this, dependentKey), + previousDependentArray = meta.dependentArrays[dependentKey]; + + if (dependentArray === previousDependentArray) { + // The array may be the same, but our item property keys may have + // changed, so we set them up again. We can't easily tell if they've + // changed: the array may be the same object, but with different + // contents. + if (cp._previousItemPropertyKeys[dependentKey]) { + delete cp._previousItemPropertyKeys[dependentKey]; + meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); + } + } else { + meta.dependentArrays[dependentKey] = dependentArray; + + if (previousDependentArray) { + meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); + } + + if (dependentArray) { + meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); + } + } + }, this); + + forEach(cp._dependentKeys, function(dependentKey) { + var dependentArray = get(this, dependentKey); + if (dependentArray) { + addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); + } + }, this); + }; + + this.func = function (propertyName) { + + + recompute.call(this, propertyName); + + return cp._instanceMeta(this, propertyName).getValue(); + }; +} + +Ember.ReduceComputedProperty = ReduceComputedProperty; +ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); + +function defaultCallback(computedValue) { + return computedValue; +} + +ReduceComputedProperty.prototype._callbacks = function () { + if (!this.callbacks) { + var options = this.options; + this.callbacks = { + removedItem: options.removedItem || defaultCallback, + addedItem: options.addedItem || defaultCallback + }; + } + return this.callbacks; +}; + +ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName; + + return !!this._instanceMetas[key]; +}; + +ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { + var guid = guidFor(context), + key = guid + ':' + propertyName, + meta = this._instanceMetas[key]; + + if (!meta) { + meta = this._instanceMetas[key] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); + meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); + } + + return meta; +}; + +ReduceComputedProperty.prototype.initialValue = function () { + switch (typeof this.options.initialValue) { + case 'undefined': + throw new Error("reduce computed properties require an initial value: did you forget to pass one to Ember.reduceComputed?"); + case 'function': + return this.options.initialValue(); + default: + return this.options.initialValue; + } +}; + +ReduceComputedProperty.prototype.resetValue = function (value) { + return this.initialValue(); +}; + +ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { + this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; + this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); +}; + +ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { + if (this._itemPropertyKeys[dependentArrayKey]) { + this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; + this._itemPropertyKeys[dependentArrayKey] = []; + } +}; + +ReduceComputedProperty.prototype.property = function () { + var cp = this, + args = a_slice.call(arguments), + propertyArgs = [], + match, + dependentArrayKey, + itemPropertyKey; + + forEach(a_slice.call(arguments), function (dependentKey) { + if (doubleEachPropertyPattern.test(dependentKey)) { + throw new Error("Nested @each properties not supported: " + dependentKey); + } else if (match = eachPropertyPattern.exec(dependentKey)) { + dependentArrayKey = match[1]; + itemPropertyKey = match[2]; + cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); + propertyArgs.push(dependentArrayKey); + } else { + propertyArgs.push(dependentKey); + } + }); + + return ComputedProperty.prototype.property.apply(this, propertyArgs); +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) a reduce computed only operates + on the change instead of re-evaluating the entire array. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following four properties. + + `initialValue` - A value or function that will be used as the initial + value for the computed. If this property is a function the result of calling + the function will be used as the initial value. This property is required. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is removed + from the array. + + `addedItem` - A function that is called each time an element is added to + the array. + + + The `initialize` function has the following signature: + + ```javascript + function (initialValue, changeMeta, instanceMeta) + ``` + + `initialValue` - The value of the `initialValue` property from the + options object. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or `initialValue`. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); + }; + ``` + + Dependent keys may refer to `@self` to observe changes to the object itself, + which must be array-like, rather than a property of the object. This is + mostly useful for array proxies, to ensure objects are retrieved via + `objectAtContent`. This is how you could sort items by properties defined on an item controller. + + Example + + ```javascript + App.PeopleController = Ember.ArrayController.extend({ + itemController: 'person', + + sortedPeople: Ember.computed.sort('@self.@each.reversedName', function(personA, personB) { + // `reversedName` isn't defined on Person, but we have access to it via + // the item controller App.PersonController. If we'd used + // `content.@each.reversedName` above, we would be getting the objects + // directly and not have access to `reversedName`. + // + var reversedNameA = get(personA, 'reversedName'), + reversedNameB = get(personB, 'reversedName'); + + return Ember.compare(reversedNameA, reversedNameB); + }) + }); + + App.PersonController = Ember.ObjectController.extend({ + reversedName: function () { + return reverse(get(this, 'name')); + }.property('name') + }) + ``` + + @method reduceComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.reduceComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Error("Reduce Computed Property declared without an options hash"); + } + + if (Ember.isNone(options.initialValue)) { + throw new Error("Reduce Computed Property declared without an initial value"); + } + + var cp = new ReduceComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +var ReduceComputedProperty = Ember.ReduceComputedProperty, + a_slice = [].slice, + o_create = Ember.create, + forEach = Ember.EnumerableUtils.forEach; + +function ArrayComputedProperty() { + var cp = this; + + ReduceComputedProperty.apply(this, arguments); + + this.func = (function(reduceFunc) { + return function (propertyName) { + if (!cp._hasInstanceMeta(this, propertyName)) { + // When we recompute an array computed property, we need already + // retrieved arrays to be updated; we can't simply empty the cache and + // hope the array is re-retrieved. + forEach(cp._dependentKeys, function(dependentKey) { + Ember.addObserver(this, dependentKey, function() { + cp.recomputeOnce.call(this, propertyName); + }); + }, this); + } + + return reduceFunc.apply(this, arguments); + }; + })(this.func); + + return this; +} +Ember.ArrayComputedProperty = ArrayComputedProperty; +ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); +ArrayComputedProperty.prototype.initialValue = function () { + return Ember.A(); +}; +ArrayComputedProperty.prototype.resetValue = function (array) { + array.clear(); + return array; +}; + +/** + Creates a computed property which operates on dependent arrays and + is updated with "one at a time" semantics. When items are added or + removed from the dependent array(s) an array computed only operates + on the change instead of re-evaluating the entire array. This should + return an array, if you'd like to use "one at a time" semantics and + compute some value other then an array look at + `Ember.reduceComputed`. + + If there are more than one arguments the first arguments are + considered to be dependent property keys. The last argument is + required to be an options object. The options object can have the + following three properties. + + `initialize` - An optional initialize function. Typically this will be used + to set up state on the instanceMeta object. + + `removedItem` - A function that is called each time an element is + removed from the array. + + `addedItem` - A function that is called each time an element is + added to the array. + + + The `initialize` function has the following signature: + + ```javascript + function (array, changeMeta, instanceMeta) + ``` + + `array` - The initial value of the arrayComputed, an empty array. + + `changeMeta` - An object which contains meta information about the + computed. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + + The `removedItem` and `addedItem` functions both have the following signature: + + ```javascript + function (accumulatedValue, item, changeMeta, instanceMeta) + ``` + + `accumulatedValue` - The value returned from the last time + `removedItem` or `addedItem` was called or an empty array. + + `item` - the element added or removed from the array + + `changeMeta` - An object which contains meta information about the + change. It contains the following properties: + + - `property` the computed property + - `propertyName` the name of the property on the object + - `index` the index of the added or removed item + - `item` the added or removed item: this is exactly the same as + the second arg + - `arrayChanged` the array that triggered the change. Can be + useful when depending on multiple arrays. + + For property changes triggered on an item property change (when + depKey is something like `someArray.@each.someProperty`), + `changeMeta` will also contain the following property: + + - `previousValues` an object whose keys are the properties that changed on + the item, and whose values are the item's previous values. + + `previousValues` is important Ember coalesces item property changes via + Ember.run.once. This means that by the time removedItem gets called, item has + the new values, but you may need the previous value (eg for sorting & + filtering). + + `instanceMeta` - An object that can be used to store meta + information needed for calculating your computed. For example a + unique computed might use this to store the number of times a given + element is found in the dependent array. + + The `removedItem` and `addedItem` functions should return the accumulated + value. It is acceptable to not return anything (ie return undefined) + to invalidate the computation. This is generally not a good idea for + arrayComputed but it's used in eg max and min. + + Example + + ```javascript + Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback(item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); + }; + ``` + + @method arrayComputed + @for Ember + @param {String} [dependentKeys*] + @param {Object} options + @return {Ember.ComputedProperty} +*/ +Ember.arrayComputed = function (options) { + var args; + + if (arguments.length > 1) { + args = a_slice.call(arguments, 0, -1); + options = a_slice.call(arguments, -1)[0]; + } + + if (typeof options !== "object") { + throw new Error("Array Computed Property declared without an options hash"); + } + + var cp = new ArrayComputedProperty(options); + + if (args) { + cp.property.apply(cp, args); + } + + return cp; +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get, + set = Ember.set, + guidFor = Ember.guidFor, + merge = Ember.merge, + a_slice = [].slice, + forEach = Ember.EnumerableUtils.forEach, + map = Ember.EnumerableUtils.map; + +/** + A computed property that calculates the maximum value in the + dependent array. This will return `-Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + maxChildAge: Ember.computed.max('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('maxChildAge'); // -Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('maxChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('maxChildAge'); // 8 + ``` + + @method computed.max + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array +*/ +Ember.computed.max = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: -Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.max(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item < accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + A computed property that calculates the minimum value in the + dependent array. This will return `Infinity` when the dependent + array is empty. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age'), + minChildAge: Ember.computed.min('childAges') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('minChildAge'); // Infinity + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('minChildAge'); // 7 + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('minChildAge'); // 5 + ``` + + @method computed.min + @for Ember + @param {String} dependentKey + @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array +*/ +Ember.computed.min = function (dependentKey) { + return Ember.reduceComputed.call(null, dependentKey, { + initialValue: Infinity, + + addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + return Math.min(accumulatedValue, item); + }, + + removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { + if (item > accumulatedValue) { + return accumulatedValue; + } + } + }); +}; + +/** + Returns an array mapped via the callback + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + excitingChores: Ember.computed.map('chores', function(chore) { + return chore.toUpperCase() + '!'; + }) + }); + + var hampster = App.Hampster.create({chores: ['cook', 'clean', 'write more unit tests']}); + hampster.get('excitingChores'); // ['COOK!', 'CLEAN!', 'WRITE MORE UNIT TESTS!'] + ``` + + @method computed.map + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} an array mapped via the callback +*/ +Ember.computed.map = function(dependentKey, callback) { + var options = { + addedItem: function(array, item, changeMeta, instanceMeta) { + var mapped = callback.call(this, item); + array.insertAt(changeMeta.index, mapped); + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + array.removeAt(changeMeta.index, 1); + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Returns an array mapped to the specified key. + + Example + + ```javascript + App.Person = Ember.Object.extend({ + childAges: Ember.computed.mapBy('children', 'age') + }); + + var lordByron = App.Person.create({children: []}); + lordByron.get('childAges'); // [] + lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); + lordByron.get('childAges'); // [7] + lordByron.get('children').pushObjects([{name: 'Allegra Byron', age: 5}, {name: 'Elizabeth Medora Leigh', age: 8}]); + lordByron.get('childAges'); // [7, 5, 8] + ``` + + @method computed.mapBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @return {Ember.ComputedProperty} an array mapped to the specified key +*/ +Ember.computed.mapBy = function(dependentKey, propertyKey) { + var callback = function(item) { return get(item, propertyKey); }; + return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.mapProperty + @for Ember + @deprecated Use `Ember.computed.mapBy` instead + @param dependentKey + @param propertyKey +*/ +Ember.computed.mapProperty = Ember.computed.mapBy; + +/** + Filters the array by the callback. + + The callback method you provide should have the following signature: + + ```javascript + function(item); + ``` + + - `item` is the current item in the iteration. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filter('chores', function(chore) { + return !chore.done; + }) + }); + + var hampster = App.Hampster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filter + @for Ember + @param {String} dependentKey + @param {Function} callback + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filter = function(dependentKey, callback) { + var options = { + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.filteredArrayIndexes = new Ember.SubArray(); + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var match = !!callback.call(this, item), + filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); + + if (match) { + array.insertAt(filterIndex, item); + } + + return array; + }, + + removedItem: function(array, item, changeMeta, instanceMeta) { + var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); + + if (filterIndex > -1) { + array.removeAt(filterIndex); + } + + return array; + } + }; + + return Ember.arrayComputed(dependentKey, options); +}; + +/** + Filters the array by the property and value + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + remainingChores: Ember.computed.filterBy('chores', 'done', false) + }); + + var hampster = App.Hampster.create({chores: [ + {name: 'cook', done: true}, + {name: 'clean', done: true}, + {name: 'write more unit tests', done: false} + ]}); + hampster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] + ``` + + @method computed.filterBy + @for Ember + @param {String} dependentKey + @param {String} propertyKey + @param {String} value + @return {Ember.ComputedProperty} the filtered array +*/ +Ember.computed.filterBy = function(dependentKey, propertyKey, value) { + var callback; + + if (arguments.length === 2) { + callback = function(item) { + return get(item, propertyKey); + }; + } else { + callback = function(item) { + return get(item, propertyKey) === value; + }; + } + + return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); +}; + +/** + @method computed.filterProperty + @for Ember + @param dependentKey + @param propertyKey + @param value + @deprecated Use `Ember.computed.filterBy` instead +*/ +Ember.computed.filterProperty = Ember.computed.filterBy; + +/** + A computed property which returns a new array with all the unique + elements from one or more dependent arrays. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + uniqueFruits: Ember.computed.uniq('fruits') + }); + + var hampster = App.Hampster.create({fruits: [ + 'banana', + 'grape', + 'kale', + 'banana' + ]}); + hampster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] + ``` + + @method computed.uniq + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.uniq = function() { + var args = a_slice.call(arguments); + args.push({ + initialize: function(array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var guid = guidFor(item); + + if (!instanceMeta.itemCounts[guid]) { + instanceMeta.itemCounts[guid] = 1; + } else { + ++instanceMeta.itemCounts[guid]; + } + array.addObject(item); + return array; + }, + removedItem: function(array, item, _, instanceMeta) { + var guid = guidFor(item), + itemCounts = instanceMeta.itemCounts; + + if (--itemCounts[guid] === 0) { + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + Alias for [Ember.computed.uniq](/api/#method_computed_uniq). + + @method computed.union + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + unique elements from the dependent array +*/ +Ember.computed.union = Ember.computed.uniq; + +/** + A computed property which returns a new array with all the duplicated + elements from two or more dependeny arrays. + + Example + + ```javascript + var obj = Ember.Object.createWithMixins({ + adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], + charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], + friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') + }); + + obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] + ``` + + @method computed.intersect + @for Ember + @param {String} propertyKey* + @return {Ember.ComputedProperty} computes a new array with all the + duplicated elements from the dependent arrays +*/ +Ember.computed.intersect = function () { + var getDependentKeyGuids = function (changeMeta) { + return map(changeMeta.property._dependentKeys, function (dependentKey) { + return guidFor(dependentKey); + }); + }; + + var args = a_slice.call(arguments); + args.push({ + initialize: function (array, changeMeta, instanceMeta) { + instanceMeta.itemCounts = {}; + }, + + addedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + itemCounts = instanceMeta.itemCounts; + + if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + + if (++itemCounts[itemGuid][dependentGuid] === 1 && + numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { + + array.addObject(item); + } + return array; + }, + removedItem: function(array, item, changeMeta, instanceMeta) { + var itemGuid = guidFor(item), + dependentGuids = getDependentKeyGuids(changeMeta), + dependentGuid = guidFor(changeMeta.arrayChanged), + numberOfDependentArrays = changeMeta.property._dependentKeys.length, + numberOfArraysItemAppearsIn, + itemCounts = instanceMeta.itemCounts; + + if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } + if (--itemCounts[itemGuid][dependentGuid] === 0) { + delete itemCounts[itemGuid][dependentGuid]; + numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; + + if (numberOfArraysItemAppearsIn === 0) { + delete itemCounts[itemGuid]; + } + array.removeObject(item); + } + return array; + } + }); + return Ember.arrayComputed.apply(null, args); +}; + +/** + A computed property which returns a new array with all the + properties from the first dependent array that are not in the second + dependent array. + + Example + + ```javascript + App.Hampster = Ember.Object.extend({ + likes: ['banana', 'grape', 'kale'], + wants: Ember.computed.setDiff('likes', 'fruits') + }); + + var hampster = App.Hampster.create({fruits: [ + 'grape', + 'kale', + ]}); + hampster.get('wants'); // ['banana'] + ``` + + @method computed.setDiff + @for Ember + @param {String} setAProperty + @param {String} setBProperty + @return {Ember.ComputedProperty} computes a new array with all the + items from the first dependent array that are not in the second + dependent array +*/ +Ember.computed.setDiff = function (setAProperty, setBProperty) { + if (arguments.length !== 2) { + throw new Error("setDiff requires exactly two dependent arrays."); + } + return Ember.arrayComputed.call(null, setAProperty, setBProperty, { + addedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setA) { + if (!setB.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var setA = get(this, setAProperty), + setB = get(this, setBProperty); + + if (changeMeta.arrayChanged === setB) { + if (setA.contains(item)) { + array.addObject(item); + } + } else { + array.removeObject(item); + } + return array; + } + }); +}; + +function binarySearch(array, item, low, high) { + var mid, midItem, res, guidMid, guidItem; + + if (arguments.length < 4) { high = get(array, 'length'); } + if (arguments.length < 3) { low = 0; } + + if (low === high) { + return low; + } + + mid = low + Math.floor((high - low) / 2); + midItem = array.objectAt(mid); + + guidMid = _guidFor(midItem); + guidItem = _guidFor(item); + + if (guidMid === guidItem) { + return mid; + } + + res = this.order(midItem, item); + if (res === 0) { + res = guidMid < guidItem ? -1 : 1; + } + + + if (res < 0) { + return this.binarySearch(array, item, mid+1, high); + } else if (res > 0) { + return this.binarySearch(array, item, low, mid); + } + + return mid; + + function _guidFor(item) { + if (Ember.ObjectProxy.detectInstance(item)) { + return guidFor(get(item, 'content')); + } + return guidFor(item); + } +} + +/** + A computed property which returns a new array with all the + properties from the first dependent array sorted based on a property + or sort function. + + The callback method you provide should have the following signature: + + ```javascript + function(itemA, itemB); + ``` + + - `itemA` the first item to compare. + - `itemB` the second item to compare. + + This function should return `-1` when `itemA` should come before + `itemB`. It should return `1` when `itemA` should come after + `itemB`. If the `itemA` and `itemB` are equal this function should return `0`. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + todosSorting: ['name'], + sortedTodos: Ember.computed.sort('todos', 'todosSorting'), + priorityTodos: Ember.computed.sort('todos', function(a, b){ + if (a.priority > b.priority) { + return 1; + } else if (a.priority < b.priority) { + return -1; + } + return 0; + }), + }); + var todoList = ToDoList.create({todos: [ + {name: 'Unit Test', priority: 2}, + {name: 'Documentation', priority: 3}, + {name: 'Release', priority: 1} + ]}); + + todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] + todoList.get('priroityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] + ``` + + @method computed.sort + @for Ember + @param {String} dependentKey + @param {String or Function} sortDefinition a dependent key to an + array of sort properties or a function to use when sorting + @return {Ember.ComputedProperty} computes a new sorted array based + on the sort property array or callback function +*/ +Ember.computed.sort = function (itemsKey, sortDefinition) { + + + var initFn, sortPropertiesKey; + + if (typeof sortDefinition === 'function') { + initFn = function (array, changeMeta, instanceMeta) { + instanceMeta.order = sortDefinition; + instanceMeta.binarySearch = binarySearch; + }; + } else { + sortPropertiesKey = sortDefinition; + initFn = function (array, changeMeta, instanceMeta) { + function setupSortProperties() { + var sortPropertyDefinitions = get(this, sortPropertiesKey), + sortProperty, + sortProperties = instanceMeta.sortProperties = [], + sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, + idx, + asc; + + + changeMeta.property.clearItemPropertyKeys(itemsKey); + + forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { + if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { + sortProperty = sortPropertyDefinition.substring(0, idx); + asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; + } else { + sortProperty = sortPropertyDefinition; + asc = true; + } + + sortProperties.push(sortProperty); + sortPropertyAscending[sortProperty] = asc; + changeMeta.property.itemPropertyKey(itemsKey, sortProperty); + }); + + sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); + } + + function updateSortPropertiesOnce() { + Ember.run.once(this, updateSortProperties, changeMeta.propertyName); + } + + function updateSortProperties(propertyName) { + setupSortProperties.call(this); + changeMeta.property.recomputeOnce.call(this, propertyName); + } + + Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); + + setupSortProperties.call(this); + + + instanceMeta.order = function (itemA, itemB) { + var sortProperty, result, asc; + for (var i = 0; i < this.sortProperties.length; ++i) { + sortProperty = this.sortProperties[i]; + result = Ember.compare(get(itemA, sortProperty), get(itemB, sortProperty)); + + if (result !== 0) { + asc = this.sortPropertyAscending[sortProperty]; + return asc ? result : (-1 * result); + } + } + + return 0; + }; + + instanceMeta.binarySearch = binarySearch; + }; + } + + return Ember.arrayComputed.call(null, itemsKey, { + initialize: initFn, + + addedItem: function (array, item, changeMeta, instanceMeta) { + var index = instanceMeta.binarySearch(array, item); + array.insertAt(index, item); + return array; + }, + + removedItem: function (array, item, changeMeta, instanceMeta) { + var proxyProperties, index, searchItem; + + if (changeMeta.previousValues) { + proxyProperties = merge({ content: item }, changeMeta.previousValues); + + searchItem = Ember.ObjectProxy.create(proxyProperties); + } else { + searchItem = item; + } + + index = instanceMeta.binarySearch(array, searchItem); + array.removeAt(index); + return array; + } + }); +}; + +})(); + + + +(function() { +/** + Expose RSVP implementation + + Documentation can be found here: https://github.com/tildeio/rsvp.js/blob/master/README.md + + @class RSVP + @namespace Ember + @constructor +*/ +Ember.RSVP = requireModule('rsvp'); + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var STRING_DASHERIZE_REGEXP = (/[ _]/g); +var STRING_DASHERIZE_CACHE = {}; +var STRING_DECAMELIZE_REGEXP = (/([a-z])([A-Z])/g); +var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); +var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); +var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); + +/** + Defines the hash of localized strings for the current language. Used by + the `Ember.String.loc()` helper. To localize, add string values to this + hash. + + @property STRINGS + @for Ember + @type Hash +*/ +Ember.STRINGS = {}; + +/** + Defines string helper methods including string formatting and localization. + Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be + added to the `String.prototype` as well. + + @class String + @namespace Ember + @static +*/ +Ember.String = { + + /** + Apply formatting options to the string. This will look for occurrences + of "%@" in your string and substitute them with the arguments you pass into + this method. If you want to control the specific order of replacement, + you can add a number after the key as well to indicate which argument + you want to insert. + + Ordered insertions are most useful when building loc strings where values + you need to insert may appear in different orders. + + ```javascript + "Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" + "Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" + ``` + + @method fmt + @param {String} str The string to format + @param {Array} formats An array of parameters to interpolate into string. + @return {String} formatted string + */ + fmt: function(str, formats) { + // first, replace any ORDERED replacements. + var idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = formats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); + }) ; + }, + + /** + Formats the passed string, but first looks up the string in the localized + strings hash. This is a convenient way to localize text. See + `Ember.String.fmt()` for more information on formatting. + + Note that it is traditional but not required to prefix localized string + keys with an underscore or other character so you can easily identify + localized strings. + + ```javascript + Ember.STRINGS = { + '_Hello World': 'Bonjour le monde', + '_Hello %@ %@': 'Bonjour %@ %@' + }; + + Ember.String.loc("_Hello World"); // 'Bonjour le monde'; + Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; + ``` + + @method loc + @param {String} str The string to format + @param {Array} formats Optional array of parameters to interpolate into string. + @return {String} formatted string + */ + loc: function(str, formats) { + str = Ember.STRINGS[str] || str; + return Ember.String.fmt(str, formats) ; + }, + + /** + Splits a string into separate units separated by spaces, eliminating any + empty strings in the process. This is a convenience method for split that + is mostly useful when applied to the `String.prototype`. + + ```javascript + Ember.String.w("alpha beta gamma").forEach(function(key) { + console.log(key); + }); + + // > alpha + // > beta + // > gamma + ``` + + @method w + @param {String} str The string to split + @return {String} split string + */ + w: function(str) { return str.split(/\s+/); }, + + /** + Converts a camelized string into all lower case separated by underscores. + + ```javascript + 'innerHTML'.decamelize(); // 'inner_html' + 'action_name'.decamelize(); // 'action_name' + 'css-class-name'.decamelize(); // 'css-class-name' + 'my favorite items'.decamelize(); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ + decamelize: function(str) { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); + }, + + /** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + 'innerHTML'.dasherize(); // 'inner-html' + 'action_name'.dasherize(); // 'action-name' + 'css-class-name'.dasherize(); // 'css-class-name' + 'my favorite items'.dasherize(); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ + dasherize: function(str) { + var cache = STRING_DASHERIZE_CACHE, + hit = cache.hasOwnProperty(str), + ret; + + if (hit) { + return cache[str]; + } else { + ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); + cache[str] = ret; + } + + return ret; + }, + + /** + Returns the lowerCamelCase form of a string. + + ```javascript + 'innerHTML'.camelize(); // 'innerHTML' + 'action_name'.camelize(); // 'actionName' + 'css-class-name'.camelize(); // 'cssClassName' + 'my favorite items'.camelize(); // 'myFavoriteItems' + 'My Favorite Items'.camelize(); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ + camelize: function(str) { + return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { + return chr ? chr.toUpperCase() : ''; + }).replace(/^([A-Z])/, function(match, separator, chr) { + return match.toLowerCase(); + }); + }, + + /** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ + classify: function(str) { + var parts = str.split("."), + out = []; + + for (var i=0, l=parts.length; i<l; i++) { + var camelized = Ember.String.camelize(parts[i]); + out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1)); + } + + return out.join("."); + }, + + /** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ + underscore: function(str) { + return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). + replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); + }, + + /** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ + capitalize: function(str) { + return str.charAt(0).toUpperCase() + str.substr(1); + } + +}; + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + + + +var fmt = Ember.String.fmt, + w = Ember.String.w, + loc = Ember.String.loc, + camelize = Ember.String.camelize, + decamelize = Ember.String.decamelize, + dasherize = Ember.String.dasherize, + underscore = Ember.String.underscore, + capitalize = Ember.String.capitalize, + classify = Ember.String.classify; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { + + /** + See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). + + @method fmt + @for String + */ + String.prototype.fmt = function() { + return fmt(this, arguments); + }; + + /** + See [Ember.String.w](/api/classes/Ember.String.html#method_w). + + @method w + @for String + */ + String.prototype.w = function() { + return w(this); + }; + + /** + See [Ember.String.loc](/api/classes/Ember.String.html#method_loc). + + @method loc + @for String + */ + String.prototype.loc = function() { + return loc(this, arguments); + }; + + /** + See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). + + @method camelize + @for String + */ + String.prototype.camelize = function() { + return camelize(this); + }; + + /** + See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). + + @method decamelize + @for String + */ + String.prototype.decamelize = function() { + return decamelize(this); + }; + + /** + See [Ember.String.dasherize](/api/classes/Ember.String.html#method_dasherize). + + @method dasherize + @for String + */ + String.prototype.dasherize = function() { + return dasherize(this); + }; + + /** + See [Ember.String.underscore](/api/classes/Ember.String.html#method_underscore). + + @method underscore + @for String + */ + String.prototype.underscore = function() { + return underscore(this); + }; + + /** + See [Ember.String.classify](/api/classes/Ember.String.html#method_classify). + + @method classify + @for String + */ + String.prototype.classify = function() { + return classify(this); + }; + + /** + See [Ember.String.capitalize](/api/classes/Ember.String.html#method_capitalize). + + @method capitalize + @for String + */ + String.prototype.capitalize = function() { + return capitalize(this); + }; + +} + + +})(); + + + +(function() { +/** +@module ember +@submodule ember-runtime +*/ + +var a_slice = Array.prototype.slice; + +if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { + + /** + The `property` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + `true`, which is the default. + + Computed properties allow you to treat a function like a property: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Call this flag to mark the function as a property + }.property() + }); + + var president = MyApp.President.create({ + firstName: "Barack", + lastName: "Obama" + }); + + president.get('fullName'); // "Barack Obama" + ``` + + Treating a function like a property is useful because they can work with + bindings, just like any other property. + + Many computed properties have dependencies on other properties. For + example, in the above example, the `fullName` property depends on + `firstName` and `lastName` to determine its value. You can tell Ember + about these dependencies like this: + + ```javascript + MyApp.President = Ember.Object.extend({ + firstName: '', + lastName: '', + + fullName: function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember.js that this computed property depends on firstName + // and lastName + }.property('firstName', 'lastName') + }); + ``` + + Make sure you list these dependencies so Ember knows when to update + bindings that connect to a computed property. Changing a dependency + will not immediately trigger an update of the computed property, but + will instead clear the cache so that it is updated when the next `get` + is called on the property. + + See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). + + @method property + @for Function + */ + Function.prototype.property = function() { + var ret = Ember.computed(this); + return ret.property.apply(ret, arguments); + }; + + /** + The `observes` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. + + You can observe property changes simply by adding the `observes` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property changes + }.observes('value') + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `observesImmediately`. + + See `Ember.observer`. + + @method observes + @for Function + */ + Function.prototype.observes = function() { + this.__ember_observes__ = a_slice.call(arguments); + return this; + }; + + /** + The `observesImmediately` extension of Javascript's Function prototype is + available when `Ember.EXTEND_PROTOTYPES` or + `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. + + You can observe property changes simply by adding the `observesImmediately` + call to the end of your method declarations in classes that you write. + For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes immediately after the "value" property changes + }.observesImmediately('value') + }); + ``` + + In the future, `observes` may become asynchronous. In this event, + `observesImmediately` will maintain the synchronous behavior. + + See `Ember.immediateObserver`. + + @method observesImmediately + @for Function + */ + Function.prototype.observesImmediately = function() { + for (var i=0, l=arguments.length; i<l; i++) { + var arg = arguments[i]; + + } + + return this.observes.apply(this, arguments); + }; + + /** + The `observesBefore` extension of Javascript's Function prototype is + available when `Ember.EXTEND_PROTOTYPES` or + `Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. + + You can get notified when a property change is about to happen by + by adding the `observesBefore` call to the end of your method + declarations in classes that you write. For example: + + ```javascript + Ember.Object.extend({ + valueObserver: function() { + // Executes whenever the "value" property is about to change + }.observesBefore('value') + }); + ``` + + See `Ember.beforeObserver`. + + @method observesBefore + @for Function + */ + Function.prototype.observesBefore = function() { + this.__ember_observesBefore__ = a_slice.call(arguments); + return this; + }; + + /** + The `on` extension of Javascript's Function prototype is available + when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is + true, which is the default. + + You can listen for events simply by adding the `on` call to the end of + your method declarations in classes or mixins that you write. For example: + + ```javascript + Ember.Mixin.create({ + doSomethingWithElement: function() { + // Executes whenever the "didInsertElement" event fires + }.on('didInsertElement') + }); + ``` + + See `Ember.on`. + + @method on + @for Function + */ + Function.prototype.on = function() { + var events = a_slice.call(arguments); + this.__ember_listens__ = events; + return this; + }; +} + + +})(); + + + +(function() { + +})(); + + + (function() { /** @module ember @@ -10897,7 +13607,10 @@ Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable,/** @submodule ember-runtime */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, + set = Ember.set, + slice = Array.prototype.slice, + getProperties = Ember.getProperties; /** ## Overview @@ -11027,15 +13740,7 @@ Ember.Observable = Ember.Mixin.create({ @return {Hash} */ getProperties: function() { - var ret = {}; - var propertyNames = arguments; - if (arguments.length === 1 && Ember.typeOf(arguments[0]) === 'array') { - propertyNames = arguments[0]; - } - for(var i = 0; i < propertyNames.length; i++) { - ret[propertyNames[i]] = get(this, propertyNames[i]); - } - return ret; + return getProperties.apply(null, [this].concat(slice.call(arguments))); }, /** @@ -11043,7 +13748,7 @@ Ember.Observable = Ember.Mixin.create({ This method is generally very similar to calling `object[key] = value` or `object.key = value`, except that it provides support for computed - properties, the `unknownProperty()` method and property observers. + properties, the `setUnknownProperty()` method and property observers. ### Computed Properties @@ -11057,9 +13762,9 @@ Ember.Observable = Ember.Mixin.create({ ### Unknown Properties If you try to set a value on a key that is undefined in the target - object, then the `unknownProperty()` handler will be called instead. This + object, then the `setUnknownProperty()` handler will be called instead. This gives you an opportunity to implement complex "virtual" properties that - are not predefined on the object. If `unknownProperty()` returns + are not predefined on the object. If `setUnknownProperty()` returns undefined, then `set()` will simply set the value on the object. ### Property Observers @@ -11289,29 +13994,6 @@ Ember.Observable = Ember.Mixin.create({ return Ember.hasListeners(this, key+':change'); }, - /** - @deprecated - @method getPath - @param {String} path The property path to retrieve - @return {Object} The property value or undefined. - */ - getPath: function(path) { - Ember.deprecate("getPath is deprecated since get now supports paths"); - return this.get(path); - }, - - /** - @deprecated - @method setPath - @param {String} path The path to the property that will be set - @param {Object} value The value to set or `null`. - @return {Ember.Observable} - */ - setPath: function(path, value) { - Ember.deprecate("setPath is deprecated since set now supports paths"); - return this.set(path, value); - }, - /** Retrieves the value of a property, or a default value in the case that the property returns `undefined`. @@ -11344,7 +14026,7 @@ Ember.Observable = Ember.Mixin.create({ */ incrementProperty: function(keyName, increment) { if (Ember.isNone(increment)) { increment = 1; } - Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); + set(this, keyName, (get(this, keyName) || 0) + increment); return get(this, keyName); }, @@ -11364,7 +14046,7 @@ Ember.Observable = Ember.Mixin.create({ */ decrementProperty: function(keyName, decrement) { if (Ember.isNone(decrement)) { decrement = 1; } - Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); + set(this, keyName, (get(this, keyName) || 0) - decrement); return get(this, keyName); }, @@ -11515,9 +14197,13 @@ Ember.TargetActionSupport = Ember.Mixin.create({ */ triggerAction: function(opts) { opts = opts || {}; - var action = opts['action'] || get(this, 'action'), - target = opts['target'] || get(this, 'targetObject'), - actionContext = opts['actionContext'] || get(this, 'actionContextObject') || this; + var action = opts.action || get(this, 'action'), + target = opts.target || get(this, 'targetObject'), + actionContext = opts.actionContext; + + if (typeof actionContext === 'undefined') { + actionContext = get(this, 'actionContextObject') || this; + } if (target && action) { var ret; @@ -11525,7 +14211,7 @@ Ember.TargetActionSupport = Ember.Mixin.create({ if (target.send) { ret = target.send.apply(target, [action, actionContext]); } else { - Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); + ret = target[action].apply(target, [actionContext]); } @@ -11661,11 +14347,6 @@ Ember.Evented = Ember.Mixin.create({ Ember.sendEvent(this, name, args); }, - fire: function(name) { - Ember.deprecate("Ember.Evented#fire() has been deprecated in favor of trigger() for compatibility with jQuery. It will be removed in 1.0. Please update your code to call trigger() instead."); - this.trigger.apply(this, arguments); - }, - /** Cancels subscription for given name, target, and method. @@ -11719,8 +14400,8 @@ Ember.DeferredMixin = Ember.Mixin.create({ Add handlers to be called when the Deferred object is resolved or rejected. @method then - @param {Function} doneCallback a callback function to be called when done - @param {Function} failCallback a callback function to be called when failed + @param {Function} resolve a callback function to be called when done + @param {Function} reject a callback function to be called when failed */ then: function(resolve, reject) { var deferred, promise, entity; @@ -11778,6 +14459,664 @@ Ember.DeferredMixin = Ember.Mixin.create({ (function() { +/** +@module ember +@submodule ember-runtime +*/ + +var get = Ember.get; + +/** + The `Ember.ActionHandler` mixin implements support for moving an `actions` + property to an `_actions` property at extend time, and adding `_actions` + to the object's mergedProperties list. + + `Ember.ActionHandler` is used internally by Ember in `Ember.View`, + `Ember.Controller`, and `Ember.Route`. + + @class ActionHandler + @namespace Ember +*/ +Ember.ActionHandler = Ember.Mixin.create({ + mergedProperties: ['_actions'], + + /** + @private + + Moves `actions` to `_actions` at extend time. Note that this currently + modifies the mixin themselves, which is technically dubious but + is practically of little consequence. This may change in the future. + + @method willMergeMixin + */ + willMergeMixin: function(props) { + if (props.actions && !props._actions) { + props._actions = Ember.merge(props._actions || {}, props.actions); + delete props.actions; + } + }, + + send: function(actionName) { + var args = [].slice.call(arguments, 1), target; + + if (this._actions && this._actions[actionName]) { + if (this._actions[actionName].apply(this, args) === true) { + // handler returned true, so this action will bubble + } else { + return; + } + } else if (this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { + if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { + // handler return true, so this action will bubble + } else { + return; + } + } + + if (target = get(this, 'target')) { + + target.send.apply(target, arguments); + } + } + +}); + +})(); + + + +(function() { +var set = Ember.set, get = Ember.get, + resolve = Ember.RSVP.resolve, + rethrow = Ember.RSVP.rethrow, + not = Ember.computed.not, + or = Ember.computed.or; + +/** + @module ember + @submodule ember-runtime + */ + +function installPromise(proxy, promise) { + promise.then(function(value) { + set(proxy, 'isFulfilled', true); + set(proxy, 'content', value); + + return value; + }, function(reason) { + set(proxy, 'isRejected', true); + set(proxy, 'reason', reason); + }).fail(rethrow); +} + +/** + A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. + + ```javascript + var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); + + var controller = ObjectPromiseController.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + controller.then(function(json){ + // the json + }, function(reason) { + // the reason why you have no json + }); + ``` + + the controller has bindable attributes which + track the promises life cycle + + ```javascript + controller.get('isPending') //=> true + controller.get('isSettled') //=> false + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> false + ``` + + When the the $.getJSON completes, and the promise is fulfilled + with json, the life cycle attributes will update accordingly. + + ```javascript + controller.get('isPending') //=> false + controller.get('isSettled') //=> true + controller.get('isRejected') //=> false + controller.get('isFulfilled') //=> true + ``` + + As the controller is an ObjectController, and the json now its content, + all the json properties will be available directly from the controller. + + ```javascript + // Assuming the following json: + { + firstName: 'Stefan', + lastName: 'Penner' + } + + // both properties will accessible on the controller + controller.get('firstName') //=> 'Stefan' + controller.get('lastName') //=> 'Penner' + ``` + + If the controller is backing a template, the attributes are + bindable from within that template + + ```handlebars + {{#if isPending}} + loading... + {{else}} + firstName: {{firstName}} + lastName: {{lastName}} + {{/if}} + ``` + @class Ember.PromiseProxyMixin +*/ +Ember.PromiseProxyMixin = Ember.Mixin.create({ + reason: null, + isPending: not('isSettled').readOnly(), + isSettled: or('isRejected', 'isFulfilled').readOnly(), + isRejected: false, + isFulfilled: false, + + promise: Ember.computed(function(key, promise) { + if (arguments.length === 2) { + promise = resolve(promise); + installPromise(this, promise); + return promise; + } else { + throw new Error("PromiseProxy's promise must be set"); + } + }), + + then: function(fulfill, reject) { + return get(this, 'promise').then(fulfill, reject); + } +}); + + +})(); + + + +(function() { + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + INSERT = 'i', + DELETE = 'd'; + +/** + An `Ember.TrackedArray` tracks array operations. It's useful when you want to + lazily compute the indexes of items in an array after they've been shifted by + subsequent operations. + + @class TrackedArray + @namespace Ember + @param {array} [items=[]] The array to be tracked. This is used just to get + the initial items for the starting state of retain:n. +*/ +Ember.TrackedArray = function (items) { + if (arguments.length < 1) { items = []; } + + var length = get(items, 'length'); + + if (length) { + this._content = [new ArrayOperation(RETAIN, length, items)]; + } else { + this._content = []; + } +}; + +Ember.TrackedArray.RETAIN = RETAIN; +Ember.TrackedArray.INSERT = INSERT; +Ember.TrackedArray.DELETE = DELETE; + +Ember.TrackedArray.prototype = { + + /** + Track that `newItems` were added to the tracked array at `index`. + + @method addItems + @param index + @param newItems + */ + addItems: function (index, newItems) { + var count = get(newItems, 'length'), + match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + composeIndex, + splitIndex, + splitItems, + splitArrayOperation, + newArrayOperation; + + newArrayOperation = new ArrayOperation(INSERT, count, newItems); + + if (arrayOperation) { + if (!match.split) { + // insert left of arrayOperation + this._content.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + } else { + // insert at end + this._content.push(newArrayOperation); + composeIndex = arrayOperationIndex; + } + + this._composeInsert(composeIndex); + }, + + /** + Track that `count` items were removed at `index`. + + @method removeItems + @param index + @param count + */ + removeItems: function (index, count) { + var match = this._findArrayOperation(index), + arrayOperation = match.operation, + arrayOperationIndex = match.index, + arrayOperationRangeStart = match.rangeStart, + newArrayOperation, + composeIndex; + + newArrayOperation = new ArrayOperation(DELETE, count); + if (!match.split) { + // insert left of arrayOperation + this._content.splice(arrayOperationIndex, 0, newArrayOperation); + composeIndex = arrayOperationIndex; + } else { + this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); + composeIndex = arrayOperationIndex + 1; + } + + return this._composeDelete(composeIndex); + }, + + /** + Apply all operations, reducing them to retain:n, for `n`, the number of + items in the array. + + `callback` will be called for each operation and will be passed the following arguments: + +* {array} items The items for the given operation +* {number} offset The computed offset of the items, ie the index in the +array of the first item for this operation. +* {string} operation The type of the operation. One of `Ember.TrackedArray.{RETAIN, DELETE, INSERT}` +* + + @method apply + @param {function} callback + */ + apply: function (callback) { + var items = [], + offset = 0; + + forEach(this._content, function (arrayOperation) { + callback(arrayOperation.items, offset, arrayOperation.operation); + + if (arrayOperation.operation !== DELETE) { + offset += arrayOperation.count; + items = items.concat(arrayOperation.items); + } + }); + + this._content = [new ArrayOperation(RETAIN, items.length, items)]; + }, + + /** + Return an ArrayOperationMatch for the operation that contains the item at `index`. + + @method _findArrayOperation + + @param {number} index the index of the item whose operation information + should be returned. + @private + */ + _findArrayOperation: function (index) { + var arrayOperationIndex, + len, + split = false, + arrayOperation, + arrayOperationRangeStart, + arrayOperationRangeEnd; + + // OPTIMIZE: we could search these faster if we kept a balanced tree. + // find leftmost arrayOperation to the right of `index` + for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._content.length; arrayOperationIndex < len; ++arrayOperationIndex) { + arrayOperation = this._content[arrayOperationIndex]; + + if (arrayOperation.operation === DELETE) { continue; } + + arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; + + if (index === arrayOperationRangeStart) { + break; + } else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { + split = true; + break; + } else { + arrayOperationRangeStart = arrayOperationRangeEnd + 1; + } + } + + return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); + }, + + _split: function (arrayOperationIndex, splitIndex, newArrayOperation) { + var arrayOperation = this._content[arrayOperationIndex], + splitItems = arrayOperation.items.slice(splitIndex), + splitArrayOperation = new ArrayOperation(arrayOperation.operation, splitItems.length, splitItems); + + // truncate LHS + arrayOperation.count = splitIndex; + arrayOperation.items = arrayOperation.items.slice(0, splitIndex); + + this._content.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); + }, + + // TODO: unify _composeInsert, _composeDelete + // see SubArray for a better implementation. + _composeInsert: function (index) { + var newArrayOperation = this._content[index], + leftArrayOperation = this._content[index-1], // may be undefined + rightArrayOperation = this._content[index+1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.operation, + rightOp = rightArrayOperation && rightArrayOperation.operation; + + if (leftOp === INSERT) { + // merge left + leftArrayOperation.count += newArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); + + if (rightOp === INSERT) { + // also merge right + leftArrayOperation.count += rightArrayOperation.count; + leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); + this._content.splice(index, 2); + } else { + // only merge left + this._content.splice(index, 1); + } + } else if (rightOp === INSERT) { + // merge right + newArrayOperation.count += rightArrayOperation.count; + newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); + this._content.splice(index + 1, 1); + } + }, + + _composeDelete: function (index) { + var arrayOperation = this._content[index], + deletesToGo = arrayOperation.count, + leftArrayOperation = this._content[index-1], // may be undefined + leftOp = leftArrayOperation && leftArrayOperation.operation, + nextArrayOperation, + nextOp, + nextCount, + removedItems = []; + + if (leftOp === DELETE) { + arrayOperation = leftArrayOperation; + index -= 1; + } + + for (var i = index + 1; deletesToGo > 0; ++i) { + nextArrayOperation = this._content[i]; + nextOp = nextArrayOperation.operation; + nextCount = nextArrayOperation.count; + + if (nextOp === DELETE) { + arrayOperation.count += nextCount; + continue; + } + + if (nextCount > deletesToGo) { + removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); + nextArrayOperation.count -= deletesToGo; + + // In the case where we truncate the last arrayOperation, we don't need to + // remove it; also the deletesToGo reduction is not the entirety of + // nextCount + i -= 1; + nextCount = deletesToGo; + + deletesToGo = 0; + } else { + removedItems = removedItems.concat(nextArrayOperation.items); + deletesToGo -= nextCount; + } + + if (nextOp === INSERT) { + arrayOperation.count -= nextCount; + } + } + + if (arrayOperation.count > 0) { + this._content.splice(index+1, i-1-index); + } else { + // The delete operation can go away; it has merely reduced some other + // operation, as in D:3 I:4 + this._content.splice(index, 1); + } + + return removedItems; + } +}; + +function ArrayOperation (operation, count, items) { + this.operation = operation; // RETAIN | INSERT | DELETE + this.count = count; + this.items = items; +} + +/** + Internal data structure used to include information when looking up operations + by item index. + + @method ArrayOperationMatch + @private + @property {ArrayOperation} operation + @property {number} index The index of `operation` in the array of operations. + @property {boolean} split Whether or not the item index searched for would + require a split for a new operation type. + @property {number} rangeStart The index of the first item in the operation, + with respect to the tracked array. The index of the last item can be computed + from `rangeStart` and `operation.count`. +*/ +function ArrayOperationMatch(operation, index, split, rangeStart) { + this.operation = operation; + this.index = index; + this.split = split; + this.rangeStart = rangeStart; +} + +})(); + + + +(function() { +var get = Ember.get, + forEach = Ember.EnumerableUtils.forEach, + RETAIN = 'r', + FILTER = 'f'; + +function Operation (type, count) { + this.type = type; + this.count = count; +} + +/** + An `Ember.SubArray` tracks an array in a way similar to, but more specialized + than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of + items within a filtered array. + + @class SubArray + @namespace Ember +*/ +Ember.SubArray = function (length) { + if (arguments.length < 1) { length = 0; } + + if (length > 0) { + this._operations = [new Operation(RETAIN, length)]; + } else { + this._operations = []; + } +}; + +Ember.SubArray.prototype = { + /** + Track that an item was added to the tracked array. + + @method addItem + + @param {number} index The index of the item in the tracked array. + @param {boolean} match `true` iff the item is included in the subarray. + + @return {number} The index of the item in the subarray. + */ + addItem: function(index, match) { + var returnValue = -1, + itemType = match ? RETAIN : FILTER, + self = this; + + this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + var newOperation, splitOperation; + + if (itemType === operation.type) { + ++operation.count; + } else if (index === rangeStart) { + // insert to the left of `operation` + self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); + } else { + newOperation = new Operation(itemType, 1); + splitOperation = new Operation(operation.type, rangeEnd - index + 1); + operation.count = index - rangeStart; + + self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); + } + + if (match) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } else { + returnValue = seenInSubArray; + } + } + + self._composeAt(operationIndex); + }, function(seenInSubArray) { + self._operations.push(new Operation(itemType, 1)); + + if (match) { + returnValue = seenInSubArray; + } + + self._composeAt(self._operations.length-1); + }); + + return returnValue; + }, + + /** + Track that an item was removed from the tracked array. + + @method removeItem + + @param {number} index The index of the item in the tracked array. + + @return {number} The index of the item in the subarray, or `-1` if the item + was not in the subarray. + */ + removeItem: function(index) { + var returnValue = -1, + self = this; + + this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { + if (operation.type === RETAIN) { + returnValue = seenInSubArray + (index - rangeStart); + } + + if (operation.count > 1) { + --operation.count; + } else { + self._operations.splice(operationIndex, 1); + self._composeAt(operationIndex); + } + }); + + return returnValue; + }, + + + _findOperation: function (index, foundCallback, notFoundCallback) { + var operationIndex, + len, + operation, + rangeStart, + rangeEnd, + seenInSubArray = 0; + + // OPTIMIZE: change to balanced tree + // find leftmost operation to the right of `index` + for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { + operation = this._operations[operationIndex]; + rangeEnd = rangeStart + operation.count - 1; + + if (index >= rangeStart && index <= rangeEnd) { + foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); + return; + } else if (operation.type === RETAIN) { + seenInSubArray += operation.count; + } + } + + notFoundCallback(seenInSubArray); + }, + + _composeAt: function(index) { + var op = this._operations[index], + otherOp; + + if (!op) { + // Composing out of bounds is a no-op, as when removing the last operation + // in the list. + return; + } + + if (index > 0) { + otherOp = this._operations[index-1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index-1, 1); + } + } + + if (index < this._operations.length-1) { + otherOp = this._operations[index+1]; + if (otherOp.type === op.type) { + op.count += otherOp.count; + this._operations.splice(index+1, 1); + } + } + } +}; })(); @@ -11810,6 +15149,8 @@ var set = Ember.set, get = Ember.get, generateGuid = Ember.generateGuid, meta = Ember.meta, rewatch = Ember.rewatch, + finishChains = Ember.finishChains, + sendEvent = Ember.sendEvent, destroy = Ember.destroy, schedule = Ember.run.schedule, Mixin = Ember.Mixin, @@ -11840,7 +15181,7 @@ function makeCtor() { } o_defineProperty(this, GUID_KEY, undefinedDescriptor); o_defineProperty(this, '_super', undefinedDescriptor); - var m = meta(this); + var m = meta(this), proto = m.proto; m.proto = this; if (initMixins) { // capture locally so we can clear the closed over variable @@ -11858,7 +15199,6 @@ function makeCtor() { for (var i = 0, l = props.length; i < l; i++) { var properties = props[i]; - Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); for (var keyName in properties) { if (!properties.hasOwnProperty(keyName)) { continue; } @@ -11878,8 +15218,8 @@ function makeCtor() { var desc = m.descs[keyName]; - Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty)); - Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); + + if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { var baseValue = this[keyName]; @@ -11909,19 +15249,11 @@ function makeCtor() { } } } - finishPartial(this, m); - var hasChains = (typeof m.chains) !== "undefined"; - delete m.proto; - - if (hasChains) { - if (m.chains.value() !== this) { - m.chains = m.chains.copy(this); - } - m.chains.didChange(true); - } - this.init.apply(this, arguments); + m.proto = proto; + finishChains(this); + sendEvent(this, "init"); }; Class.toString = Mixin.prototype.toString; @@ -11965,8 +15297,6 @@ CoreObject.PrototypeMixin = Mixin.create({ return this; }, - isInstance: true, - /** An overridable method called when objects are instantiated. By default, does nothing unless it is overridden during class definition. @@ -12055,7 +15385,10 @@ CoreObject.PrototypeMixin = Mixin.create({ are also concatenated, in addition to `classNames`. This feature is available for you to use throughout the Ember object model, - although typical app developers are likely to use it infrequently. + although typical app developers are likely to use it infrequently. Since + it changes expectations about behavior of properties, you should properly + document its usage in each individual concatenated property (to not + mislead your users to think they can override the property in a subclass). @property concatenatedProperties @type Array @@ -12109,6 +15442,8 @@ CoreObject.PrototypeMixin = Mixin.create({ /** Override to implement teardown. + + @method willDestroy */ willDestroy: Ember.K, @@ -12272,12 +15607,65 @@ var ClassMixin = Mixin.create({ return new C(); }, + /** + + Augments a constructor's prototype with additional + properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + o = MyObject.create(); + o.get('name'); // 'an object' + + MyObject.reopen({ + say: function(msg){ + console.log(msg); + } + }) + + o2 = MyObject.create(); + o2.say("hello"); // logs "hello" + + o.say("goodbye"); // logs "goodbye" + ``` + + To add functions and properties to the constructor itself, + see `reopenClass` + + @method reopen + */ reopen: function() { this.willReopen(); reopen.apply(this.PrototypeMixin, arguments); return this; }, + /** + Augments a constructor's own properties and functions: + + ```javascript + MyObject = Ember.Object.extend({ + name: 'an object' + }); + + + MyObject.reopenClass({ + canBuild: false + }); + + MyObject.canBuild; // false + o = MyObject.create(); + ``` + + To add functions and properties to instances of + a constructor by extending the constructor's prototype + see `reopen` + + @method reopenClass + */ reopenClass: function() { reopen.apply(this.ClassMixin, arguments); applyMixin(this, arguments, false); @@ -12327,7 +15715,6 @@ var ClassMixin = Mixin.create({ metaForProperty: function(key) { var desc = meta(this.proto(), false).descs[key]; - Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); return desc._meta || {}; }, @@ -12529,7 +15916,7 @@ function findNamespaces() { } if (isNamespace) { - Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); + obj[NAME_KEY] = prop; } } @@ -12556,6 +15943,8 @@ function classToString() { if (this[NAME_KEY]) { ret = this[NAME_KEY]; + } else if (this._toString) { + ret = this._toString; } else { var str = superClassString(this); if (str) { @@ -12745,7 +16134,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array _contentDidChange: Ember.observer(function() { var content = get(this, 'content'); - Ember.assert("Can't set ArrayProxy's content to itself", content !== this); this._setupContent(); }, 'content'), @@ -12775,7 +16163,6 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array var arrangedContent = get(this, 'arrangedContent'), len = arrangedContent ? get(arrangedContent, 'length') : 0; - Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); this._setupArrangedContent(); @@ -12820,7 +16207,7 @@ Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray,/** @scope Ember.Array _replace: function(idx, amt, objects) { var content = get(this, 'content'); - Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); + if (content) this.replaceContent(idx, amt, objects); return this; }, @@ -12952,7 +16339,9 @@ var get = Ember.get, removeBeforeObserver = Ember.removeBeforeObserver, removeObserver = Ember.removeObserver, propertyWillChange = Ember.propertyWillChange, - propertyDidChange = Ember.propertyDidChange; + propertyDidChange = Ember.propertyDidChange, + meta = Ember.meta, + defineProperty = Ember.defineProperty; function contentPropertyWillChange(content, contentKey) { var key = contentKey.slice(8); // remove "content." @@ -13043,7 +16432,7 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * */ content: null, _contentDidChange: Ember.observer(function() { - Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); + }, 'content'), isTruthy: Ember.computed.bool('content'), @@ -13070,29 +16459,19 @@ Ember.ObjectProxy = Ember.Object.extend(/** @scope Ember.ObjectProxy.prototype * }, setUnknownProperty: function (key, value) { + var m = meta(this); + if (m.proto === this) { + // if marked as prototype then just defineProperty + // rather than delegate + defineProperty(this, key, null, value); + return value; + } + var content = get(this, 'content'); - 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({ - create: function () { - var mixin, prototype, i, l, properties, keyName; - if (arguments.length) { - prototype = this.proto(); - for (i = 0, l = arguments.length; i < l; i++) { - properties = arguments[i]; - for (keyName in properties) { - if (!properties.hasOwnProperty(keyName) || keyName in prototype) { continue; } - if (!mixin) mixin = {}; - mixin[keyName] = null; - } - } - if (mixin) this._initMixins([mixin]); - } - return this._super.apply(this, arguments); - } }); })(); @@ -13107,7 +16486,8 @@ Ember.ObjectProxy.reopenClass({ var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; -var forEach = Ember.EnumerableUtils.forEach; +var forEach = Ember.EnumerableUtils.forEach, + indexOf = Ember.ArrayPolyfills.indexOf; var EachArray = Ember.Object.extend(Ember.Array, { @@ -13139,7 +16519,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'); @@ -13165,7 +16545,7 @@ function removeObserverForContentKey(content, keyName, proxy, idx, loc) { guid = guidFor(item); indicies = objects[guid]; - indicies[indicies.indexOf(loc)] = null; + indicies[indexOf.call(indicies, loc)] = null; } } } @@ -13314,7 +16694,7 @@ Ember.EachProxy = Ember.Object.extend({ */ -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; // Add Ember.Array to Array.prototype. Remove methods with native // implementations and supply some more optimized versions of generic methods @@ -13336,7 +16716,7 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember // primitive for array support. replace: function(idx, amt, objects) { - if (this.isFrozen) throw Ember.FROZEN_ERROR ; + if (this.isFrozen) throw Ember.FROZEN_ERROR; // if we replaced exactly the same number of items, then pass only the // replaced range. Otherwise, pass the full remaining array length @@ -13345,14 +16725,13 @@ var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember this.arrayContentWillChange(idx, amt, len); if (!objects || objects.length === 0) { - this.splice(idx, amt) ; + this.splice(idx, amt); } else { - var args = [idx, amt].concat(objects) ; - this.splice.apply(this,args) ; + replace(this, idx, amt, objects); } this.arrayContentDidChange(idx, amt, len); - return this ; + return this; }, // If you ask for an unknown property, then try to collect the value @@ -13429,7 +16808,26 @@ Ember.NativeArray = NativeArray; /** Creates an `Ember.NativeArray` from an Array like object. - Does not modify the original object. + Does not modify the original object. Ember.A is not needed if + `Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, + it is recommended that you use Ember.A when creating addons for + ember or when you can not garentee that `Ember.EXTEND_PROTOTYPES` + will be `true`. + + Example + + ```js + var Pagination = Ember.CollectionView.extend({ + tagName: 'ul', + classNames: ['pagination'], + init: function() { + this._super(); + if (!this.get('content')) { + this.set('content', Ember.A([])); + } + } + }); + ``` @method A @for Ember @@ -13442,7 +16840,17 @@ Ember.A = function(arr) { /** Activates the mixin on the Array.prototype if not already applied. Calling - this method more than once is safe. + this method more than once is safe. This will be called when ember is loaded + unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` + set to `false`. + + Example + + ```js + if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { + Ember.NativeArray.activate(); + } + ``` @method activate @for Ember.NativeArray @@ -13537,8 +16945,8 @@ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.is When using `Ember.Set`, you can observe the `"[]"` property to be alerted whenever the content changes. You can also add an enumerable observer to the set to be notified of specific objects that are added and - removed from the set. See `Ember.Enumerable` for more information on - enumerables. + removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) + for more information on enumerables. This is often unhelpful. If you are filtering sets of objects, for instance, it is very inefficient to re-filter all of the items each time the set @@ -13953,6 +17361,20 @@ var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; var loaded = {}; /** + +Detects when a specific package of Ember (e.g. 'Ember.Handlebars') +has fully loaded and is available for extension. + +The provided `callback` will be called with the `name` passed +resolved from a string into the object: + +```javascript +Ember.onLoad('Ember.Handlebars' function(hbars){ + hbars.registerHelper(...); +}); +``` + + @method onLoad @for Ember @param name {String} name of hook @@ -13970,6 +17392,10 @@ Ember.onLoad = function(name, callback) { }; /** + +Called when an Ember.js package (e.g Ember.Handlebars) has finished +loading. Triggers any callbacks registered for this event. + @method runLoadHooks @for Ember @param name {String} name of hook @@ -14008,39 +17434,18 @@ var get = Ember.get; compose Ember's controller layer: `Ember.Controller`, `Ember.ArrayController`, and `Ember.ObjectController`. - Within an `Ember.Router`-managed application single shared instaces of every - Controller object in your application's namespace will be added to the - application's `Ember.Router` instance. See `Ember.Application#initialize` - for additional information. - - ## Views - - By default a controller instance will be the rendering context - for its associated `Ember.View.` This connection is made during calls to - `Ember.ControllerMixin#connectOutlet`. - - Within the view's template, the `Ember.View` instance can be accessed - through the controller with `{{view}}`. - - ## Target Forwarding - - By default a controller will target your application's `Ember.Router` - instance. Calls to `{{action}}` within the template of a controller's view - are forwarded to the router. See `Ember.Handlebars.helpers.action` for - additional information. - @class ControllerMixin @namespace Ember */ -Ember.ControllerMixin = Ember.Mixin.create({ +Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { /* ducktype as a controller */ isController: true, /** - The object to which events from the view should be sent. + The object to which actions from the view should be sent. For example, when a Handlebars template uses the `{{action}}` helper, - it will attempt to send the event to the view's controller's `target`. + it will attempt to send the action to the view's controller's `target`. By default, a controller's `target` is set to the router after it is instantiated by `Ember.Application#initialize`. @@ -14058,16 +17463,16 @@ Ember.ControllerMixin = Ember.Mixin.create({ model: Ember.computed.alias('content'), - send: function(actionName) { - var args = [].slice.call(arguments, 1), target; + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, - 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')) { - Ember.assert("The target for controller " + this + " (" + target + ") did not define a `send` method", typeof target.send === 'function'); - target.send.apply(target, arguments); - } + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + + + this[actionName].apply(this, args); + return; } }); @@ -14116,6 +17521,29 @@ var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} ``` + If you add or remove the properties to sort by or change the sort direction the content + sort order will be automatically updated. + + ```javascript + songsController.set('sortProperties', ['title']); + songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} + + songsController.toggleProperty('sortAscending'); + songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} + ``` + + SortableMixin works by sorting the arrangedContent array, which is the array that + arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that + array will not display the sorted list: + + ```javascript + songsController.get('content').get('firstObject'); // Returns the unsorted original content + songsController.get('firstObject'); // Returns the sorted content. + ``` + + Although the sorted content can also be accessed through the arrangedContent property, + it is preferable to use the proxied class and not the arrangedContent array directly. + @class SortableMixin @namespace Ember @uses Ember.MutableEnumerable @@ -14125,6 +17553,9 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** Specifies which properties dictate the arrangedContent's sort order. + When specifying multiple properties the sorting will use properties + from the `sortProperties` array prioritized from first to last. + @property {Array} sortProperties */ sortProperties: null, @@ -14138,7 +17569,7 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { /** 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 + want to do custom comparisons. Functions must be of the type expected by Array#sort, i.e. return 0 if the two parameters are equal, return a negative value if the first parameter is smaller than the second or @@ -14164,7 +17595,6 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { sortAscending = get(this, 'sortAscending'), sortFunction = get(this, 'sortFunction'); - Ember.assert("you need to define `sortProperties`", !!sortProperties); forEach(sortProperties, function(propertyName) { if (result === 0) { @@ -14195,6 +17625,13 @@ Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { isSorted: Ember.computed.bool('sortProperties'), + /** + Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. + Also sets up observers for each sortProperty on each item in the content Array. + + @property arrangedContent + */ + arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { var content = get(this, 'content'), isSorted = get(this, 'isSorted'), @@ -14512,28 +17949,36 @@ Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, }, init: function() { - if (!this.get('content')) { Ember.defineProperty(this, 'content', undefined, Ember.A()); } this._super(); + this.set('_subControllers', Ember.A()); }, + content: Ember.computed(function () { + return Ember.A(); + }), + 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; }, @@ -14563,12 +18008,11 @@ 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. It is intended + to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying + content object, and to forward unhandled action attempts to its `target`. - `Ember.ObjectController` derives its functionality from its superclass + `Ember.ObjectController` derives this functionality from its superclass `Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. @class ObjectController @@ -14606,7 +18050,7 @@ Ember Runtime */ var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); + /** Alias for jQuery @@ -14750,7 +18194,7 @@ var setInnerHTML = function(element, html) { } else { // Firefox versions < 11 do not have support for element.outerHTML. var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); - Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", outerHTML); + var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], endTag = '</'+tagName+'>'; @@ -14852,9 +18296,14 @@ function escapeAttribute(value) { final representation. `Ember.RenderBuffer` will generate HTML which can be pushed to the DOM. + ```javascript + var buffer = Ember.RenderBuffer('div'); + ``` + @class RenderBuffer @namespace Ember @constructor + @param {String} tagName tag name (such as 'div' or 'p') used for the buffer */ Ember.RenderBuffer = function(tagName) { return new Ember._RenderBuffer(tagName); @@ -15401,13 +18850,11 @@ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.pro rootElement = Ember.$(get(this, 'rootElement')); - Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); - Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); - Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length); + + rootElement.addClass('ember-application'); - Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application')); for (event in events) { if (events.hasOwnProperty(event)) { @@ -15493,7 +18940,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(); } @@ -15601,6 +19050,7 @@ var get = Ember.get, set = Ember.set; var guidFor = Ember.guidFor; var a_forEach = Ember.EnumerableUtils.forEach; var a_addObject = Ember.EnumerableUtils.addObject; +var meta = Ember.meta; var childViewsProperty = Ember.computed(function() { var childViews = this._childViews, ret = Ember.A(), view = this; @@ -15618,7 +19068,7 @@ var childViewsProperty = Ember.computed(function() { ret.replace = function (idx, removedCount, addedViews) { if (view instanceof Ember.ContainerView) { - Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); + return view.replace(idx, removedCount, addedViews); } throw new Error("childViews is immutable"); @@ -15627,7 +19077,6 @@ var childViewsProperty = Ember.computed(function() { return ret; }); -Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); /** Global hash of shared templates. This will automatically be populated @@ -15641,7 +19090,13 @@ Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionali Ember.TEMPLATES = {}; /** - `Ember.CoreView` is + `Ember.CoreView` is an abstract class that exists to give view-like behavior + to both Ember's main view class `Ember.View` and other classes like + `Ember._SimpleMetamorphView` that don't need the fully functionaltiy of + `Ember.View`. + + Unless you have specific needs for `CoreView`, you will use `Ember.View` + in your applications. @class CoreView @namespace Ember @@ -15649,7 +19104,7 @@ Ember.TEMPLATES = {}; @uses Ember.Evented */ -Ember.CoreView = Ember.Object.extend(Ember.Evented, { +Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { isView: true, states: states, @@ -15764,6 +19219,18 @@ Ember.CoreView = Ember.Object.extend(Ember.Evented, { } }, + deprecatedSendHandles: function(actionName) { + return !!this[actionName]; + }, + + deprecatedSend: function(actionName) { + var args = [].slice.call(arguments, 1); + + + this[actionName].apply(this, args); + return; + }, + has: function(name) { return Ember.typeOf(this[name]) === 'function' || this._super(name); }, @@ -15865,7 +19332,7 @@ var EMPTY_ARRAY = []; The default HTML tag name used for a view's DOM representation is `div`. This can be customized by setting the `tagName` property. The following view -class: + class: ```javascript ParagraphView = Ember.View.extend({ @@ -15991,7 +19458,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 }); @@ -16014,7 +19481,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 }); @@ -16039,8 +19506,8 @@ class: will be removed. Both `classNames` and `classNameBindings` are concatenated properties. See - `Ember.Object` documentation for more information about concatenated - properties. + [Ember.Object](/api/classes/Ember.Object.html) documentation for more + information about concatenated properties. ## HTML Attributes @@ -16100,7 +19567,7 @@ class: Updates to the the property of an attribute binding will result in automatic update of the HTML attribute in the view's rendered HTML representation. - `attributeBindings` is a concatenated property. See `Ember.Object` + `attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) documentation for more information about concatenated properties. ## Templates @@ -16143,9 +19610,6 @@ class: Using a value for `templateName` that does not have a Handlebars template with a matching `data-template-name` attribute will throw an error. - Assigning a value to both `template` and `templateName` properties will throw - an error. - For views classes that may have a template later defined (e.g. as the block portion of a `{{view}}` Handlebars helper call in another template or in a subclass), you can provide a `defaultTemplate` property set to compiled @@ -16251,7 +19715,8 @@ class: </div> ``` - See `Handlebars.helpers.yield` for more information. + See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) + for more information. ## Responding to Browser Events @@ -16308,7 +19773,7 @@ class: }, eventManager: Ember.Object.create({ mouseEnter: function(event, view) { - // takes presedence over AView#mouseEnter + // takes precedence over AView#mouseEnter } }) }); @@ -16348,7 +19813,7 @@ class: ### Handlebars `{{action}}` Helper - See `Handlebars.helpers.action`. + See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). ### Event Names @@ -16403,8 +19868,8 @@ class: ## Handlebars `{{view}}` Helper Other `Ember.View` instances can be included as part of a view's template by - using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for - additional information. + using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) + for additional information. @class View @namespace Ember @@ -16479,7 +19944,6 @@ Ember.View = Ember.CoreView.extend( var templateName = get(this, 'templateName'), template = this.templateForName(templateName, 'template'); - Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); return template || get(this, 'defaultTemplate'); }).property('templateName'), @@ -16514,14 +19978,18 @@ Ember.View = Ember.CoreView.extend( var layoutName = get(this, 'layoutName'), layout = this.templateForName(layoutName, 'layout'); - Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); 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); + // the defaultContainer is deprecated var container = this.container || (Ember.Container && Ember.Container.defaultContainer); @@ -16549,14 +20017,6 @@ Ember.View = Ember.CoreView.extend( } }).volatile(), - /** - The parent context for this template. - */ - parentContext: function() { - var parentView = get(this, '_parentView'); - return parentView && get(parentView, '_context'); - }, - /** @private @@ -16654,7 +20114,7 @@ Ember.View = Ember.CoreView.extend( @deprecated */ nearestInstanceOf: function(klass) { - Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType."); + var view = get(this, 'parentView'); while (view) { @@ -16790,7 +20250,6 @@ Ember.View = Ember.CoreView.extend( // is the view's controller by default. A hash of data is also passed that provides // the template with access to the view and render buffer. - Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function'); // The template should write directly to the render buffer instead // of returning a string. output = template(context, { data: data }); @@ -17061,7 +20520,8 @@ 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 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); }); @@ -17082,7 +20542,8 @@ Ember.View = Ember.CoreView.extend( @return {Ember.View} received */ replaceIn: function(target) { - Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); + + this._insertElementLater(function() { Ember.$(target).empty(); @@ -17345,8 +20806,7 @@ Ember.View = Ember.CoreView.extend( */ _elementDidChange: Ember.observer(function() { this.forEachChildView(function(view) { - var meta = Ember.meta(view); - delete meta.cache['element']; + delete meta(view).cache.element; }); }, 'element'), @@ -17441,7 +20901,7 @@ Ember.View = Ember.CoreView.extend( visually challenged users navigate rich web applications. The full list of valid WAI-ARIA roles is available at: - http://www.w3.org/TR/wai-aria/roles#roles_categorization + [http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) @property ariaRole @type String @@ -17467,7 +20927,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the 'high' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['priority'] priority: 'high' }); @@ -17478,7 +20938,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the 'is-urgent' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isUrgent'] isUrgent: true }); @@ -17489,7 +20949,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the 'urgent' class to the view element - Ember.View.create({ + Ember.View.extend({ classNameBindings: ['isUrgent:urgent'] isUrgent: true }); @@ -17510,7 +20970,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Applies the type attribute to the element // with the value "button", like <div type="button"> - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['type'], type: 'button' }); @@ -17521,7 +20981,7 @@ Ember.View = Ember.CoreView.extend( ```javascript // Renders something like <div enabled="enabled"> - Ember.View.create({ + Ember.View.extend({ attributeBindings: ['enabled'], enabled: true }); @@ -17553,19 +21013,9 @@ Ember.View = Ember.CoreView.extend( // setup child views. be sure to clone the child views array first this._childViews = this._childViews.slice(); - Ember.assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array'); this.classNameBindings = Ember.A(this.classNameBindings.slice()); - 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) { @@ -17676,22 +21126,26 @@ 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 */ createChildView: function(view, attrs) { + if (!view) { + throw new TypeError("createChildViews first argument must exist"); + } + if (view.isView && view._parentView === this && view.container === this.container) { return view; } 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 @@ -17699,14 +21153,23 @@ 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); + + + attrs.templateData = get(this, 'templateData'); + view = View.create(attrs); } else { - Ember.assert('You must pass instance or subclass of View', view.isView); + + attrs.container = this.container; + + if (!get(view, 'templateData')) { + attrs.templateData = get(this, 'templateData'); + } Ember.setProperties(view, attrs); - if (!get(view, 'templateData')) { - set(view, 'templateData', get(this, 'templateData')); - } } return view; @@ -17915,7 +21378,7 @@ Ember.View.reopenClass({ Parse a path and return an object which holds the parsed properties. - For example a path like "content.isEnabled:enabled:disabled" wil return the + For example a path like "content.isEnabled:enabled:disabled" will return the following object: ```javascript @@ -18136,10 +21599,18 @@ Ember.merge(preRender, { var viewCollection = view.viewHierarchyCollection(); viewCollection.trigger('willInsertElement'); - // after createElement, the view will be in the hasElement state. + fn.call(view); - viewCollection.transitionTo('inDOM', false); - viewCollection.trigger('didInsertElement'); + + // We transition to `inDOM` if the element exists in the DOM + var element = view.get('element'); + while (element = element.parentNode) { + if (element === document) { + viewCollection.transitionTo('inDOM', false); + viewCollection.trigger('didInsertElement'); + } + } + }, renderToBufferIfNeeded: function(view, buffer) { @@ -18216,7 +21687,7 @@ Ember.merge(inBuffer, { }, empty: function() { - Ember.assert("Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications."); + }, renderToBufferIfNeeded: function (view, buffer) { @@ -18343,7 +21814,7 @@ Ember.merge(inDOM, { // Register the view for event handling. This hash is used by // Ember.EventDispatcher to dispatch incoming events. if (!view.isVirtual) { - Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); + Ember.View.views[view.elementId] = view; } @@ -18653,7 +22124,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { replace: function(idx, removedCount, addedViews) { var addedCount = addedViews ? get(addedViews, 'length') : 0; var self = this; - Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); + this.arrayContentWillChange(idx, removedCount, addedCount); this.childViewsWillChange(this._childViews, idx, removedCount); @@ -18678,7 +22149,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { length: Ember.computed(function () { return this._childViews.length; - }), + }).volatile(), /** @private @@ -18755,7 +22226,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); @@ -18775,7 +22249,7 @@ Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { _currentViewDidChange: Ember.observer(function() { var currentView = get(this, 'currentView'); if (currentView) { - Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); + this.pushObject(currentView); } }, 'currentView'), @@ -19005,11 +22479,6 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; manipulated. Instead, add, remove, replace items from its `content` property. This will trigger appropriate changes to its rendered HTML. - ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper - - `Ember.Handlebars` provides a helper specifically for adding - `CollectionView`s to templates. See `Ember.Handlebars.collection` for more - details @class CollectionView @namespace Ember @@ -19054,12 +22523,25 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie */ itemViewClass: Ember.View, + /** + Setup a CollectionView + + @method init + */ init: function() { var ret = this._super(); this._contentDidChange(); return ret; }, + /** + @private + + Invoked when the content property is about to change. Notifies observers that the + entire array content will change. + + @method _contentWillChange + */ _contentWillChange: Ember.beforeObserver(function() { var content = this.get('content'); @@ -19090,10 +22572,22 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie this.arrayDidChange(content, 0, null, len); }, 'content'), + /** + @private + + Ensure that the content implements Ember.Array + + @method _assertArrayLike + */ _assertArrayLike: function(content) { - Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); + }, + /** + Removes the content and content observers. + + @method destroy + */ destroy: function() { if (!this._super()) { return; } @@ -19107,6 +22601,19 @@ Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionVie return this; }, + /** + Called when a mutation to the underlying content array will occur. + + This method will remove any views that are no longer in the underlying + content array. + + Invokes whenever the content array itself will change. + + @method arrayWillChange + @param {Array} content the managed collection of objects + @param {Number} start the index at which the changes will occurr + @param {Number} removed number of object to be removed from content + */ arrayWillChange: function(content, start, removedCount) { // If the contents were empty before and this template collection has an // empty view remove it now. @@ -19152,17 +22659,19 @@ 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; + } + + for (idx = start; idx < start+added; idx++) { item = content.objectAt(idx); @@ -19174,27 +22683,50 @@ 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); }, + /** + Instantiates a view to be added to the childViews array during view + initialization. You generally will not call this method directly unless + you are overriding `createChildViews()`. Note that this method will + automatically configure the correct settings on the new view instance to + act as a child of the parent. + + The tag name for the view will be set to the tagName of the viewClass + passed in. + + @method createChildView + @param {Class} viewClass + @param {Hash} [attrs] Attributes to add + @return {Ember.View} new instance + */ createChildView: function(view, attrs) { 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; } @@ -19254,7 +22786,7 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; ```html <!-- app-profile template --> <h1>{{person.title}}</h1> - <img {{bindAttr src=person.avatar}}> + <img {{bind-attr src=person.avatar}}> <p class='signature'>{{person.signature}}</p> ``` @@ -19276,15 +22808,18 @@ var get = Ember.get, set = Ember.set, isNone = Ember.isNone; 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 - component. + component. Note that `Component` needs to be appended to the name of + your subclass like `AppProfileComponent`. For example, you could implement the action `hello` for the `app-profile` component: - ```js + ```javascript App.AppProfileComponent = Ember.Component.extend({ - hello: function(name) { - console.log("Hello", name) + actions: { + hello: function(name) { + console.log("Hello", name); + } } }); ``` @@ -19316,60 +22851,122 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, { this._super(); set(this, 'context', this); set(this, 'controller', this); - set(this, 'templateData', {keywords: {}}); }, + // during render, isolate keywords + cloneKeywords: function() { + return { + view: this, + controller: this + }; + }, + + _yield: function(context, options) { + var view = options.data.view, + parentView = this._parentView, + template = get(this, 'template'); + + if (template) { + + + view.appendChild(Ember.View, { + isVirtual: true, + tagName: '', + _contextView: parentView, + template: template, + context: get(parentView, 'context'), + controller: get(parentView, 'controller'), + templateData: { keywords: parentView.cloneKeywords() } + }); + } + }, + + /** + If the component is currently inserted into the DOM of a parent view, this + property will point to the controller of the parent view. + + @property targetObject + @type Ember.Controller + @default null + */ 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. + Sends an action to component's controller. A component inherits its + controller from the context in which it is used. - By default, calling `sendAction()` will send an action with the name - of the component's `action` property. + By default, calling `sendAction()` will send an action with the name + of the component's `action` property. - For example, if the component had a property `action` with the value - `"addItem"`, calling `sendAction()` would send the `addItem` action - to the component's controller. + For example, if the component had a property `action` with the value + `"addItem"`, calling `sendAction()` would send the `addItem` action + to the component's controller. - If you provide an argument to `sendAction()`, that key will be used to look - up the action name. + If you provide the `action` argument to `sendAction()`, that key will + be used to look up the action name. - For example, if the component had a property `playing` with the value - `didStartPlaying`, calling `sendAction('playing')` would send the - `didStartPlaying` action to the component's controller. + For example, if the component had a property `playing` with the value + `didStartPlaying`, calling `sendAction('playing')` would send the + `didStartPlaying` action to the component's controller. - Whether or not you are using the default action or a named action, if - the action name is not defined on the component, calling `sendAction()` - does not have any effect. + Whether or not you are using the default action or a named action, if + the action name is not defined on the component, calling `sendAction()` + does not have any effect. - For example, if you call `sendAction()` on a component that does not have - an `action` property defined, no action will be sent to the controller, - nor will an exception be raised. + For example, if you call `sendAction()` on a component that does not have + an `action` property defined, no action will be sent to the controller, + nor will an exception be raised. - @param [action] {String} the action to trigger + You can send a context object with the action by supplying the `context` + argument. The context will be supplied as the first argument in the + target's action method. Example: + + ```javascript + App.MyTreeComponent = Ember.Component.extend({ + click: function() { + this.sendAction('didClickTreeNode', this.get('node')); + } + }); + + App.CategoriesController = Ember.Controller.extend({ + actions: { + didClickCategory: function(category) { + //Do something with the node/category that was clicked + } + } + }); + ``` + + ```handlebars + {{! categories.hbs}} + {{my-tree didClickTreeNode='didClickCategory'}} + ``` + + @method sendAction + @param [action] {String} the action to trigger + @param [context] {*} a context to send with the action */ - sendAction: function(action) { + sendAction: function(action, context) { var actionName; // Send the default action if (action === undefined) { actionName = get(this, 'action'); - 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 + action: actionName, + actionContext: context }); } }); @@ -19472,9 +23069,10 @@ define("metamorph", 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 = false, + 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 @@ -19581,6 +23179,14 @@ define("metamorph", range.insertNode(fragment); }; + /** + * @public + * + * Remove this object (including starting and ending + * placeholders). + * + * @method remove + */ removeFunc = function() { // get a range for the current metamorph object including // the starting and ending placeholders. @@ -19621,7 +23227,7 @@ define("metamorph", }; } else { - /** + /* * This code is mostly taken from jQuery, with one exception. In jQuery's case, we * have some HTML and we need to figure out how to convert it into some nodes. * @@ -19675,12 +23281,12 @@ define("metamorph", } }; - /** + /* * Given a parent node and some HTML, generate a set of nodes. Return the first * node, which will allow us to traverse the rest using nextSibling. * * We need to do this because innerHTML in IE does not really parse the nodes. - **/ + */ var firstNodeFor = function(parentNode, html) { var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; var depth = arr[0], start = arr[1], end = arr[2]; @@ -19713,7 +23319,7 @@ define("metamorph", return element; }; - /** + /* * In some cases, Internet Explorer can create an anonymous node in * the hierarchy with no tagName. You can create this scenario via: * @@ -19723,7 +23329,7 @@ define("metamorph", * * If our script markers are inside such a node, we need to find that * node and use *it* as the marker. - **/ + */ var realNode = function(start) { while (start.parentNode.tagName === "") { start = start.parentNode; @@ -19732,7 +23338,7 @@ define("metamorph", return start; }; - /** + /* * When automatically adding a tbody, Internet Explorer inserts the * tbody immediately before the first <tr>. Other browsers create it * before the first node, no matter what. @@ -19759,7 +23365,8 @@ define("metamorph", * * This code reparents the first script tag by making it the tbody's * first child. - **/ + * + */ var fixParentage = function(start, end) { if (start.parentNode !== end.parentNode) { end.parentNode.insertBefore(start, end.parentNode.firstChild); @@ -19939,8 +23546,7 @@ if (!Handlebars && typeof require === 'function') { Handlebars = require('handlebars'); } -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 @@ -19997,7 +23603,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 @@ -20026,20 +23632,11 @@ function makeBindings(options) { @param {String} dependentKeys* */ Ember.Handlebars.helper = function(name, value) { - if (Ember.Component.detect(value)) { - Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", name.match(/-/)); - var proto = value.proto(); - if (!proto.layoutName && !proto.templateName) { - value.reopen({ - layoutName: 'components/' + name - }); - } - } if (Ember.View.detect(value)) { Ember.Handlebars.registerHelper(name, function(options) { - 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); }); @@ -20088,7 +23685,6 @@ if (Handlebars.JavaScriptCompiler) { Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; - Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { return "''"; }; @@ -20190,7 +23786,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; }; } @@ -20274,7 +23873,6 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { } return value; }; -Ember.Handlebars.getPath = Ember.deprecateFunc('`Ember.Handlebars.getPath` has been changed to `Ember.Handlebars.get` for consistency.', Ember.Handlebars.get); Ember.Handlebars.resolveParams = function(context, params, options) { var resolvedParams = [], types = options.types, param, type; @@ -20453,61 +24051,100 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { numProperties = properties.length, options = arguments[arguments.length - 1], normalizedProperties = [], + types = options.types, data = options.data, hash = options.hash, view = data.view, currentContext = (options.contexts && options.contexts[0]) || this, - normalized, - pathRoot, path, prefixPathForDependentKeys = '', - loc, hashOption; + prefixPathForDependentKeys = '', + loc, len, hashOption, + boundOption, property, + normalizedValue = Ember._SimpleHandlebarsView.prototype.normalizedValue; - Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); // Detect bound options (e.g. countBinding="otherCount") - hash.boundOptions = {}; + var boundOptions = hash.boundOptions = {}; for (hashOption in hash) { - if (!hash.hasOwnProperty(hashOption)) { continue; } - - if (Ember.IS_BINDING.test(hashOption) && typeof hash[hashOption] === 'string') { + if (Ember.IS_BINDING.test(hashOption)) { // Lop off 'Binding' suffix. - hash.boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; + boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; } } // Expose property names on data.properties object. + var watchedProperties = []; data.properties = []; for (loc = 0; loc < numProperties; ++loc) { data.properties.push(properties[loc]); - normalizedProperties.push(normalizePath(currentContext, properties[loc], data)); + if (types[loc] === 'ID') { + var normalizedProp = normalizePath(currentContext, properties[loc], data); + normalizedProperties.push(normalizedProp); + watchedProperties.push(normalizedProp); + } else { + normalizedProperties.push(null); + } } + // Handle case when helper invocation is preceded by `unbound`, e.g. + // {{unbound myHelper foo}} if (data.isUnbound) { return evaluateUnboundHelper(this, fn, normalizedProperties, options); } - if (dependentKeys.length === 0) { - return evaluateMultiPropertyBoundHelper(currentContext, fn, normalizedProperties, options); - } - - Ember.assert("Dependent keys can only be used with single-property helpers.", properties.length === 1); - - normalized = normalizedProperties[0]; - - pathRoot = normalized.root; - path = normalized.path; - - var bindView = new Ember._SimpleHandlebarsView( - path, pathRoot, !options.hash.unescaped, options.data - ); + var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); + // Override SimpleHandlebarsView's method for generating the view's content. bindView.normalizedValue = function() { - var value = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - return fn.call(view, value, options); + var args = [], boundOption; + + // Copy over bound hash options. + for (boundOption in boundOptions) { + if (!boundOptions.hasOwnProperty(boundOption)) { continue; } + property = normalizePath(currentContext, boundOptions[boundOption], data); + bindView.path = property.path; + bindView.pathRoot = property.root; + hash[boundOption] = normalizedValue.call(bindView); + } + + for (loc = 0; loc < numProperties; ++loc) { + property = normalizedProperties[loc]; + if (property) { + bindView.path = property.path; + bindView.pathRoot = property.root; + args.push(normalizedValue.call(bindView)); + } else { + args.push(properties[loc]); + } + } + args.push(options); + + // Run the supplied helper function. + return fn.apply(currentContext, args); }; view.appendChild(bindView); - view.registerObserver(pathRoot, path, bindView, bindView.rerender); + // Assemble list of watched properties that'll re-render this helper. + for (boundOption in boundOptions) { + if (boundOptions.hasOwnProperty(boundOption)) { + watchedProperties.push(normalizePath(currentContext, boundOptions[boundOption], data)); + } + } + + // Observe each property. + for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { + property = watchedProperties[loc]; + view.registerObserver(property.root, property.path, bindView, bindView.rerender); + } + + if (types[0] !== 'ID' || normalizedProperties.length === 0) { + return; + } + + // Add dependent key observers to the first param + var normalized = normalizedProperties[0], + pathRoot = normalized.root, + path = normalized.path; if(!Ember.isEmpty(path)) { prefixPathForDependentKeys = path + '.'; @@ -20521,68 +24158,6 @@ Ember.Handlebars.registerBoundHelper = function(name, fn) { Ember.Handlebars.registerHelper(name, helper); }; -/** - @private - - Renders the unbound form of an otherwise bound helper function. - - @method evaluateMultiPropertyBoundHelper - @param {Function} fn - @param {Object} context - @param {Array} normalizedProperties - @param {String} options -*/ -function evaluateMultiPropertyBoundHelper(context, fn, normalizedProperties, options) { - var numProperties = normalizedProperties.length, - data = options.data, - view = data.view, - hash = options.hash, - boundOptions = hash.boundOptions, - watchedProperties, - boundOption, bindView, loc, property, len; - - bindView = new Ember._SimpleHandlebarsView(null, null, !hash.unescaped, data); - bindView.normalizedValue = function() { - var args = [], boundOption; - - // Copy over bound options. - for (boundOption in boundOptions) { - if (!boundOptions.hasOwnProperty(boundOption)) { continue; } - property = normalizePath(context, boundOptions[boundOption], data); - bindView.path = property.path; - bindView.pathRoot = property.root; - hash[boundOption] = Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView); - } - - for (loc = 0; loc < numProperties; ++loc) { - property = normalizedProperties[loc]; - bindView.path = property.path; - bindView.pathRoot = property.root; - args.push(Ember._SimpleHandlebarsView.prototype.normalizedValue.call(bindView)); - } - args.push(options); - return fn.apply(context, args); - }; - - view.appendChild(bindView); - - // Assemble list of watched properties that'll re-render this helper. - watchedProperties = []; - for (boundOption in boundOptions) { - if (boundOptions.hasOwnProperty(boundOption)) { - watchedProperties.push(normalizePath(context, boundOptions[boundOption], data)); - } - } - watchedProperties = watchedProperties.concat(normalizedProperties); - - // Observe each property. - for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { - property = watchedProperties[loc]; - view.registerObserver(property.root, property.path, bindView, bindView.rerender); - } - -} - /** @private @@ -20632,19 +24207,19 @@ Ember.Handlebars.template = function(spec) { (function() { /** - * Mark a string as safe for unescaped output with Handlebars. If you - * return HTML from a Handlebars helper, use this function to - * ensure Handlebars does not escape the HTML. - * - * ```javascript - * Ember.String.htmlSafe('<div>someString</div>') - * ``` - * - * @method htmlSafe - * @for Ember.String - * @static - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as safe for unescaped output with Handlebars. If you + return HTML from a Handlebars helper, use this function to + ensure Handlebars does not escape the HTML. + + ```javascript + Ember.String.htmlSafe('<div>someString</div>') + ``` + + @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); }; @@ -20654,18 +24229,18 @@ var htmlSafe = Ember.String.htmlSafe; if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { /** - * Mark a string as being safe for unescaped output with Handlebars. - * - * ```javascript - * '<div>someString</div>'.htmlSafe() - * ``` - * - * See `Ember.String.htmlSafe`. - * - * @method htmlSafe - * @for String - * @return {Handlebars.SafeString} a string that will not be html escaped by Handlebars - */ + Mark a string as being safe for unescaped output with Handlebars. + + ```javascript + '<div>someString</div>'.htmlSafe() + ``` + + See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_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); }; @@ -20741,19 +24316,21 @@ var DOMManager = { view.clearRenderedChildren(); var buffer = view.renderToBuffer(); - view.propertyWillChange('element'); + view.invokeRecursively(function(view) { + view.propertyWillChange('element'); + }); view.triggerRecursively('willInsertElement'); morph.replaceWith(buffer.string()); view.transitionTo('inDOM'); - view.propertyDidChange('element'); + view.invokeRecursively(function(view) { + view.propertyDidChange('element'); + }); view.triggerRecursively('didInsertElement'); notifyMutationListeners(); }); - - }, empty: function(view) { @@ -20779,7 +24356,7 @@ Ember._Metamorph = Ember.Mixin.create({ init: function() { this._super(); this.morph = Metamorph(); - Ember.deprecate('Supplying a tagName to Metamorph views is unreliable and is deprecated. You may be setting the tagName on a Handlebars helper that creates a Metamorph.', !this.tagName); + }, beforeRender: function(buffer) { @@ -20843,6 +24420,8 @@ function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) { this.morph = Metamorph(); this.state = 'preRender'; this.updateId = null; + this._parentView = null; + this.buffer = null; } Ember._SimpleHandlebarsView = SimpleHandlebarsView; @@ -20856,7 +24435,11 @@ SimpleHandlebarsView.prototype = { Ember.run.cancel(this.updateId); this.updateId = null; } + if (this._parentView) { + this._parentView.removeChild(this); + } this.morph = null; + this.state = 'destroyed'; }, propertyWillChange: Ember.K, @@ -20911,7 +24494,7 @@ SimpleHandlebarsView.prototype = { rerender: function() { switch(this.state) { case 'preRender': - case 'destroying': + case 'destroyed': break; case 'inBuffer': throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); @@ -21302,7 +24885,7 @@ function simpleBind(property, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { - Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); + if (helpers[property]) { return helpers[property].call(this, fn); } @@ -21336,7 +24919,7 @@ EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { @return {String} HTML string */ EmberHandlebars.registerHelper('bind', function(property, options) { - Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); + var context = (options.contexts && options.contexts[0]) || this; @@ -21392,12 +24975,10 @@ EmberHandlebars.registerHelper('with', function(context, options) { if (arguments.length === 4) { var keywordName, path, rootPath, normalized; - Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); options = arguments[3]; keywordName = arguments[2]; path = arguments[0]; - Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); if (Ember.isGlobalPath(path)) { Ember.bind(options.data.keywords, keywordName, path); @@ -21418,15 +24999,15 @@ EmberHandlebars.registerHelper('with', function(context, options) { return bind.call(this, path, options, true, exists); } else { - Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); - Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); + + return helpers.bind.call(options.contexts[0], context, options); } }); /** - See `boundIf` + See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) @method if @for Ember.Handlebars.helpers @@ -21435,8 +25016,8 @@ EmberHandlebars.registerHelper('with', function(context, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('if', function(context, options) { - Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); - Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); + + return helpers.boundIf.call(options.contexts[0], context, options); }); @@ -21449,8 +25030,8 @@ EmberHandlebars.registerHelper('if', function(context, options) { @return {String} HTML string */ EmberHandlebars.registerHelper('unless', function(context, options) { - Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); - Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); + + var fn = options.fn, inverse = options.inverse; @@ -21461,11 +25042,11 @@ EmberHandlebars.registerHelper('unless', function(context, options) { }); /** - `bindAttr` allows you to create a binding between DOM element attributes and + `bind-attr` allows you to create a binding between DOM element attributes and Ember objects. For example: ```handlebars - <img {{bindAttr src="imageUrl" alt="imageTitle"}}> + <img {{bind-attr src="imageUrl" alt="imageTitle"}}> ``` The above handlebars template will fill the `<img>`'s `src` attribute will @@ -21487,17 +25068,17 @@ EmberHandlebars.registerHelper('unless', function(context, options) { <img src="http://lolcats.info/haz-a-funny" alt="A humorous image of a cat"> ``` - `bindAttr` cannot redeclare existing DOM element attributes. The use of `src` - in the following `bindAttr` example will be ignored and the hard coded value + `bind-attr` cannot redeclare existing DOM element attributes. The use of `src` + in the following `bind-attr` example will be ignored and the hard coded value of `src="/failwhale.gif"` will take precedence: ```handlebars - <img src="/failwhale.gif" {{bindAttr src="imageUrl" alt="imageTitle"}}> + <img src="/failwhale.gif" {{bind-attr src="imageUrl" alt="imageTitle"}}> ``` - ### `bindAttr` and the `class` attribute + ### `bind-attr` and the `class` attribute - `bindAttr` supports a special syntax for handling a number of cases unique + `bind-attr` supports a special syntax for handling a number of cases unique to the `class` DOM element attribute. The `class` attribute combines multiple discreet values into a single attribute as a space-delimited list of strings. Each string can be: @@ -21506,7 +25087,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { * a boolean return value of an object's property * a hard-coded value - A string return value works identically to other uses of `bindAttr`. The + A string return value works identically to other uses of `bind-attr`. The return value of the property will become the value of the attribute. For example, the following view and template: @@ -21519,7 +25100,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - <img {{bindAttr class="view.someProperty}}> + <img {{bind-attr class="view.someProperty}}> ``` Result in the following rendered output: @@ -21541,7 +25122,7 @@ EmberHandlebars.registerHelper('unless', function(context, options) { ``` ```handlebars - <img {{bindAttr class="view.someBool:class-name-if-true"}}> + <img {{bind-attr class="view.someBool:class-name-if-true"}}> ``` Result in the following rendered output: @@ -21555,14 +25136,14 @@ EmberHandlebars.registerHelper('unless', function(context, options) { value changes: ```handlebars - <img {{bindAttr class="view.someBool:class-name-if-true:class-name-if-false"}}> + <img {{bind-attr class="view.someBool:class-name-if-true:class-name-if-false"}}> ``` A hard-coded value can be used by prepending `:` to the desired class name: `:class-name-to-always-apply`. ```handlebars - <img {{bindAttr class=":class-name-to-always-apply"}}> + <img {{bind-attr class=":class-name-to-always-apply"}}> ``` Results in the following rendered output: @@ -21575,19 +25156,18 @@ EmberHandlebars.registerHelper('unless', function(context, options) { hard-coded value – can be combined in a single declaration: ```handlebars - <img {{bindAttr class=":class-name-to-always-apply view.someBool:class-name-if-true view.someProperty"}}> + <img {{bind-attr class=":class-name-to-always-apply view.someBool:class-name-if-true view.someProperty"}}> ``` - @method bindAttr + @method bind-attr @for Ember.Handlebars.helpers @param {Hash} options @return {String} HTML string */ -EmberHandlebars.registerHelper('bindAttr', function(options) { +EmberHandlebars.registerHelper('bind-attr', function(options) { var attrs = options.hash; - Ember.assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length); var view = options.data.view; var ret = []; @@ -21615,21 +25195,18 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { var path = attrs[attr], normalized; - 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); var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), type = Ember.typeOf(value); - Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); var observer, invoker; observer = function observer() { var result = handlebarsGet(ctx, path, options); - Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), result === null || result === undefined || typeof result === 'number' || typeof result === 'string' || typeof result === 'boolean'); var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); @@ -21649,7 +25226,7 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { // When the observer fires, find the element using the // unique data id and update the attribute to the new value. // Note: don't add observer when path is 'this' or path - // is whole keyword e.g. {{#each x in list}} ... {{bindAttr attr="x"}} + // is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { view.registerObserver(normalized.root, normalized.path, observer); } @@ -21669,6 +25246,18 @@ EmberHandlebars.registerHelper('bindAttr', function(options) { return new EmberHandlebars.SafeString(ret.join(' ')); }); +/** + See `bind-attr` + + @method bindAttr + @for Ember.Handlebars.helpers + @deprecated + @param {Function} context + @param {Hash} options + @return {String} HTML string +*/ +EmberHandlebars.registerHelper('bindAttr', EmberHandlebars.helpers['bind-attr']); + /** @private @@ -21840,7 +25429,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ } if (hash.attributeBindings) { - Ember.assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); + extensions.attributeBindings = null; dup = true; } @@ -21920,18 +25509,16 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ // 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; } - Ember.assert(Ember.String.fmt('You must pass a view to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView) || Ember.View.detectInstance(newView)); var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); var currentView = data.view; @@ -21939,7 +25526,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ var newViewProto = newView.proto ? newView.proto() : newView; if (fn) { - Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName')); + viewOptions.template = fn; } @@ -22119,7 +25706,7 @@ EmberHandlebars.ViewHelper = Ember.Object.create({ @return {String} HTML string */ EmberHandlebars.registerHelper('view', function(path, options) { - Ember.assert("The view helper only takes a single argument", arguments.length <= 2); + // If no path is provided, treat path param as options. if (path && path.data && path.data.isRenderData) { @@ -22148,8 +25735,8 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm /** `{{collection}}` is a `Ember.Handlebars` helper for adding instances of - `Ember.CollectionView` to a template. See `Ember.CollectionView` for - additional information on how a `CollectionView` functions. + `Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) + for additional information on how a `CollectionView` functions. `{{collection}}`'s primary use is as a block helper with a `contentBinding` option pointing towards an `Ember.Array`-compatible object. An `Ember.View` @@ -22269,15 +25856,15 @@ var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fm @deprecated Use `{{each}}` helper instead. */ Ember.Handlebars.registerHelper('collection', function(path, options) { - Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); + // If no path is provided, treat path param as options. if (path && path.data && path.data.isRenderData) { options = path; path = undefined; - Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); + } else { - Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); + } var fn = options.fn; @@ -22289,7 +25876,7 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { // Otherwise, just default to the standard class. var collectionClass; collectionClass = path ? handlebarsGet(this, path, options) : Ember.CollectionView; - Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); + var hash = options.hash, itemHash = {}, match; @@ -22299,17 +25886,16 @@ Ember.Handlebars.registerHelper('collection', function(path, options) { 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; delete hash.itemView; @@ -22421,7 +26007,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 @@ -22474,12 +26060,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); @@ -22499,8 +26085,8 @@ Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { }, _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) { @@ -22602,6 +26188,8 @@ GroupedEach.prototype = { }, addArrayObservers: function() { + if (!this.content) { return; } + this.content.addArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -22609,6 +26197,8 @@ GroupedEach.prototype = { }, removeArrayObservers: function() { + if (!this.content) { return; } + this.content.removeArrayObserver(this, { willChange: 'contentArrayWillChange', didChange: 'contentArrayDidChange' @@ -22626,6 +26216,8 @@ GroupedEach.prototype = { }, render: function() { + if (!this.content) { return; } + var content = this.content, contentLength = get(content, 'length'), data = this.options.data, @@ -22638,12 +26230,21 @@ GroupedEach.prototype = { }, rerenderContainingView: function() { - Ember.run.scheduleOnce('render', this.containingView, 'rerender'); + var self = this; + Ember.run.scheduleOnce('render', this, function() { + // It's possible it's been destroyed after we enqueued a re-render call. + if (!self.destroyed) { + self.containingView.rerender(); + } + }); }, destroy: function() { this.removeContentObservers(); - this.removeArrayObservers(); + if (this.content) { + this.removeArrayObservers(); + } + this.destroyed = true; } }; @@ -22767,6 +26368,49 @@ GroupedEach.prototype = { Each itemController will receive a reference to the current controller as a `parentController` property. + ### (Experimental) Grouped Each + + When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), + you can inform Handlebars to re-render an entire group of items instead of + re-rendering them one at a time (in the event that they are changed en masse + or an item is added/removed). + + ```handlebars + {{#group}} + {{#each people}} + {{firstName}} {{lastName}} + {{/each}} + {{/group}} + ``` + + This can be faster than the normal way that Handlebars re-renders items + in some cases. + + If for some reason you have a group with more than one `#each`, you can make + one of the collections be updated in normal (non-grouped) fashion by setting + the option `groupedRows=true` (counter-intuitive, I know). + + For example, + + ```handlebars + {{dealershipName}} + + {{#group}} + {{#each dealers}} + {{firstName}} {{lastName}} + {{/each}} + + {{#each car in cars groupedRows=true}} + {{car.make}} {{car.model}} {{car.color}} + {{/each}} + {{/group}} + ``` + Any change to `dealershipName` or the `dealers` collection will cause the + entire group to be re-rendered. However, changes to the `cars` collection + will be re-rendered individually (as normal). + + Note that `group` behavior is also disabled by specifying an `itemViewClass`. + @method each @for Ember.Handlebars.helpers @param [name] {String} name for item (used with `in`) @@ -22774,10 +26418,11 @@ GroupedEach.prototype = { @param [options] {Object} Handlebars key/value pairs of options @param [options.itemViewClass] {String} a path to a view class used for each item @param [options.itemController] {String} name of a controller to be created for each item + @param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper */ Ember.Handlebars.registerHelper('each', function(path, options) { if (arguments.length === 4) { - Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); + var keywordName = arguments[0]; @@ -22859,7 +26504,7 @@ Ember.Handlebars.registerHelper('each', function(path, options) { */ Ember.Handlebars.registerHelper('template', function(name, options) { - 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); }); @@ -22910,7 +26555,6 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { template = view.templateForName(underscoredName), deprecatedTemplate = !template && view.templateForName(name); - Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); template = template || deprecatedTemplate; @@ -22930,6 +26574,10 @@ Ember.Handlebars.registerHelper('partial', function(name, options) { var get = Ember.get, set = Ember.set; /** + `{{yield}}` denotes an area of a template that will be rendered inside + of another template. It has two main uses: + + ### Use with `layout` When used in a Handlebars template that is assigned to an `Ember.View` instance's `layout` property Ember will render the layout template first, inserting the view's own rendered output at the `{{yield}}` location. @@ -22972,7 +26620,34 @@ var get = Ember.get, set = Ember.set; bView.appendTo('body'); // throws - // Uncaught Error: assertion failed: You called yield in a template that was not a layout + // Uncaught Error: assertion failed: + // You called yield in a template that was not a layout + ``` + + ### Use with Ember.Component + When designing components `{{yield}}` is used to denote where, inside the component's + template, an optional block passed to the component should render: + + ```handlebars + <!-- application.hbs --> + {{#labeled-textfield value=someProperty}} + First name: + {{/my-component}} + ``` + + ```handlebars + <!-- components/my-component.hbs --> + <label> + {{yield}} {{input value=value}} + </label> + ``` + + Result: + + ```html + <label> + First name: <input type="text" /> + <label> ``` @method yield @@ -22981,26 +26656,18 @@ var get = Ember.get, set = Ember.set; @return {String} HTML string */ Ember.Handlebars.registerHelper('yield', function(options) { - var currentView = options.data.view, view = currentView, template; + var view = options.data.view; while (view && !get(view, 'layout')) { - view = get(view, 'parentView'); + if (view._contextView) { + view = view._contextView; + } else { + view = get(view, 'parentView'); + } } - Ember.assert("You called yield in a template that was not a layout", !!view); - template = get(view, 'template'); - - var keywords = view._parentView.cloneKeywords(); - - currentView.appendChild(Ember.View, { - isVirtual: true, - tagName: '', - template: template, - context: get(view._parentView, 'context'), - controller: get(view._parentView, 'controller'), - templateData: {keywords: keywords} - }); + view._yield(this, options); }); })(); @@ -23019,11 +26686,11 @@ Ember.Handlebars.registerHelper('yield', function(options) { ```html <script type="text/x-handlebars" data-template-name="home"> - {{loc welcome}} + {{loc "welcome"}} </script> ``` - Take note that `welcome` is a string and not an object + Take note that `"welcome"` is a string and not an object reference. @method loc @@ -23060,26 +26727,12 @@ Ember.Handlebars.registerHelper('loc', function(str) { var set = Ember.set, get = Ember.get; /** - The `Ember.Checkbox` view class renders a checkbox - [input](https://developer.mozilla.org/en/HTML/Element/Input) element. It - allows for binding an Ember property (`checked`) to the status of the - checkbox. + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `checkbox`. - Example: + See Handlebars.helpers.input for usage details. - ```handlebars - {{view Ember.Checkbox checkedBinding="receiveEmail"}} - ``` - - You can add a `label` tag yourself in the template where the `Ember.Checkbox` - is being used. - - ```handlebars - <label> - {{view Ember.Checkbox classNames="applicaton-specific-checkbox"}} - Some Title - </label> - ``` + ## Direct manipulation of `checked` The `checked` attribute of an `Ember.Checkbox` object should always be set through the Ember object or by interacting with its rendered element @@ -23090,8 +26743,8 @@ var set = Ember.set, get = Ember.get; ## Layout and LayoutName properties Because HTML `input` elements are self closing `layout` and `layoutName` - properties will not be applied. See `Ember.View`'s layout section for more - information. + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. @class Checkbox @namespace Ember @@ -23146,14 +26799,11 @@ var get = Ember.get, set = Ember.set; Ember.TextSupport = Ember.Mixin.create({ value: "", - attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex'], + attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly'], placeholder: null, disabled: false, maxlength: null, - insertNewline: Ember.K, - cancel: Ember.K, - init: function() { this._super(); this.on("focusOut", this, this._elementValueDidChange); @@ -23164,120 +26814,6 @@ Ember.TextSupport = Ember.Mixin.create({ this.on("keyUp", this, this.interpretKeyEvents); }, - interpretKeyEvents: function(event) { - var map = Ember.TextSupport.KEY_EVENTS; - var method = map[event.keyCode]; - - this._elementValueDidChange(); - if (method) { return this[method](event); } - }, - - _elementValueDidChange: function() { - set(this, 'value', this.$().val()); - } - -}); - -Ember.TextSupport.KEY_EVENTS = { - 13: 'insertNewline', - 27: 'cancel' -}; - -})(); - - - -(function() { -/** -@module ember -@submodule ember-handlebars -*/ - -var get = Ember.get, set = Ember.set; - -/** - The `Ember.TextField` view class renders a text - [input](https://developer.mozilla.org/en/HTML/Element/Input) element. It - allows for binding Ember properties to the text field contents (`value`), - live-updating as the user inputs text. - - Example: - - ```handlebars - {{view Ember.TextField valueBinding="firstName"}} - ``` - - ## Layout and LayoutName properties - - Because HTML `input` elements are self closing `layout` and `layoutName` - properties will not be applied. See `Ember.View`'s layout section for more - information. - - ## HTML Attributes - - By default `Ember.TextField` provides support for `type`, `value`, `size`, - `pattern`, `placeholder`, `disabled`, `maxlength` and `tabindex` attributes - on a text field. If you need to support more attributes have a look at the - `attributeBindings` property in `Ember.View`'s HTML Attributes section. - - To globally add support for additional attributes you can reopen - `Ember.TextField` or `Ember.TextSupport`. - - ```javascript - Ember.TextSupport.reopen({ - attributeBindings: ["required"] - }) - ``` - - @class TextField - @namespace Ember - @extends Ember.View - @uses Ember.TextSupport -*/ -Ember.TextField = Ember.View.extend(Ember.TextSupport, - /** @scope Ember.TextField.prototype */ { - - classNames: ['ember-text-field'], - tagName: "input", - attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], - - /** - The `value` attribute of the input element. As the user inputs text, this - property is updated live. - - @property value - @type String - @default "" - */ - value: "", - - /** - The `type` attribute of the input element. - - @property type - @type String - @default "text" - */ - type: "text", - - /** - The `size` of the text field in characters. - - @property size - @type String - @default null - */ - size: null, - - /** - The `pattern` the pattern attribute of input element. - - @property pattern - @type String - @default null - */ - pattern: null, - /** The action to be sent when the user presses the return key. @@ -23322,31 +26858,109 @@ Ember.TextField = Ember.View.extend(Ember.TextSupport, */ bubbles: false, - insertNewline: function(event) { - sendAction('enter', this, event); + interpretKeyEvents: function(event) { + var map = Ember.TextSupport.KEY_EVENTS; + var method = map[event.keyCode]; + + this._elementValueDidChange(); + if (method) { return this[method](event); } }, + _elementValueDidChange: function() { + set(this, 'value', this.$().val()); + }, + + /** + The action to be sent when the user inserts a new line. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method insertNewLine + @param {Event} event + */ + insertNewline: function(event) { + sendAction('enter', this, event); + sendAction('insert-newline', this, event); + }, + + /** + Called when the user hits escape. + + Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. + Uses sendAction to send the `enter` action to the controller. + + @method cancel + @param {Event} event + */ + cancel: function(event) { + sendAction('escape-press', this, event); + }, + + /** + Called when the text area is focused. + + @method focusIn + @param {Event} event + */ + focusIn: function(event) { + sendAction('focus-in', this, event); + }, + + /** + Called when the text area is blurred. + + @method focusOut + @param {Event} event + */ + focusOut: function(event) { + sendAction('focus-out', this, event); + }, + + /** + The action to be sent when the user presses a key. Enabled by setting + the `onEvent` property to `keyPress`. + + Uses sendAction to send the `keyPress` action to the controller. + + @method keyPress + @param {Event} event + */ keyPress: function(event) { - sendAction('keyPress', this, event); + sendAction('key-press', this, event); } + }); +Ember.TextSupport.KEY_EVENTS = { + 13: 'insertNewline', + 27: 'cancel' +}; + +// In principle, this shouldn't be necessary, but the legacy +// sectionAction semantics for TextField are different from +// the component semantics so this method normalizes them. function sendAction(eventName, view, event) { - var action = get(view, 'action'), - on = get(view, 'onEvent'); + var action = get(view, eventName), + on = get(view, 'onEvent'), + value = get(view, 'value'); - if (action && on === eventName) { - var controller = get(view, 'controller'), - value = get(view, 'value'), - bubbles = get(view, 'bubbles'); + // back-compat support for keyPress as an event name even though + // it's also a method name that consumes the event (and therefore + // incompatible with sendAction semantics). + if (on === eventName || (on === 'keyPress' && eventName === 'key-press')) { + view.sendAction('action', value); + } - controller.send(action, value, view); + view.sendAction(eventName, value); - if (!bubbles) { + if (action || on === eventName) { + if(!get(view, 'bubbles')) { event.stopPropagation(); } } } + })(); @@ -23360,6 +26974,81 @@ function sendAction(eventName, view, event) { var get = Ember.get, set = Ember.set; /** + + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `text`. + + See [handlebars.helpers.input](api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. + + @class TextField + @namespace Ember + @extends Ember.Component + @uses Ember.TextSupport +*/ +Ember.TextField = Ember.Component.extend(Ember.TextSupport, + /** @scope Ember.TextField.prototype */ { + + classNames: ['ember-text-field'], + tagName: "input", + attributeBindings: ['type', 'value', 'size', 'pattern', 'name'], + + /** + The `value` attribute of the input element. As the user inputs text, this + property is updated live. + + @property value + @type String + @default "" + */ + value: "", + + /** + The `type` attribute of the input element. + + @property type + @type String + @default "text" + */ + type: "text", + + /** + The `size` of the text field in characters. + + @property size + @type String + @default null + */ + size: null, + + /** + The `pattern` the pattern attribute of input element. + + @property pattern + @type String + @default null + */ + pattern: null +}); + +})(); + + + +(function() { +/* +@module ember +@submodule ember-handlebars +*/ + +var get = Ember.get, set = Ember.set; + +/* @class Button @namespace Ember @extends Ember.View @@ -23376,7 +27065,7 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { attributeBindings: ['type', 'disabled', 'href', 'tabindex'], - /** + /* @private Overrides `TargetActionSupport`'s `targetObject` computed @@ -23470,7 +27159,7 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { }, init: function() { - Ember.deprecate("Ember.Button is deprecated and will be removed from future releases. Consider using the `{{action}}` helper."); + this._super(); } }); @@ -23488,39 +27177,23 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { var get = Ember.get, set = Ember.set; /** - The `Ember.TextArea` view class renders a - [textarea](https://developer.mozilla.org/en/HTML/Element/textarea) element. - It allows for binding Ember properties to the text area contents (`value`), - live-updating as the user inputs text. + The internal class used to create textarea element when the `{{textarea}}` + helper is used. + + See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. ## Layout and LayoutName properties Because HTML `textarea` elements do not contain inner HTML the `layout` and - `layoutName` properties will not be applied. See `Ember.View`'s layout - section for more information. - - ## HTML Attributes - - By default `Ember.TextArea` provides support for `rows`, `cols`, - `placeholder`, `disabled`, `maxlength` and `tabindex` attributes on a - textarea. If you need to support more attributes have a look at the - `attributeBindings` property in `Ember.View`'s HTML Attributes section. - - To globally add support for additional attributes you can reopen - `Ember.TextArea` or `Ember.TextSupport`. - - ```javascript - Ember.TextSupport.reopen({ - attributeBindings: ["required"] - }) - ``` + `layoutName` properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s + layout section for more information. @class TextArea @namespace Ember - @extends Ember.View + @extends Ember.Component @uses Ember.TextSupport */ -Ember.TextArea = Ember.View.extend(Ember.TextSupport, { +Ember.TextArea = Ember.Component.extend(Ember.TextSupport, { classNames: ['ember-text-area'], tagName: "textarea", @@ -23636,7 +27309,7 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `content` property. The underlying data object of the selected `<option>` is stored in the `Element.Select`'s `value` property. - ### `content` as an array of Strings + ## The Content Property (array of strings) The simplest version of an `Ember.Select` takes an array of strings as its `content` property. The string will be used as both the `value` property and @@ -23645,11 +27318,13 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ Example: ```javascript - App.names = ["Yehuda", "Tom"]; + App.ApplicationController = Ember.Controller.extend({ + names: ["Yehuda", "Tom"] + }); ``` ```handlebars - {{view Ember.Select contentBinding="App.names"}} + {{view Ember.Select contentBinding="names"}} ``` Would result in the following HTML: @@ -23665,16 +27340,16 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `value` property directly or as a binding: ```javascript - App.names = Ember.Object.create({ - selected: 'Tom', - content: ["Yehuda", "Tom"] + App.ApplicationController = Ember.Controller.extend({ + selectedName: 'Tom', + names: ["Yehuda", "Tom"] }); ``` ```handlebars {{view Ember.Select - contentBinding="App.names.content" - valueBinding="App.names.selected" + contentBinding="names" + valueBinding="selectedName" }} ``` @@ -23688,9 +27363,9 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` A user interacting with the rendered `<select>` to choose "Yehuda" would - update the value of `App.names.selected` to "Yehuda". + update the value of `selectedName` to "Yehuda". - ### `content` as an Array of Objects + ## The Content Property (array of Objects) An `Ember.Select` can also take an array of JavaScript or Ember objects as its `content` property. @@ -23705,15 +27380,17 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ element's text. Both paths must reference each object itself as `content`: ```javascript - App.programmers = [ - Ember.Object.create({firstName: "Yehuda", id: 1}), - Ember.Object.create({firstName: "Tom", id: 2}) - ]; + App.ApplicationController = Ember.Controller.extend({ + programmers: [ + {firstName: "Yehuda", id: 1}, + {firstName: "Tom", id: 2} + ] + }); ``` ```handlebars {{view Ember.Select - contentBinding="App.programmers" + contentBinding="programmers" optionValuePath="content.id" optionLabelPath="content.firstName"}} ``` @@ -23732,22 +27409,23 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ `valueBinding` option: ```javascript - App.programmers = [ - Ember.Object.create({firstName: "Yehuda", id: 1}), - Ember.Object.create({firstName: "Tom", id: 2}) - ]; - - App.currentProgrammer = Ember.Object.create({ - id: 2 + App.ApplicationController = Ember.Controller.extend({ + programmers: [ + {firstName: "Yehuda", id: 1}, + {firstName: "Tom", id: 2} + ], + currentProgrammer: { + id: 2 + } }); ``` ```handlebars {{view Ember.Select - contentBinding="App.programmers" + contentBinding="programmers" optionValuePath="content.id" optionLabelPath="content.firstName" - valueBinding="App.currentProgrammer.id"}} + valueBinding="currentProgrammer.id"}} ``` Would result in the following HTML with a selected option: @@ -23760,7 +27438,7 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` Interacting with the rendered element by selecting the first option - ('Yehuda') will update the `id` value of `App.currentProgrammer` + ('Yehuda') will update the `id` of `currentProgrammer` to match the `value` property of the newly selected `<option>`. Alternatively, you can control selection through the underlying objects @@ -23770,21 +27448,21 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ element: ```javascript - App.controller = Ember.Object.create({ + App.ApplicationController = Ember.Controller.extend({ selectedPerson: null, - content: [ - Ember.Object.create({firstName: "Yehuda", id: 1}), - Ember.Object.create({firstName: "Tom", id: 2}) + programmers: [ + {firstName: "Yehuda", id: 1}, + {firstName: "Tom", id: 2} ] }); ``` ```handlebars {{view Ember.Select - contentBinding="App.controller.content" + contentBinding="programmers" optionValuePath="content.id" optionLabelPath="content.firstName" - selectionBinding="App.controller.selectedPerson"}} + selectionBinding="selectedPerson"}} ``` Would result in the following HTML with a selected option: @@ -23797,19 +27475,19 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` Interacting with the rendered element by selecting the first option - ('Yehuda') will update the `selectedPerson` value of `App.controller` - to match the content object of the newly selected `<option>`. In this - case it is the first object in the `App.controller.content` + ('Yehuda') will update the `selectedPerson` to match the object of + the newly selected `<option>`. In this case it is the first object + in the `programmers` - ### Supplying a Prompt + ## Supplying a Prompt A `null` value for the `Ember.Select`'s `value` or `selection` property results in there being no `<option>` with a `selected` attribute: ```javascript - App.controller = Ember.Object.create({ - selected: null, - content: [ + App.ApplicationController = Ember.Controller.extend({ + selectedProgrammer: null, + programmers: [ "Yehuda", "Tom" ] @@ -23818,8 +27496,8 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ``` handlebars {{view Ember.Select - contentBinding="App.controller.content" - valueBinding="App.controller.selected" + contentBinding="programmers" + valueBinding="selectedProgrammer" }} ``` @@ -23832,16 +27510,16 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ </select> ``` - Although `App.controller.selected` is `null` and no `<option>` + Although `selectedProgrammer` is `null` and no `<option>` has a `selected` attribute the rendered HTML will display the first item as though it were selected. You can supply a string value for the `Ember.Select` to display when there is no selection with the `prompt` option: ```javascript - App.controller = Ember.Object.create({ - selected: null, - content: [ + App.ApplicationController = Ember.Controller.extend({ + selectedProgrammer: null, + programmers: [ "Yehuda", "Tom" ] @@ -23850,8 +27528,8 @@ Ember.SelectOptgroup = Ember.CollectionView.extend({ ```handlebars {{view Ember.Select - contentBinding="App.controller.content" - valueBinding="App.controller.selected" + contentBinding="programmers" + valueBinding="selectedProgrammer" prompt="Please select a name" }} ``` @@ -23881,7 +27559,7 @@ 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("<option value=\"\">"); hashTypes = {}; @@ -23892,7 +27570,7 @@ function program1(depth0,data) { } function program3(depth0,data) { - + var stack1, hashTypes, hashContexts; hashTypes = {}; hashContexts = {}; @@ -23901,7 +27579,7 @@ function program3(depth0,data) { else { data.buffer.push(''); } } function program4(depth0,data) { - + var hashContexts, hashTypes; hashContexts = {'contentBinding': depth0,'labelBinding': depth0}; hashTypes = {'contentBinding': "ID",'labelBinding': "ID"}; @@ -23912,7 +27590,7 @@ function program4(depth0,data) { } function program6(depth0,data) { - + var stack1, hashTypes, hashContexts; hashTypes = {}; hashContexts = {}; @@ -23921,7 +27599,7 @@ function program6(depth0,data) { else { data.buffer.push(''); } } function program7(depth0,data) { - + var hashContexts, hashTypes; hashContexts = {'contentBinding': depth0}; hashTypes = {'contentBinding': "STRING"}; @@ -23939,7 +27617,7 @@ function program7(depth0,data) { stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],hashContexts:hashContexts,hashTypes:hashTypes,data:data}); if(stack1 || stack1 === 0) { data.buffer.push(stack1); } return buffer; - + }), attributeBindings: ['multiple', 'disabled', 'tabindex', 'name'], @@ -23953,6 +27631,14 @@ function program7(depth0,data) { */ multiple: false, + /** + The `disabled` attribute of the select element. Indicates whether + the element is disabled from interactions. + + @property multiple + @type Boolean + @default false + */ disabled: false, /** @@ -24019,7 +27705,7 @@ function program7(depth0,data) { prompt: null, /** - The path of the option labels. See `content`. + The path of the option labels. See [content](/api/classes/Ember.Select.html#property_content). @property optionLabelPath @type String @@ -24028,7 +27714,7 @@ function program7(depth0,data) { optionLabelPath: 'content', /** - The path of the option values. See `content`. + The path of the option values. See [content](/api/classes/Ember.Select.html#property_content). @property optionValuePath @type String @@ -24209,6 +27895,11 @@ function program7(depth0,data) { (function() { +/** +@module ember +@submodule ember-handlebars-compiler +*/ + function normalizeHash(hash, hashTypes) { for (var prop in hash) { if (hashTypes[prop] === 'ID') { @@ -24219,16 +27910,145 @@ function normalizeHash(hash, hashTypes) { } /** - * `{{input}}` inserts a new instance of either Ember.TextField or - * Ember.Checkbox, depending on the `type` option passed in. If no `type` - * is supplied it defaults to Ember.TextField. - * - * @method input - * @for Ember.Handlebars.helpers - * @param {Hash} options - */ + + The `{{input}}` helper inserts an HTML `<input>` tag into the template, + with a `type` value of either `text` or `checkbox`. If no `type` is provided, + `text` will be the default value applied. The attributes of `{{input}}` + match those of the native HTML tag as closely as possible for these two types. + + ## Use as text field + An `{{input}}` with no `type` or a `type` of `text` will render an HTML text input. + The following HTML attributes can be set via the helper: + +* `value` +* `size` +* `name` +* `pattern` +* `placeholder` +* `disabled` +* `maxlength` +* `tabindex` + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input value="http://www.facebook.com"}} + ``` + + + ```html + <input type="text" value="http://www.facebook.com"/> + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + firstName: "Stanley", + entryNotAllowed: true + }); + ``` + + + ```handlebars + {{input type="text" value=firstName disabled=entryNotAllowed size="50"}} + ``` + + + ```html + <input type="text" value="Stanley" disabled="disabled" size="50"/> + ``` + + ## Extension + + Internally, `{{input type="text"}}` creates an instance of `Ember.TextField`, passing + arguments from the helper to `Ember.TextField`'s `create` method. You can extend the + capablilties of text inputs in your applications by reopening this class. For example, + if you are deploying to browsers where the `required` attribute is used, you + can add this to the `TextField`'s `attributeBindings` property: + + ```javascript + Ember.TextField.reopen({ + attributeBindings: ['required'] + }); + ``` + + Keep in mind when writing `Ember.TextField` subclasses that `Ember.TextField` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + + ## Use as checkbox + + An `{{input}}` with a `type` of `checkbox` will render an HTML checkbox input. + The following HTML attributes can be set via the helper: + +* `checked` +* `disabled` +* `tabindex` +* `indeterminate` +* `name` + + + When set to a quoted string, these values will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + ## Unbound: + + ```handlebars + {{input type="checkbox" name="isAdmin"}} + ``` + + ```html + <input type="checkbox" name="isAdmin" /> + ``` + + ## Bound: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + isAdmin: true + }); + ``` + + + ```handlebars + {{input type="checkbox" checked=isAdmin }} + ``` + + + ```html + <input type="checkbox" checked="checked" /> + ``` + + ## Extension + + Internally, `{{input type="checkbox"}}` creates an instance of `Ember.Checkbox`, passing + arguments from the helper to `Ember.Checkbox`'s `create` method. You can extend the + capablilties of checkbox inputs in your applications by reopening this class. For example, + if you wanted to add a css class to all checkboxes in your application: + + ```javascript + Ember.Checkbox.reopen({ + classNames: ['my-app-checkbox'] + }); + ``` + + + @method input + @for Ember.Handlebars.helpers + @param {Hash} options +*/ Ember.Handlebars.registerHelper('input', function(options) { - Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2); + var hash = options.hash, types = options.hashTypes, @@ -24250,15 +28070,154 @@ Ember.Handlebars.registerHelper('input', function(options) { }); /** - * `{{textarea}}` inserts a new instance of Ember.TextArea into the template - * passing its options to `Ember.TextArea`'s `create` method. - * - * @method textarea - * @for Ember.Handlebars.helpers - * @param {Hash} options - */ + `{{textarea}}` inserts a new instance of `<textarea>` tag into the template. + The attributes of `{{textarea}}` match those of the native HTML tags as + closely as possible. + + The following HTML attributes can be set: + + * `value` + * `name` + * `rows` + * `cols` + * `placeholder` + * `disabled` + * `maxlength` + * `tabindex` + + When set to a quoted string, these value will be directly applied to the HTML + element. When left unquoted, these values will be bound to a property on the + template's current rendering context (most typically a controller instance). + + Unbound: + + ```handlebars + {{textarea value="Lots of static text that ISN'T bound"}} + ``` + + Would result in the following HTML: + + ```html + <textarea class="ember-text-area"> + Lots of static text that ISN'T bound + </textarea> + ``` + + Bound: + + In the following example, the `writtenWords` property on `App.ApplicationController` + will be updated live as the user types 'Lots of text that IS bound' into + the text area of their browser's window. + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound" + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + ``` + + Would result in the following HTML: + + ```html + <textarea class="ember-text-area"> + Lots of text that IS bound + </textarea> + ``` + + If you wanted a one way binding between the text area and a div tag + somewhere else on your screen, you could use `Ember.computed.oneWay`: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + outputWrittenWords: Ember.computed.oneWay("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + + <div> + {{outputWrittenWords}} + </div> + ``` + + Would result in the following HTML: + + ```html + <textarea class="ember-text-area"> + Lots of text that IS bound + </textarea> + + <-- the following div will be updated in real time as you type --> + + <div> + Lots of text that IS bound + </div> + ``` + + Finally, this example really shows the power and ease of Ember when two + properties are bound to eachother via `Ember.computed.alias`. Type into + either text area box and they'll both stay in sync. Note that + `Ember.computed.alias` costs more in terms of performance, so only use it when + your really binding in both directions: + + ```javascript + App.ApplicationController = Ember.Controller.extend({ + writtenWords: "Lots of text that IS bound", + twoWayWrittenWords: Ember.computed.alias("writtenWords") + }); + ``` + + ```handlebars + {{textarea value=writtenWords}} + {{textarea value=twoWayWrittenWords}} + ``` + + ```html + <textarea id="ember1" class="ember-text-area"> + Lots of text that IS bound + </textarea> + + <-- both updated in real time --> + + <textarea id="ember2" class="ember-text-area"> + Lots of text that IS bound + </textarea> + ``` + + ## Extension + + Internally, `{{textarea}}` creates an instance of `Ember.TextArea`, passing + arguments from the helper to `Ember.TextArea`'s `create` method. You can + extend the capabilities of text areas in your application by reopening this + class. For example, if you are deploying to browsers where the `required` + attribute is used, you can globally add support for the `required` attribute + on all {{textarea}}'s' in your app by reopening `Ember.TextArea` or + `Ember.TextSupport` and adding it to the `attributeBindings` concatenated + property: + + ```javascript + Ember.TextArea.reopen({ + attributeBindings: ['required'] + }); + ``` + + Keep in mind when writing `Ember.TextArea` subclasses that `Ember.TextArea` + itself extends `Ember.Component`, meaning that it does NOT inherit + the `controller` of the parent view. + + See more about [Ember components](api/classes/Ember.Component.html) + + @method textarea + @for Ember.Handlebars.helpers + @param {Hash} options +*/ Ember.Handlebars.registerHelper('textarea', function(options) { - Ember.assert('You can only pass attributes to the `textarea` helper, not arguments', arguments.length < 2); + var hash = options.hash, types = options.hashTypes; @@ -24312,6 +28271,11 @@ Ember.Handlebars.bootstrap = function(ctx) { templateName = script.attr('data-template-name') || script.attr('id') || 'application', template = compile(script.html()); + // Check if template of same name already exists + if (Ember.TEMPLATES[templateName] !== undefined) { + throw new Error('Template named "' + templateName + '" already exists.'); + } + // For templates which have a name, we save them and then remove them from the DOM Ember.TEMPLATES[templateName] = template; @@ -24335,18 +28299,22 @@ function registerComponents(container) { } } + function registerComponent(container, name) { - Ember.assert("You provided a template named 'components/" + name + "', but custom components must include a '-'", name.match(/-/)); - var className = name.replace(/-/g, '_'); - var Component = container.lookupFactory('component:' + className) || container.lookupFactory('component:' + name); - var View = Component || Ember.Component.extend(); - View.reopen({ - layoutName: 'components/' + name - }); + var fullName = 'component:' + name; - Ember.Handlebars.helper(name, View); + container.injection(fullName, 'layout', 'template:components/' + name); + + var Component = container.lookupFactory(fullName); + + if (!Component) { + container.register(fullName, Ember.Component); + Component = container.lookupFactory(fullName); + } + + Ember.Handlebars.helper(name, Component); } /* @@ -24361,21 +28329,16 @@ function registerComponent(container, name) { */ Ember.onLoad('Ember.Application', function(Application) { - if (Application.initializer) { - Application.initializer({ - name: 'domTemplates', - initialize: bootstrap - }); + Application.initializer({ + name: 'domTemplates', + initialize: bootstrap + }); - Application.initializer({ - name: 'registerComponents', - after: 'domTemplates', - initialize: registerComponents - }); - } else { - // for ember-old-router - Ember.onLoad('application', bootstrap); - } + Application.initializer({ + name: 'registerComponents', + after: 'domTemplates', + initialize: registerComponents + }); }); })(); @@ -24499,7 +28462,7 @@ define("route-recognizer", results.push(new StarSegment(match[1])); names.push(match[1]); types.stars++; - } else if (segment === "") { + } else if(segment === "") { results.push(new EpsilonSegment()); } else { results.push(new StaticSegment(segment)); @@ -24648,19 +28611,31 @@ define("route-recognizer", return nextStates; } - function findHandler(state, path) { + function findHandler(state, path, queryParams) { var handlers = state.handlers, regex = state.regex; var captures = path.match(regex), currentCapture = 1; var result = []; for (var i=0, l=handlers.length; i<l; i++) { - var handler = handlers[i], names = handler.names, params = {}; + var handler = handlers[i], names = handler.names, params = {}, + watchedQueryParams = handler.queryParams || [], + activeQueryParams = {}, + j, m; - for (var j=0, m=names.length; j<m; j++) { + for (j=0, m=names.length; j<m; j++) { params[names[j]] = captures[currentCapture++]; } - - result.push({ handler: handler.handler, params: params, isDynamic: !!names.length }); + for (j=0, m=watchedQueryParams.length; j < m; j++) { + var key = watchedQueryParams[j]; + if(queryParams[key]){ + activeQueryParams[key] = queryParams[key]; + } + } + var currentResult = { handler: handler.handler, params: params, isDynamic: !!names.length }; + if(watchedQueryParams && watchedQueryParams.length > 0) { + currentResult.queryParams = activeQueryParams; + } + result.push(currentResult); } return result; @@ -24715,7 +28690,11 @@ define("route-recognizer", regex += segment.regex(); } - handlers.push({ handler: route.handler, names: names }); + var handler = { handler: route.handler, names: names }; + if(route.queryParams) { + handler.queryParams = route.queryParams; + } + handlers.push(handler); } if (isEmpty) { @@ -24767,12 +28746,61 @@ define("route-recognizer", if (output.charAt(0) !== '/') { output = '/' + output; } + if (params && params.queryParams) { + output += this.generateQueryString(params.queryParams, route.handlers); + } + return output; }, + generateQueryString: function(params, handlers) { + var pairs = [], allowedParams = []; + for(var i=0; i < handlers.length; i++) { + var currentParamList = handlers[i].queryParams; + if(currentParamList) { + allowedParams.push.apply(allowedParams, currentParamList); + } + } + for(var key in params) { + if (params.hasOwnProperty(key)) { + if(!~allowedParams.indexOf(key)) { + throw 'Query param "' + key + '" is not specified as a valid param for this route'; + } + var value = params[key]; + var pair = encodeURIComponent(key); + if(value !== true) { + pair += "=" + encodeURIComponent(value); + } + pairs.push(pair); + } + } + + if (pairs.length === 0) { return ''; } + + return "?" + pairs.join("&"); + }, + + parseQueryString: function(queryString) { + var pairs = queryString.split("&"), queryParams = {}; + for(var i=0; i < pairs.length; i++) { + var pair = pairs[i].split('='), + key = decodeURIComponent(pair[0]), + value = pair[1] ? decodeURIComponent(pair[1]) : true; + queryParams[key] = value; + } + return queryParams; + }, + recognize: function(path) { var states = [ this.rootState ], - pathLen, i, l; + pathLen, i, l, queryStart, queryParams = {}; + + queryStart = path.indexOf('?'); + if (~queryStart) { + var queryString = path.substr(queryStart + 1, path.length); + path = path.substr(0, queryStart); + queryParams = this.parseQueryString(queryString); + } // DEBUG GROUP path @@ -24800,7 +28828,7 @@ define("route-recognizer", var state = solutions[0]; if (state && state.handlers) { - return findHandler(state, path); + return findHandler(state, path, queryParams); } } }; @@ -24825,12 +28853,25 @@ define("route-recognizer", if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); } this.matcher.addChild(this.path, target, callback, this.delegate); } + return this; + }, + + withQueryParams: function() { + if (arguments.length === 0) { throw new Error("you must provide arguments to the withQueryParams method"); } + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] !== "string") { + throw new Error('you should call withQueryParams with a list of strings, e.g. withQueryParams("foo", "bar")'); + } + } + var queryParams = [].slice.call(arguments); + this.matcher.addQueryParams(this.path, queryParams); } }; function Matcher(target) { this.routes = {}; this.children = {}; + this.queryParams = {}; this.target = target; } @@ -24839,6 +28880,10 @@ define("route-recognizer", this.routes[path] = handler; }, + addQueryParams: function(path, params) { + this.queryParams[path] = params; + }, + addChild: function(path, target, callback, delegate) { var matcher = new Matcher(target); this.children[path] = matcher; @@ -24865,23 +28910,26 @@ define("route-recognizer", }; } - function addRoute(routeArray, path, handler) { + function addRoute(routeArray, path, handler, queryParams) { var len = 0; for (var i=0, l=routeArray.length; i<l; i++) { len += routeArray[i].path.length; } path = path.substr(len); - routeArray.push({ path: path, handler: handler }); + var route = { path: path, handler: handler }; + if(queryParams) { route.queryParams = queryParams; } + routeArray.push(route); } function eachRoute(baseRoute, matcher, callback, binding) { var routes = matcher.routes; + var queryParams = matcher.queryParams; for (var path in routes) { if (routes.hasOwnProperty(path)) { var routeArray = baseRoute.slice(); - addRoute(routeArray, path, routes[path]); + addRoute(routeArray, path, routes[path], queryParams[path]); if (matcher.children[path]) { eachRoute(routeArray, matcher.children[path], callback, binding); @@ -25020,9 +29068,9 @@ define("router", */ retry: function() { this.abort(); - var recogHandlers = this.router.recognizer.handlersFor(this.targetName), - newTransition = performTransition(this.router, recogHandlers, this.providedModelsArray, this.params, this.data); + handlerInfos = generateHandlerInfosWithQueryParams(this.router, recogHandlers, this.queryParams), + newTransition = performTransition(this.router, handlerInfos, this.providedModelsArray, this.params, this.queryParams, this.data); return newTransition; }, @@ -25047,6 +29095,10 @@ define("router", method: function(method) { this.urlMethod = method; return this; + }, + + toString: function() { + return "Transition (sequence " + this.sequence + ")"; } }; @@ -25191,8 +29243,21 @@ define("router", @param {Array[Object]} contexts @return {Object} a serialized parameter hash */ - paramsForHandler: function(handlerName, callback) { - return paramsForHandler(this, handlerName, slice.call(arguments, 1)); + + paramsForHandler: function(handlerName, contexts) { + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)); + return paramsForHandler(this, handlerName, partitionedArgs[0], partitionedArgs[1]); + }, + + /** + This method takes a handler name and returns a list of query params + that are valid to pass to the handler or its parents + + @param {String} handlerName + @return {Array[String]} a list of query parameters + */ + queryParamsForHandler: function (handlerName) { + return queryParamsForHandler(this, handlerName); }, /** @@ -25206,12 +29271,41 @@ define("router", @return {String} a URL */ generate: function(handlerName) { - var params = paramsForHandler(this, handlerName, slice.call(arguments, 1)); + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), + suppliedParams = partitionedArgs[0], + queryParams = partitionedArgs[1]; + + var params = paramsForHandler(this, handlerName, suppliedParams, queryParams), + validQueryParams = queryParamsForHandler(this, handlerName); + + var missingParams = []; + + for (var key in queryParams) { + if (queryParams.hasOwnProperty(key) && !~validQueryParams.indexOf(key)) { + missingParams.push(key); + } + } + + if (missingParams.length > 0) { + var err = 'You supplied the params '; + err += missingParams.map(function(param) { + return '"' + param + "=" + queryParams[param] + '"'; + }).join(' and '); + + err += ' which are not valid for the "' + handlerName + '" handler or its parents'; + + throw new Error(err); + } + return this.recognizer.generate(handlerName, params); }, isActive: function(handlerName) { - var contexts = slice.call(arguments, 1); + var partitionedArgs = extractQueryParams(slice.call(arguments, 1)), + contexts = partitionedArgs[0], + queryParams = partitionedArgs[1], + activeQueryParams = {}, + effectiveQueryParams = {}; var targetHandlerInfos = this.targetHandlerInfos, found = false, names, object, handlerInfo, handlerObj; @@ -25219,20 +29313,25 @@ define("router", if (!targetHandlerInfos) { return false; } var recogHandlers = this.recognizer.handlersFor(targetHandlerInfos[targetHandlerInfos.length - 1].name); - for (var i=targetHandlerInfos.length-1; i>=0; i--) { handlerInfo = targetHandlerInfos[i]; if (handlerInfo.name === handlerName) { found = true; } if (found) { - if (contexts.length === 0) { break; } + var recogHandler = recogHandlers[i]; - if (handlerInfo.isDynamic) { + merge(activeQueryParams, handlerInfo.queryParams); + if (queryParams !== false) { + merge(effectiveQueryParams, handlerInfo.queryParams); + mergeSomeKeys(effectiveQueryParams, queryParams, recogHandler.queryParams); + } + + if (handlerInfo.isDynamic && contexts.length > 0) { object = contexts.pop(); if (isParam(object)) { - var recogHandler = recogHandlers[i], name = recogHandler.names[0]; - if (object.toString() !== this.currentParams[name]) { return false; } + var name = recogHandler.names[0]; + if ("" + object !== this.currentParams[name]) { return false; } } else if (handlerInfo.context !== object) { return false; } @@ -25240,12 +29339,13 @@ define("router", } } - return contexts.length === 0 && found; + + return contexts.length === 0 && found && queryParamsEqual(activeQueryParams, effectiveQueryParams); }, trigger: function(name) { var args = slice.call(arguments); - trigger(this.currentHandlerInfos, false, args); + trigger(this, this.currentHandlerInfos, false, args); }, /** @@ -25263,7 +29363,7 @@ define("router", a shared pivot parent route and other data necessary to perform a transition. */ - function getMatchPoint(router, handlers, objects, inputParams) { + function getMatchPoint(router, handlers, objects, inputParams, queryParams) { var matchPoint = handlers.length, providedModels = {}, i, @@ -25318,6 +29418,12 @@ define("router", } } + // If there is an old handler, see if query params are the same. If there isn't an old handler, + // hasChanged will already be true here + if(oldHandlerInfo && !queryParamsEqual(oldHandlerInfo.queryParams, handlerObj.queryParams)) { + hasChanged = true; + } + if (hasChanged) { matchPoint = i; } } @@ -25342,8 +29448,8 @@ define("router", } } else if (activeTransition) { // Use model from previous transition attempt, preferably the resolved one. - return (paramName && activeTransition.providedModels[handlerName]) || - activeTransition.resolvedModels[handlerName]; + return activeTransition.resolvedModels[handlerName] || + (paramName && activeTransition.providedModels[handlerName]); } } @@ -25351,6 +29457,28 @@ define("router", return (typeof object === "string" || object instanceof String || !isNaN(object)); } + + + /** + @private + + This method takes a handler name and returns a list of query params + that are valid to pass to the handler or its parents + + @param {Router} router + @param {String} handlerName + @return {Array[String]} a list of query parameters + */ + function queryParamsForHandler(router, handlerName) { + var handlers = router.recognizer.handlersFor(handlerName), + queryParams = []; + + for (var i = 0; i < handlers.length; i++) { + queryParams.push.apply(queryParams, handlers[i].queryParams || []); + } + + return queryParams; + } /** @private @@ -25362,13 +29490,17 @@ define("router", @param {Array[Object]} objects @return {Object} a serialized parameter hash */ - function paramsForHandler(router, handlerName, objects) { + function paramsForHandler(router, handlerName, objects, queryParams) { var handlers = router.recognizer.handlersFor(handlerName), params = {}, - matchPoint = getMatchPoint(router, handlers, objects).matchPoint, + handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams), + matchPoint = getMatchPoint(router, handlerInfos, objects).matchPoint, + mergedQueryParams = {}, object, handlerObj, handler, names, i; + params.queryParams = {}; + for (i=0; i<handlers.length; i++) { handlerObj = handlers[i]; handler = router.getHandler(handlerObj.handler); @@ -25387,7 +29519,13 @@ define("router", // Serialize to generate params merge(params, serialize(handler, object, names)); } + if (queryParams !== false) { + mergeSomeKeys(params.queryParams, router.currentQueryParams, handlerObj.queryParams); + mergeSomeKeys(params.queryParams, queryParams, handlerObj.queryParams); + } } + + if (queryParamsEqual(params.queryParams, {})) { delete params.queryParams; } return params; } @@ -25397,24 +29535,84 @@ define("router", } } + function mergeSomeKeys(hash, other, keys) { + if (!other || !keys) { return; } + for(var i = 0; i < keys.length; i++) { + var key = keys[i], value; + if(other.hasOwnProperty(key)) { + value = other[key]; + if(value === null || value === false || typeof value === "undefined") { + delete hash[key]; + } else { + hash[key] = other[key]; + } + } + } + } + + /** + @private + */ + + function generateHandlerInfosWithQueryParams(router, handlers, queryParams) { + var handlerInfos = []; + + for (var i = 0; i < handlers.length; i++) { + var handler = handlers[i], + handlerInfo = { handler: handler.handler, names: handler.names, context: handler.context, isDynamic: handler.isDynamic }, + activeQueryParams = {}; + + if (queryParams !== false) { + mergeSomeKeys(activeQueryParams, router.currentQueryParams, handler.queryParams); + mergeSomeKeys(activeQueryParams, queryParams, handler.queryParams); + } + + if (handler.queryParams && handler.queryParams.length > 0) { + handlerInfo.queryParams = activeQueryParams; + } + + handlerInfos.push(handlerInfo); + } + + return handlerInfos; + } + + /** + @private + */ + function createQueryParamTransition(router, queryParams) { + var currentHandlers = router.currentHandlerInfos, + currentHandler = currentHandlers[currentHandlers.length - 1], + name = currentHandler.name; + + log(router, "Attempting query param transition"); + + return createNamedTransition(router, [name, queryParams]); + } + /** @private */ function createNamedTransition(router, args) { - var handlers = router.recognizer.handlersFor(args[0]); + var partitionedArgs = extractQueryParams(args), + pureArgs = partitionedArgs[0], + queryParams = partitionedArgs[1], + handlers = router.recognizer.handlersFor(pureArgs[0]), + handlerInfos = generateHandlerInfosWithQueryParams(router, handlers, queryParams); - log(router, "Attempting transition to " + args[0]); - return performTransition(router, handlers, slice.call(args, 1), router.currentParams); + log(router, "Attempting transition to " + pureArgs[0]); + + return performTransition(router, handlerInfos, slice.call(pureArgs, 1), router.currentParams, queryParams); } /** @private */ function createURLTransition(router, url) { - var results = router.recognizer.recognize(url), - currentHandlerInfos = router.currentHandlerInfos; + currentHandlerInfos = router.currentHandlerInfos, + queryParams = {}; log(router, "Attempting URL transition to " + url); @@ -25422,7 +29620,11 @@ define("router", return errorTransition(router, new Router.UnrecognizedURLError(url)); } - return performTransition(router, results, [], {}); + for(var i = 0; i < results.length; i++) { + merge(queryParams, results[i].queryParams); + } + + return performTransition(router, results, [], {}, queryParams); } @@ -25489,10 +29691,6 @@ define("router", eachHandler(partition.entered, function(handlerInfo) { handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); }); - - if (router.didTransition) { - router.didTransition(handlerInfos); - } } /** @@ -25510,13 +29708,14 @@ define("router", checkAbort(transition); setContext(handler, context); + setQueryParams(handler, handlerInfo.queryParams); - if (handler.setup) { handler.setup(context); } + if (handler.setup) { handler.setup(context, handlerInfo.queryParams); } checkAbort(transition); } catch(e) { if (!(e instanceof Router.TransitionAborted)) { // Trigger the `error` event starting from this failed handler. - trigger(currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]); + trigger(transition.router, currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]); } // Propagate the error so that the transition promise will reject. @@ -25542,6 +29741,29 @@ define("router", } } + /** + @private + + determines if two queryparam objects are the same or not + **/ + function queryParamsEqual(a, b) { + a = a || {}; + b = b || {}; + var checkedKeys = [], key; + for(key in a) { + if (!a.hasOwnProperty(key)) { continue; } + if(b[key] !== a[key]) { return false; } + checkedKeys.push(key); + } + for(key in b) { + if (!b.hasOwnProperty(key)) { continue; } + if (~checkedKeys.indexOf(key)) { continue; } + // b has a key not in a + return false; + } + return true; + } + /** @private @@ -25591,19 +29813,21 @@ define("router", unchanged: [] }; - var handlerChanged, contextChanged, i, l; + var handlerChanged, contextChanged, queryParamsChanged, i, l; for (i=0, l=newHandlers.length; i<l; i++) { var oldHandler = oldHandlers[i], newHandler = newHandlers[i]; if (!oldHandler || oldHandler.handler !== newHandler.handler) { handlerChanged = true; + } else if (!queryParamsEqual(oldHandler.queryParams, newHandler.queryParams)) { + queryParamsChanged = true; } if (handlerChanged) { handlers.entered.push(newHandler); if (oldHandler) { handlers.exited.unshift(oldHandler); } - } else if (contextChanged || oldHandler.context !== newHandler.context) { + } else if (contextChanged || oldHandler.context !== newHandler.context || queryParamsChanged) { contextChanged = true; handlers.updatedContext.push(newHandler); } else { @@ -25618,7 +29842,11 @@ define("router", return handlers; } - function trigger(handlerInfos, ignoreFailure, args) { + function trigger(router, handlerInfos, ignoreFailure, args) { + if (router.triggerEvent) { + router.triggerEvent(handlerInfos, ignoreFailure, args); + return; + } var name = args.shift(); @@ -25652,20 +29880,45 @@ define("router", if (handler.contextDidChange) { handler.contextDidChange(); } } + function setQueryParams(handler, queryParams) { + handler.queryParams = queryParams; + if (handler.queryParamsDidChange) { handler.queryParamsDidChange(); } + } + + + /** + @private + + Extracts query params from the end of an array + **/ + + function extractQueryParams(array) { + var len = (array && array.length), head, queryParams; + + if(len && len > 0 && array[len - 1] && array[len - 1].hasOwnProperty('queryParams')) { + queryParams = array[len - 1].queryParams; + head = slice.call(array, 0, len - 1); + return [head, queryParams]; + } else { + return [array, null]; + } + } + /** @private Creates, begins, and returns a Transition. */ - function performTransition(router, recogHandlers, providedModelsArray, params, data) { + function performTransition(router, recogHandlers, providedModelsArray, params, queryParams, data) { - var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params), + var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params, queryParams), targetName = recogHandlers[recogHandlers.length - 1].handler, - wasTransitioning = false; + wasTransitioning = false, + currentHandlerInfos = router.currentHandlerInfos; // Check if there's already a transition underway. if (router.activeTransition) { - if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray)) { + if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray, queryParams)) { return router.activeTransition; } router.activeTransition.abort(); @@ -25680,6 +29933,7 @@ define("router", transition.providedModelsArray = providedModelsArray; transition.params = matchPointResults.params; transition.data = data || {}; + transition.queryParams = queryParams; router.activeTransition = transition; var handlerInfos = generateHandlerInfos(router, recogHandlers); @@ -25687,7 +29941,7 @@ define("router", // Fire 'willTransition' event on current handlers, but don't fire it // if a transition was already underway. if (!wasTransitioning) { - trigger(router.currentHandlerInfos, true, ['willTransition', transition]); + trigger(router, currentHandlerInfos, true, ['willTransition', transition]); } log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); @@ -25700,7 +29954,20 @@ define("router", checkAbort(transition); try { - finalizeTransition(transition, handlerInfos); + log(router, transition.sequence, "Validation succeeded, finalizing transition;"); + + // Don't overwrite contexts / update URL if this was a noop transition. + if (!currentHandlerInfos || !currentHandlerInfos.length || + !router.recognizer.hasRoute(currentHandlerInfos[currentHandlerInfos.length - 1].name) || + currentHandlerInfos.length !== matchPointResults.matchPoint) { + finalizeTransition(transition, handlerInfos); + } + + if (router.didTransition) { + router.didTransition(handlerInfos); + } + + log(router, transition.sequence, "TRANSITION COMPLETE."); // Resolve with the final handler. deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); @@ -25733,11 +30000,16 @@ define("router", var handlerObj = recogHandlers[i], isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length); - handlerInfos.push({ + + var handlerInfo = { isDynamic: !!isDynamic, name: handlerObj.handler, handler: router.getHandler(handlerObj.handler) - }); + }; + if(handlerObj.queryParams) { + handlerInfo.queryParams = handlerObj.queryParams; + } + handlerInfos.push(handlerInfo); } return handlerInfos; } @@ -25745,7 +30017,7 @@ define("router", /** @private */ - function transitionsIdentical(oldTransition, targetName, providedModelsArray) { + function transitionsIdentical(oldTransition, targetName, providedModelsArray, queryParams) { if (oldTransition.targetName !== targetName) { return false; } @@ -25755,6 +30027,11 @@ define("router", for (var i = 0, len = oldModels.length; i < len; ++i) { if (oldModels[i] !== providedModelsArray[i]) { return false; } } + + if(!queryParamsEqual(oldTransition.queryParams, queryParams)) { + return false; + } + return true; } @@ -25768,13 +30045,12 @@ define("router", var router = transition.router, seq = transition.sequence, - handlerName = handlerInfos[handlerInfos.length - 1].name; - - log(router, seq, "Validation succeeded, finalizing transition;"); + handlerName = handlerInfos[handlerInfos.length - 1].name, + i; // Collect params for URL. var objects = [], providedModels = transition.providedModelsArray.slice(); - for (var i = handlerInfos.length - 1; i>=0; --i) { + for (i = handlerInfos.length - 1; i>=0; --i) { var handlerInfo = handlerInfos[i]; if (handlerInfo.isDynamic) { var providedModel = providedModels.pop(); @@ -25782,12 +30058,18 @@ define("router", } } - var params = paramsForHandler(router, handlerName, objects); + var newQueryParams = {}; + for (i = handlerInfos.length - 1; i>=0; --i) { + merge(newQueryParams, handlerInfos[i].queryParams); + } + router.currentQueryParams = newQueryParams; + + + var params = paramsForHandler(router, handlerName, objects, transition.queryParams); - transition.providedModelsArray = []; - transition.providedContexts = {}; router.currentParams = params; + var urlMethod = transition.urlMethod; if (urlMethod) { var url = router.recognizer.generate(handlerName, params); @@ -25801,7 +30083,6 @@ define("router", } setupContexts(transition, handlerInfos); - log(router, seq, "TRANSITION COMPLETE."); } /** @@ -25830,7 +30111,9 @@ define("router", // We're before the match point, so don't run any hooks, // just use the already resolved context from the handler. - transition.resolvedModels[handlerInfo.name] = handlerInfo.handler.context; + transition.resolvedModels[handlerInfo.name] = + transition.providedModels[handlerInfo.name] || + handlerInfo.handler.context; return proceed(); } @@ -25841,8 +30124,8 @@ define("router", .then(handleAbort) .then(afterModel) .then(handleAbort) - .then(proceed) - .then(null, handleError); + .then(null, handleError) + .then(proceed); function handleAbort(result) { if (transition.isAborted) { @@ -25867,11 +30150,7 @@ define("router", // An error was thrown / promise rejected, so fire an // `error` event from this handler info up to root. - trigger(handlerInfos.slice(0, index + 1), true, ['error', reason, transition]); - - if (handler.error) { - handler.error(reason, transition); - } + trigger(router, handlerInfos.slice(0, index + 1), true, ['error', reason, transition]); // Propagate the original error. return RSVP.reject(reason); @@ -25881,13 +30160,20 @@ define("router", log(router, seq, handlerName + ": calling beforeModel hook"); - var p = handler.beforeModel && handler.beforeModel(transition); + var args; + + if (handlerInfo.queryParams) { + args = [handlerInfo.queryParams, transition]; + } else { + args = [transition]; + } + + var p = handler.beforeModel && handler.beforeModel.apply(handler, args); return (p instanceof Transition) ? null : p; } function model() { log(router, seq, handlerName + ": resolving model"); - var p = getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); return (p instanceof Transition) ? null : p; } @@ -25902,7 +30188,15 @@ define("router", transition.resolvedModels[handlerInfo.name] = context; - var p = handler.afterModel && handler.afterModel(context, transition); + var args; + + if (handlerInfo.queryParams) { + args = [context, handlerInfo.queryParams, transition]; + } else { + args = [context, transition]; + } + + var p = handler.afterModel && handler.afterModel.apply(handler, args); return (p instanceof Transition) ? null : p; } @@ -25933,9 +30227,8 @@ define("router", or use one of the models provided to `transitionTo`. */ function getModel(handlerInfo, transition, handlerParams, needsUpdate) { - var handler = handlerInfo.handler, - handlerName = handlerInfo.name; + handlerName = handlerInfo.name, args; if (!needsUpdate && handler.hasOwnProperty('context')) { return handler.context; @@ -25946,7 +30239,13 @@ define("router", return typeof providedModel === 'function' ? providedModel() : providedModel; } - return handler.model && handler.model(handlerParams || {}, transition); + if (handlerInfo.queryParams) { + args = [handlerParams || {}, handlerInfo.queryParams, transition]; + } else { + args = [handlerParams || {}, transition, handlerInfo.queryParams]; + } + + return handler.model && handler.model.apply(handler, args); } /** @@ -25979,10 +30278,12 @@ define("router", // Normalize blank transitions to root URL transitions. var name = args[0] || '/'; - if (name.charAt(0) === '/') { + if(args.length === 1 && args[0].hasOwnProperty('queryParams')) { + return createQueryParamTransition(router, args[0]); + } else if (name.charAt(0) === '/') { return createURLTransition(router, name); } else { - return createNamedTransition(router, args); + return createNamedTransition(router, slice.call(args)); } } @@ -26060,21 +30361,21 @@ DSL.prototype = { if (callback) { var dsl = new DSL(name); callback.call(dsl); - this.push(options.path, name, dsl.generate()); + this.push(options.path, name, dsl.generate(), options.queryParams); } else { - this.push(options.path, name); + this.push(options.path, name, null, options.queryParams); } }, - push: function(url, name, callback) { + push: function(url, name, callback, queryParams) { var parts = name.split('.'); if (url === "" || url === "/" || parts[parts.length-1] === "index") { this.explicitIndex = true; } - this.matches.push([url, name, callback]); + this.matches.push([url, name, callback, queryParams]); }, route: function(name, options) { - Ember.assert("You must use `this.resource` to nest", typeof options !== 'function'); + options = options || {}; @@ -26086,7 +30387,7 @@ DSL.prototype = { name = this.parent + "." + name; } - this.push(options.path, name); + this.push(options.path, name, null, options.queryParams); }, generate: function() { @@ -26099,7 +30400,12 @@ DSL.prototype = { return function(match) { for (var i=0, l=dslMatches.length; i<l; i++) { var dslMatch = dslMatches[i]; - match(dslMatch[0]).to(dslMatch[1], dslMatch[2]); + var matchObj = match(dslMatch[0]).to(dslMatch[1], dslMatch[2]); + if (Ember.FEATURES.isEnabled("query-params")) { + if(dslMatch[3]) { + matchObj.withQueryParams.apply(matchObj, dslMatch[3]); + } + } } }; } @@ -26125,43 +30431,51 @@ var get = Ember.get; @submodule ember-routing */ +/** + + Finds a controller instance. + + @for Ember + @method controllerFor + @private +*/ Ember.controllerFor = function(container, controllerName, lookupOptions) { return container.lookup('controller:' + controllerName, lookupOptions); }; -/* + +/** Generates a controller automatically if none was provided. The type of generated controller depends on the context. You can customize your generated controllers by defining `App.ObjectController` and `App.ArrayController` + + @for Ember + @method generateController + @private */ Ember.generateController = function(container, controllerName, context) { - var controller, DefaultController, fullName, instance; + var ControllerFactory, fullName, instance, name, factoryName, controllerType; if (context && Ember.isArray(context)) { - DefaultController = container.resolve('controller:array'); - controller = DefaultController.extend({ - isGenerated: true - }); + controllerType = 'array'; } else if (context) { - DefaultController = container.resolve('controller:object'); - controller = DefaultController.extend({ - isGenerated: true - }); + controllerType = 'object'; } else { - DefaultController = container.resolve('controller:basic'); - controller = DefaultController.extend({ - isGenerated: true - }); + controllerType = 'basic'; } - controller.toString = function() { - return "(generated " + controllerName + " controller)"; - }; + factoryName = 'controller:' + controllerType; - controller.isGenerated = true; + ControllerFactory = container.lookupFactory(factoryName).extend({ + isGenerated: true, + toString: function() { + return "(generated " + controllerName + " controller)"; + } + }); fullName = 'controller:' + controllerName; - container.register(fullName, controller); + + container.register(fullName, ControllerFactory); instance = container.lookup(fullName); @@ -26187,22 +30501,6 @@ var get = Ember.get, set = Ember.set; var defineProperty = Ember.defineProperty; var DefaultView = Ember._MetamorphView; -function setupLocation(router) { - var location = get(router, 'location'), - rootURL = get(router, 'rootURL'), - options = {}; - - if (typeof rootURL === 'string') { - options.rootURL = rootURL; - } - - if ('string' === typeof location) { - options.implementation = location; - location = set(router, 'location', Ember.Location.create(options)); - - } -} - /** The `Ember.Router` class manages the application state and URLs. Refer to the [routing guide](http://emberjs.com/guides/routing/) for documentation. @@ -26217,7 +30515,7 @@ Ember.Router = Ember.Object.extend({ init: function() { this.router = this.constructor.router || this.constructor.map(Ember.K); this._activeViews = {}; - setupLocation(this); + this._setupLocation(); }, url: Ember.computed(function() { @@ -26232,7 +30530,7 @@ Ember.Router = Ember.Object.extend({ container = this.container, self = this; - setupRouter(this, router, location); + this._setupRouter(router, location); container.register('view:default', DefaultView); container.register('view:toplevel', Ember.View.extend()); @@ -26246,12 +30544,14 @@ Ember.Router = Ember.Object.extend({ didTransition: function(infos) { var appController = this.container.lookup('controller:application'), - path = routePath(infos); + path = Ember.Router._routePath(infos); - if (!('currentPath' in appController)) { - defineProperty(appController, 'currentPath'); - } + if (!('currentPath' in appController)) { defineProperty(appController, 'currentPath'); } set(appController, 'currentPath', path); + + if (!('currentRouteName' in appController)) { defineProperty(appController, 'currentRouteName'); } + set(appController, 'currentRouteName', infos[infos.length - 1].name); + this.notifyPropertyChange('url'); if (get(this, 'namespace').LOG_TRANSITIONS) { @@ -26260,15 +30560,15 @@ Ember.Router = Ember.Object.extend({ }, handleURL: function(url) { - return doTransition(this, 'handleURL', [url]); + return this._doTransition('handleURL', [url]); }, transitionTo: function() { - return doTransition(this, 'transitionTo', arguments); + return this._doTransition('transitionTo', arguments); }, replaceWith: function() { - return doTransition(this, 'replaceWith', arguments); + return this._doTransition('replaceWith', arguments); }, generate: function() { @@ -26319,166 +30619,212 @@ Ember.Router = Ember.Object.extend({ this._activeViews[templateName] = [view, disconnect]; view.one('willDestroyElement', this, disconnect); + }, + + _setupLocation: function() { + var location = get(this, 'location'), + rootURL = get(this, 'rootURL'), + options = {}; + + if (typeof rootURL === 'string') { + options.rootURL = rootURL; + } + + if ('string' === typeof location) { + options.implementation = location; + location = set(this, 'location', Ember.Location.create(options)); + } + }, + + _getHandlerFunction: function() { + var seen = {}, container = this.container, + DefaultRoute = container.lookupFactory('route:basic'), + self = this; + + return function(name) { + var routeName = 'route:' + name, + handler = container.lookup(routeName); + + if (seen[name]) { return handler; } + + seen[name] = true; + + if (!handler) { + if (name === 'loading') { return {}; } + + container.register(routeName, DefaultRoute.extend()); + handler = container.lookup(routeName); + + if (get(self, 'namespace.LOG_ACTIVE_GENERATION')) { + Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + } + } + + if (name === 'application') { + // Inject default `error` handler. + // Note: `events` is deprecated, but we'll let the + // deprecation warnings be handled at event-handling time rather + // than duplicating that logic here. + var actions = handler._actions || handler.events; + if (!actions) { actions = handler._actions = {}; } + actions.error = actions.error || Ember.Router._defaultErrorHandler; + } + + handler.routeName = name; + return handler; + }; + }, + + _setupRouter: function(router, location) { + var lastURL, emberRouter = this; + + router.getHandler = this._getHandlerFunction(); + + var doUpdateURL = function() { + location.setURL(lastURL); + }; + + router.updateURL = function(path) { + lastURL = path; + Ember.run.once(doUpdateURL); + }; + + if (location.replaceURL) { + var doReplaceURL = function() { + location.replaceURL(lastURL); + }; + + router.replaceURL = function(path) { + lastURL = path; + Ember.run.once(doReplaceURL); + }; + } + + router.didTransition = function(infos) { + emberRouter.didTransition(infos); + }; + }, + + _doTransition: function(method, args) { + // Normalize blank route to root URL. + args = [].slice.call(args); + args[0] = args[0] || '/'; + + var passedName = args[0], name, self = this, + isQueryParamsOnly = false; + + if (Ember.FEATURES.isEnabled("query-params")) { + isQueryParamsOnly = (args.length === 1 && args[0].hasOwnProperty('queryParams')); + } + + if (!isQueryParamsOnly && passedName.charAt(0) === '/') { + name = passedName; + } else if (!isQueryParamsOnly) { + if (!this.router.hasRoute(passedName)) { + name = args[0] = passedName + '.index'; + } else { + name = passedName; + } + + } + + var transitionPromise = this.router[method].apply(this.router, args); + + // Don't schedule loading state entry if user has already aborted the transition. + if (this.router.activeTransition) { + this._scheduleLoadingStateEntry(); + } + + transitionPromise.then(function(route) { + self._transitionCompleted(route); + }, function(error) { + if (error.name === "UnrecognizedURLError") { + + } + }); + + // We want to return the configurable promise object + // so that callers of this function can use `.method()` on it, + // which obviously doesn't exist for normal RSVP promises. + return transitionPromise; + }, + + _scheduleLoadingStateEntry: function() { + if (this._loadingStateActive) { return; } + this._shouldEnterLoadingState = true; + Ember.run.scheduleOnce('routerTransitions', this, this._enterLoadingState); + }, + + _enterLoadingState: function() { + if (this._loadingStateActive || !this._shouldEnterLoadingState) { return; } + + var loadingRoute = this.router.getHandler('loading'); + if (loadingRoute) { + if (loadingRoute.enter) { loadingRoute.enter(); } + if (loadingRoute.setup) { loadingRoute.setup(); } + this._loadingStateActive = true; + } + }, + + _exitLoadingState: function () { + this._shouldEnterLoadingState = false; + if (!this._loadingStateActive) { return; } + + var loadingRoute = this.router.getHandler('loading'); + if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } + this._loadingStateActive = false; + }, + + _transitionCompleted: function(route) { + this.notifyPropertyChange('url'); + this._exitLoadingState(); } }); -function getHandlerFunction(router) { - var seen = {}, container = router.container, - DefaultRoute = container.resolve('route:basic'); +function triggerEvent(handlerInfos, ignoreFailure, args) { + var name = args.shift(); - return function(name) { - var routeName = 'route:' + name, - handler = container.lookup(routeName); + if (!handlerInfos) { + if (ignoreFailure) { return; } + throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); + } - if (seen[name]) { return handler; } + var eventWasHandled = false; - seen[name] = true; + for (var i=handlerInfos.length-1; i>=0; i--) { + var handlerInfo = handlerInfos[i], + handler = handlerInfo.handler; - if (!handler) { - if (name === 'loading') { return {}; } + if (handler._actions && handler._actions[name]) { + if (handler._actions[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; + } + } else if (handler.events && handler.events[name]) { - container.register(routeName, DefaultRoute.extend()); - handler = container.lookup(routeName); - - if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) { - Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); + if (handler.events[name].apply(handler, args) === true) { + eventWasHandled = true; + } else { + return; } } - - if (name === 'application') { - // Inject default `error` handler. - handler.events = handler.events || {}; - handler.events.error = handler.events.error || defaultErrorHandler; - } - - handler.routeName = name; - return handler; - }; -} - -function defaultErrorHandler(error, transition) { - Ember.Logger.error('Error while loading route:', error); - - // Using setTimeout allows us to escape from the Promise's try/catch block - setTimeout(function() { throw error; }); -} - - -function routePath(handlerInfos) { - var path = []; - - for (var i=1, l=handlerInfos.length; i<l; i++) { - var name = handlerInfos[i].name, - nameParts = name.split("."); - - path.push(nameParts[nameParts.length - 1]); } - return path.join("."); -} - -function setupRouter(emberRouter, router, location) { - var lastURL; - - router.getHandler = getHandlerFunction(emberRouter); - - var doUpdateURL = function() { - location.setURL(lastURL); - }; - - router.updateURL = function(path) { - lastURL = path; - Ember.run.once(doUpdateURL); - }; - - if (location.replaceURL) { - var doReplaceURL = function() { - location.replaceURL(lastURL); - }; - - router.replaceURL = function(path) { - lastURL = path; - Ember.run.once(doReplaceURL); - }; + if (!eventWasHandled && !ignoreFailure) { + throw new Error("Nothing handled the event '" + name + "'."); } - - router.didTransition = function(infos) { - emberRouter.didTransition(infos); - }; -} - -function doTransition(router, method, args) { - // Normalize blank route to root URL. - args = [].slice.call(args); - args[0] = args[0] || '/'; - - var passedName = args[0], name; - - if (passedName.charAt(0) === '/') { - name = passedName; - } else { - if (!router.router.hasRoute(passedName)) { - name = args[0] = passedName + '.index'; - } else { - name = passedName; - } - - Ember.assert("The route " + passedName + " was not found", router.router.hasRoute(name)); - } - - var transitionPromise = router.router[method].apply(router.router, args); - - // Don't schedule loading state entry if user has already aborted the transition. - if (router.router.activeTransition) { - scheduleLoadingStateEntry(router); - } - - transitionPromise.then(transitionCompleted); - - // We want to return the configurable promise object - // so that callers of this function can use `.method()` on it, - // which obviously doesn't exist for normal RSVP promises. - return transitionPromise; -} - -function scheduleLoadingStateEntry(router) { - if (router._loadingStateActive) { return; } - router._shouldEnterLoadingState = true; - Ember.run.scheduleOnce('routerTransitions', null, enterLoadingState, router); -} - -function enterLoadingState(router) { - if (router._loadingStateActive || !router._shouldEnterLoadingState) { return; } - - var loadingRoute = router.router.getHandler('loading'); - if (loadingRoute) { - if (loadingRoute.enter) { loadingRoute.enter(); } - if (loadingRoute.setup) { loadingRoute.setup(); } - router._loadingStateActive = true; - } -} - -function exitLoadingState(router) { - router._shouldEnterLoadingState = false; - if (!router._loadingStateActive) { return; } - - var loadingRoute = router.router.getHandler('loading'); - if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } - router._loadingStateActive = false; -} - -function transitionCompleted(route) { - var router = route.router; - router.notifyPropertyChange('url'); - exitLoadingState(router); } Ember.Router.reopenClass({ + router: null, map: function(callback) { var router = this.router; if (!router) { - router = this.router = new Router(); + router = new Router(); router.callbacks = []; + router.triggerEvent = triggerEvent; + this.reopenClass({ router: router }); } if (get(this, 'namespace.LOG_TRANSITIONS_INTERNAL')) { @@ -26497,6 +30843,26 @@ Ember.Router.reopenClass({ router.callbacks.push(callback); router.map(dsl.generate()); return router; + }, + + _defaultErrorHandler: function(error, transition) { + Ember.Logger.error('Error while loading route:', error); + + // Using setTimeout allows us to escape from the Promise's try/catch block + setTimeout(function() { throw error; }); + }, + + _routePath: function(handlerInfos) { + var path = []; + + for (var i=1, l=handlerInfos.length; i<l; i++) { + var name = handlerInfos[i].name, + nameParts = name.split("."); + + path.push(nameParts[nameParts.length - 1]); + } + + return path.join("."); } }); @@ -26512,6 +30878,7 @@ Ember.Router.reopenClass({ */ var get = Ember.get, set = Ember.set, + getProperties = Ember.getProperties, classify = Ember.String.classify, fmt = Ember.String.fmt, a_forEach = Ember.EnumerableUtils.forEach, @@ -26525,7 +30892,7 @@ var get = Ember.get, set = Ember.set, @namespace Ember @extends Ember.Object */ -Ember.Route = Ember.Object.extend({ +Ember.Route = Ember.Object.extend(Ember.ActionHandler, { /** @private @@ -26552,28 +30919,124 @@ Ember.Route = Ember.Object.extend({ These functions will be invoked when a matching `{{action}}` is triggered from within a template and the application's current route is this route. - Events can also be invoked from other parts of your application via `Route#send` + Actions can also be invoked from other parts of your application via `Route#send` or `Controller#send`. - The context of the event will be this route. + The `actions` hash will inherit action handlers from + the `actions` hash defined on extended Route parent classes + or mixins rather than just replace the entire hash, e.g.: + + ```js + App.CanDisplayBanner = Ember.Mixin.create({ + actions: { + displayBanner: function(msg) { + // ... + } + } + }); + + App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { + actions: { + playMusic: function() { + // ... + } + } + }); + + // `WelcomeRoute`, when active, will be able to respond + // to both actions, since the actions hash is merged rather + // then replaced when extending mixins / parent classes. + this.send('displayBanner'); + this.send('playMusic'); + ``` + + Within a route's action handler, the value of the `this` context + is the Route object: + + ```js + App.SongRoute = Ember.Route.extend({ + actions: { + myAction: function() { + this.controllerFor("song"); + this.transitionTo("other.route"); + ... + } + } + }); + ``` + + It is also possible to call `this._super()` from within an + action handler if it overrides a handler defined on a parent + class or mixin: + + Take for example the following routes: + + ```js + App.DebugRoute = Ember.Mixin.create({ + actions: { + debugRouteInformation: function() { + console.debug("trololo"); + } + } + }); + + App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { + actions: { + debugRouteInformation: function() { + // also call the debugRouteInformation of mixed in App.DebugRoute + this._super(); + + // show additional annoyance + window.alert(...); + } + } + }); + ``` ## Bubbling - By default, an event will stop bubbling once a handler defined - on the `events` hash handles it. To continue bubbling the event, - you must return `true` from the handler. + By default, an action will stop bubbling once a handler defined + on the `actions` hash handles it. To continue bubbling the action, + you must return `true` from the handler: - ## Built-in events + ```js + App.Router.map(function() { + this.resource("album", function() { + this.route("song"); + }); + }); - There are a few built-in events pertaining to transitions that you + App.AlbumRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + } + } + }); + + App.AlbumSongRoute = Ember.Route.extend({ + actions: { + startPlaying: function() { + // ... + + if (actionShouldAlsoBeTriggeredOnParentRoute) { + return true; + } + } + } + }); + ``` + + ## Built-in actions + + There are a few built-in actions pertaining to transitions that you can use to customize transition behavior: `willTransition` and `error`. ### `willTransition` - The `willTransition` event is fired at the beginning of any + The `willTransition` action is fired at the beginning of any attempted transition with a `Transition` object as the sole - argument. This event can be used for aborting, redirecting, + argument. This action can be used for aborting, redirecting, or decorating the transition from the currently active routes. A good example is preventing navigation when a form is @@ -26581,7 +31044,7 @@ Ember.Route = Ember.Object.extend({ ```js App.ContactFormRoute = Ember.Route.extend({ - events: { + actions: { willTransition: function(transition) { if (this.controller.get('userHasEnteredData')) { this.controller.displayNavigationConfirm(); @@ -26597,7 +31060,7 @@ Ember.Route = Ember.Object.extend({ Note that `willTransition` will not be fired for the redirecting `transitionTo`, since `willTransition` doesn't fire when there is already a transition underway. If you want - subsequent `willTransition` events to fire for the redirecting + subsequent `willTransition` actions to fire for the redirecting transition, you must first explicitly call `transition.abort()`. @@ -26605,7 +31068,7 @@ Ember.Route = Ember.Object.extend({ When attempting to transition into a route, any of the hooks may throw an error, or return a promise that rejects, at which - point an `error` event will be fired on the partially-entered + point an `error` action will be fired on the partially-entered routes, allowing for per-route error handling logic, or shared error handling logic defined on a parent route. @@ -26622,7 +31085,7 @@ Ember.Route = Ember.Object.extend({ return Ember.RSVP.reject("bad things!"); }, - events: { + actions: { error: function(error, transition) { // Assuming we got here due to the error in `beforeModel`, // we can expect that error === "bad things!", @@ -26640,14 +31103,14 @@ Ember.Route = Ember.Object.extend({ }); ``` - `error` events that bubble up all the way to `ApplicationRoute` + `error` actions that bubble up all the way to `ApplicationRoute` will fire a default error handler that logs the error. You can specify your own global default error handler by overriding the `error` handler on `ApplicationRoute`: ```js App.ApplicationRoute = Ember.Route.extend({ - events: { + actions: { error: function(error, transition) { this.controllerFor('banner').displayError(error.message); } @@ -26658,12 +31121,22 @@ Ember.Route = Ember.Object.extend({ @see {Ember.Route#send} @see {Handlebars.helpers.action} - @property events + @property actions @type Hash @default null */ + actions: null, + + /** + @deprecated + + Please use `actions` instead. + @method events + */ events: null, + mergedProperties: ['events'], + /** This hook is executed when the router completely exits this route. It is not executed when the model for the route changes. @@ -26685,9 +31158,30 @@ Ember.Route = Ember.Object.extend({ route in question. The model will be serialized into the URL using the `serialize` hook. + Example + + ```javascript + App.Router.map(function() { + this.route("index"); + this.route("secret"); + this.route("fourOhFour", { path: "*:"}); + }); + + App.IndexRoute = Ember.Route.extend({ + actions: { + moveToSecret: function(context){ + if (authorized()){ + this.transitionTo('secret', context); + } + this.transitionTo('fourOhFour'); + } + } + }); + ``` + @method transitionTo @param {String} name the name of the route - @param {...Object} models the + @param {...Object} models */ transitionTo: function(name, context) { var router = this.router; @@ -26698,18 +31192,66 @@ Ember.Route = Ember.Object.extend({ Transition into another route while replacing the current URL if possible. Identical to `transitionTo` in all other respects. - Of the bundled location types, only `history` currently supports - this behavior. + Example + + ```javascript + App.Router.map(function() { + this.route("index"); + this.route("secret"); + }); + + App.SecretRoute = Ember.Route.extend({ + afterModel: function() { + if (!authorized()){ + this.replaceWith('index'); + } + } + }); + ``` @method replaceWith @param {String} name the name of the route - @param {...Object} models the + @param {...Object} models */ replaceWith: function() { var router = this.router; return this.router.replaceWith.apply(this.router, arguments); }, + /** + Sends an action to the router, which will delegate it to the currently + active route hierarchy per the bubbling rules explained under `actions`. + + Example + + ```javascript + App.Router.map(function() { + this.route("index"); + }); + + App.ApplicationRoute = Ember.Route.extend({ + actions: { + track: function(arg) { + console.log(arg, 'was clicked'); + } + } + }); + + App.IndexRoute = Ember.Route.extend({ + actions: { + trackIfDebug: function(arg) { + if (debug) { + this.send('track', arg); + } + } + } + }); + ``` + + @method send + @param {String} name the name of the action to trigger + @param {...*} args + */ send: function() { return this.router.send.apply(this.router, arguments); }, @@ -26721,7 +31263,7 @@ Ember.Route = Ember.Object.extend({ @method setup */ - setup: function(context) { + setup: function(context, queryParams) { var controllerName = this.controllerName || this.routeName, controller = this.controllerFor(controllerName, true); if (!controller) { @@ -26729,21 +31271,27 @@ Ember.Route = Ember.Object.extend({ } // Assign the route's controller so that it can more easily be - // referenced in event handlers + // referenced in action handlers this.controller = controller; + var args = [controller, context]; + + if (Ember.FEATURES.isEnabled("query-params")) { + args.push(queryParams); + } + if (this.setupControllers) { - Ember.deprecate("Ember.Route.setupControllers is deprecated. Please use Ember.Route.setupController(controller, model) instead."); + this.setupControllers(controller, context); } else { - this.setupController(controller, context); + this.setupController.apply(this, args); } if (this.renderTemplates) { - Ember.deprecate("Ember.Route.renderTemplates is deprecated. Please use Ember.Route.renderTemplate(controller, model) instead."); + this.renderTemplates(context); } else { - this.renderTemplate(controller, context); + this.renderTemplate.apply(this, args); } }, @@ -26834,6 +31382,7 @@ Ember.Route = Ember.Object.extend({ @method beforeModel @param {Transition} transition + @param {Object} queryParams the active query params for this route @return {Promise} if the value returned from this hook is a promise, the transition will pause until the transition resolves. Otherwise, non-promise return values are not @@ -26850,7 +31399,7 @@ Ember.Route = Ember.Object.extend({ resolved. ```js - App.PostRoute = Ember.Route.extend({ + App.PostsRoute = Ember.Route.extend({ afterModel: function(posts, transition) { if (posts.length === 1) { this.transitionTo('post.show', posts[0]); @@ -26864,13 +31413,16 @@ Ember.Route = Ember.Object.extend({ from this hook. @method afterModel + @param {Object} resolvedModel the value returned from `model`, + or its resolved value if it was a promise @param {Transition} transition + @param {Object} queryParams the active query params for this handler @return {Promise} if the value returned from this hook is a promise, the transition will pause until the transition resolves. Otherwise, non-promise return values are not utilized in any way. */ - afterModel: function(resolvedModel, transition) { + afterModel: function(resolvedModel, transition, queryParams) { this.redirect(resolvedModel, transition); }, @@ -26917,10 +31469,26 @@ Ember.Route = Ember.Object.extend({ if a promise returned from `model` fails, the error will be handled by the `error` hook on `Ember.Route`. + Example + + ```js + App.PostRoute = Ember.Route.extend({ + model: function(params) { + return App.Post.find(params.post_id); + } + }); + ``` + @method model @param {Object} params the parameters extracted from the URL + @param {Transition} transition + @param {Object} queryParams the query params for this route + @return {Object|Promise} the model for this route. If + a promise is returned, the transition will pause until + the promise resolves, and the resolved value of the promise + will be used as the model for this route. */ - model: function(params, resolvedParentModels) { + model: function(params, transition) { var match, name, sawParams, value; for (var prop in params) { @@ -26934,12 +31502,47 @@ Ember.Route = Ember.Object.extend({ if (!name && sawParams) { return params; } else if (!name) { return; } - var modelClass = this.container.lookupFactory('model:' + name); + return this.findModel(name, value); + }, + + /** + + @method findModel + @param {String} type the model type + @param {Object} value the value passed to find + */ + findModel: function(){ + var store = get(this, 'store'); + return store.find.apply(store, arguments); + }, + + /** + Store property provides a hook for data persistence libraries to inject themselves. + + By default, this store property provides the exact same functionality previously + in the model hook. + + Currently, the required interface is: + + `store.find(modelName, findArguments)` + + @method store + @param {Object} store + */ + store: Ember.computed(function(){ + var container = this.container; + var routeName = this.routeName; var namespace = get(this, 'router.namespace'); - Ember.assert("You used the dynamic segment " + name + "_id in your router, but " + namespace + "." + classify(name) + " did not exist and you did not override your route's `model` hook.", modelClass); - return modelClass.find(value); - }, + return { + find: function(name, value) { + var modelClass = container.lookupFactory('model:' + name); + + + return modelClass.find(value); + } + }; + }), /** A hook you can implement to convert the route's model into parameters @@ -26963,8 +31566,10 @@ Ember.Route = Ember.Object.extend({ }); ``` - The default `serialize` method inserts the model's `id` into the - route's dynamic segment (in this case, `:post_id`). + The default `serialize` method will insert the model's `id` into the + route's dynamic segment (in this case, `:post_id`) if the segment contains '_id'. + If the route has multiple dynamic segments or does not contain '_id', `serialize` + will return `Ember.getProperties(model, params)` This method is called when `transitionTo` is called with a context in order to populate the URL. @@ -26976,14 +31581,14 @@ Ember.Route = Ember.Object.extend({ @return {Object} the serialized parameters */ serialize: function(model, params) { - if (params.length !== 1) { return; } + if (params.length < 1) { return; } var name = params[0], object = {}; - if (/_id$/.test(name)) { - object[name] = get(model, 'id'); + if (/_id$/.test(name) && params.length === 1) { + object[name] = get(model, "id"); } else { - object[name] = model; + object = getProperties(model, params); } return object; @@ -27023,7 +31628,18 @@ Ember.Route = Ember.Object.extend({ be used if it is defined. If it is not defined, an `Ember.ObjectController` instance would be used. + Example + ```js + App.PostRoute = Ember.Route.extend({ + setupController: function(controller, model) { + controller.set('model', model); + } + }); + ``` + @method setupController + @param {Controller} controller instance + @param {Object} model */ setupController: function(controller, context) { if (controller && (context !== undefined)) { @@ -27032,7 +31648,10 @@ Ember.Route = Ember.Object.extend({ }, /** - Returns the controller for a particular route. + Returns the controller for a particular route or name. + + The controller instance must already have been created, either through entering the + associated route or using `generateController`. ```js App.PostRoute = Ember.Route.extend({ @@ -27044,17 +31663,24 @@ Ember.Route = Ember.Object.extend({ ``` @method controllerFor - @param {String} name the name of the route + @param {String} name the name of the route or controller @return {Ember.Controller} */ controllerFor: function(name, _skipAssert) { - var container = this.router.container, - controller = container.lookup('controller:' + name); + var container = this.container, + route = container.lookup('route:'+name), + controller; + + if (route && route.controllerName) { + name = route.controllerName; + } + + controller = container.lookup('controller:' + name); // NOTE: We're specifically checking that skipAssert is true, because according // to the old API the second parameter was model. We do not want people who // passed a model to skip the assertion. - Ember.assert("The controller "+name+" could not be found. Make sure the controller has been generated first. This will happen the first time the associated route is entered.", controller || _skipAssert === true); + return controller; }, @@ -27065,17 +31691,26 @@ Ember.Route = Ember.Object.extend({ If the optional model is passed then the controller type is determined automatically, e.g., an ArrayController for arrays. + Example + + ```js + App.PostRoute = Ember.Route.extend({ + setupController: function(controller, post) { + this._super(controller, post); + this.generateController('posts', post); + } + }); + ``` + @method generateController @param {String} name the name of the controller @param {Object} model the model to infer the type of the controller (optional) */ generateController: function(name, model) { - var container = this.router.container; + var container = this.container; model = model || this.modelFor(name); - Ember.assert("You are trying to look up a controller that you did not define, and for which Ember does not know the model.\n\nThis is not a controller for a route, so you must explicitly define the controller ("+this.router.namespace.toString() + "." + Ember.String.capitalize(Ember.String.camelize(name))+"Controller) or pass a model as the second parameter to `controllerFor`, so that Ember knows which type of controller to create for you.", model || this.container.lookup('route:' + name)); - return Ember.generateController(container, name, model); }, @@ -27085,6 +31720,22 @@ Ember.Route = Ember.Object.extend({ This is the object returned by the `model` hook of the route in question. + Example + + ```js + App.Router.map(function() { + this.resource('post', { path: '/post/:post_id' }, function() { + this.resource('comments'); + }); + }); + + App.CommentsRoute = Ember.Route.extend({ + afterModel: function() { + this.set('post', this.modelFor('post')); + } + }); + ``` + @method modelFor @param {String} name the name of the route @return {Object} the model object @@ -27116,6 +31767,22 @@ Ember.Route = Ember.Object.extend({ This method can be overridden to set up and render additional or alternative templates. + ```js + App.PostsRoute = Ember.Route.extend({ + renderTemplate: function(controller, model) { + var favController = this.controllerFor('favoritePost'); + + // Render the `favoritePost` template into + // the outlet `posts`, and display the `favoritePost` + // controller. + this.render('favoritePost', { + outlet: 'posts', + controller: favController + }); + } + }); + ``` + @method renderTemplate @param {Object} controller the route's controller @param {Object} model the route's model @@ -27176,7 +31843,7 @@ Ember.Route = Ember.Object.extend({ @param {Object} options the options */ render: function(name, options) { - Ember.assert("The name in the given arguments is undefined", arguments.length > 0 ? !Ember.isNone(arguments[0]) : true); + var namePassed = !!name; @@ -27185,14 +31852,21 @@ Ember.Route = Ember.Object.extend({ name = this.routeName; } + options = options || {}; name = name ? name.replace(/\//g, '.') : this.routeName; + var viewName = options.view || this.viewName || name; + var templateName = this.templateName || name; var container = this.container, - view = container.lookup('view:' + name), - template = container.lookup('template:' + name); + view = container.lookup('view:' + viewName), + template = view ? view.get('template') : null; + + if (!template) { + template = container.lookup('template:' + templateName); + } if (!view && !template) { - Ember.assert("Could not find \"" + name + "\" template or view.", !namePassed); + if (get(this.router, 'namespace.LOG_VIEW_LOOKUPS')) { Ember.Logger.info("Could not find \"" + name + "\" template or view. Nothing will be rendered", { fullName: 'template:' + name }); } @@ -27220,7 +31894,7 @@ Ember.Route = Ember.Object.extend({ ```js App.ApplicationRoute = App.Route.extend({ - events: { + actions: { showModal: function(evt) { this.render(evt.modalName, { outlet: 'modal', @@ -27253,6 +31927,11 @@ Ember.Route = Ember.Object.extend({ this.teardownViews(); }, + /** + @private + + @method teardownViews + */ teardownViews: function() { // Tear down the top level view if (this.teardownTopLevelView) { this.teardownTopLevelView(); } @@ -27303,7 +31982,6 @@ function normalizeOptions(route, name, template, options) { options.template = template; options.LOG_VIEW_LOOKUPS = get(route.router, 'namespace.LOG_VIEW_LOOKUPS'); - Ember.assert("An outlet ("+options.outlet+") was specified but was not found.", options.outlet === 'main' || options.into); var controller = options.controller, namedController; @@ -27312,7 +31990,7 @@ function normalizeOptions(route, name, template, options) { } else if (namedController = route.container.lookup('controller:' + name)) { controller = namedController; } else { - controller = route.routeName; + controller = route.controllerName || route.routeName; } if (typeof controller === 'string') { @@ -27390,24 +32068,45 @@ function generateOutletTeardown(parentView, outlet) { Ember.onLoad('Ember.Handlebars', function() { var handlebarsResolve = Ember.Handlebars.resolveParams, map = Ember.ArrayPolyfills.map, - get = Ember.get; + get = Ember.get, + handlebarsGet = Ember.Handlebars.get; function resolveParams(context, params, options) { - var resolved = handlebarsResolve(context, params, options); - return map.call(resolved, unwrap); + return map.call(resolvePaths(context, params, options), function(path, i) { + if (null === path) { + // Param was string/number, not a path, so just return raw string/number. + return params[i]; + } else { + return handlebarsGet(context, path, options); + } + }); + } - function unwrap(object, i) { - if (params[i] === 'controller') { return object; } + function resolvePaths(context, params, options) { + var resolved = handlebarsResolve(context, params, options), + types = options.types; + + return map.call(resolved, function(object, i) { + if (types[i] === 'ID') { + return unwrap(object, params[i]); + } else { + return null; + } + }); + + function unwrap(object, path) { + if (path === 'controller') { return path; } if (Ember.ControllerMixin.detect(object)) { - return unwrap(get(object, 'model')); + return unwrap(get(object, 'model'), path ? path + '.model' : 'model'); } else { - return object; + return path; } } } Ember.Router.resolveParams = resolveParams; + Ember.Router.resolvePaths = resolvePaths; }); })(); @@ -27424,6 +32123,7 @@ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; Ember.onLoad('Ember.Handlebars', function(Handlebars) { var resolveParams = Ember.Router.resolveParams, + resolvePaths = Ember.Router.resolvePaths, isSimpleClick = Ember.ViewUtils.isSimpleClick; function fullRouteName(router, name) { @@ -27434,19 +32134,12 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return name; } - function resolvedPaths(options) { - var types = options.options.types.slice(1), + function getResolvedPaths(options) { + + var types = options.options.types, data = options.options.data; - return resolveParams(options.context, options.params, { types: types, data: data }); - } - - function createPath(path) { - var fullPath = 'paramsContext'; - if (path !== '') { - fullPath += '.' + path; - } - return fullPath; + return resolvePaths(options.context, options.params, { types: types, data: data }); } /** @@ -27455,17 +32148,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { a supplied route by name. Instances of `LinkView` will most likely be created through - the `linkTo` Handlebars helper, but properties of this class + the `link-to` Handlebars helper, but properties of this class can be overridden to customize application-wide behavior. @class LinkView @namespace Ember @extends Ember.View - @see {Handlebars.helpers.linkTo} + @see {Handlebars.helpers.link-to} **/ var LinkView = Ember.LinkView = Ember.View.extend({ tagName: 'a', - namedRoute: null, currentWhen: null, /** @@ -27476,6 +32168,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { **/ title: null, + /** + Sets the `rel` attribute of the `LinkView`'s HTML element. + + @property rel + @default null + **/ + rel: null, + /** The CSS class to apply to `LinkView`'s element when its `active` property is `true`. @@ -27516,11 +32216,30 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @default false **/ replace: false, - attributeBindings: ['href', 'title'], + + /** + By default the `{{link-to}}` helper will bind to the `href` and + `title` attributes. It's discourage that you override these defaults, + however you can push onto the array if needed. + + @property attributeBindings + @type Array | String + @default ['href', 'title', 'rel'] + **/ + attributeBindings: ['href', 'title', 'rel'], + + /** + By default the `{{link-to}}` helper will bind to the `active`, `loading`, and + `disabled` classes. It is discouraged to override these directly. + + @property classNameBindings + @type Array + @default ['active', 'loading', 'disabled'] + **/ classNameBindings: ['active', 'loading', 'disabled'], /** - By default the `{{linkTo}}` helper responds to the `click` event. You + By default the `{{link-to}}` helper responds to the `click` event. You can override this globally by setting this property to your custom event name. @@ -27545,6 +32264,28 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @event click **/ + /** + An overridable method called when LinkView objects are instantiated. + + Example: + + ```javascript + App.MyLinkView = Ember.LinkView.extend({ + init: function() { + this._super(); + Ember.Logger.log('Event is ' + this.get('eventName')); + } + }); + ``` + + NOTE: If you do override `init` for a framework class like `Ember.View` or + `Ember.ArrayController`, be sure to call `this._super()` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. + + @method init + */ init: function() { this._super.apply(this, arguments); @@ -27552,27 +32293,57 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { var eventName = get(this, 'eventName'); this.on(eventName, this, this._invoke); - var params = this.parameters.params, - length = params.length, - context = this.parameters.context, - self = this, - path, paths = Ember.A([]), i; - - set(this, 'paramsContext', context); + var helperParameters = this.parameters, + templateContext = helperParameters.context, + paths = getResolvedPaths(helperParameters), + length = paths.length, + path, i; for(i=0; i < length; i++) { - paths.pushObject(createPath(params[i])); + path = paths[i]; + if (null === path) { + // A literal value was provided, not a path, so nothing to observe. + continue; + } + + var normalizedPath = + Ember.Handlebars.normalizePath(templateContext, path, helperParameters.options.data); + this.registerObserver(normalizedPath.root, normalizedPath.path, this, this._paramsChanged); } - var observer = function(object, path) { - this.notifyPropertyChange('routeArgs'); - }; - for(i=0; i < length; i++) { - this.registerObserver(this, paths[i], this, observer); + if (Ember.FEATURES.isEnabled("query-params")) { + var queryParams = get(this, '_potentialQueryParams') || []; + + for(i=0; i < queryParams.length; i++) { + this.registerObserver(this, queryParams[i], this, this._queryParamsChanged); + } } }, + /** + @private + + This method is invoked by observers installed during `init` that fire + whenever the params change + @method _paramsChanged + */ + _paramsChanged: function() { + this.notifyPropertyChange('resolvedParams'); + }, + + + /** + @private + + This method is invoked by observers installed during `init` that fire + whenever the query params change + */ + _queryParamsChanged: function (object, path) { + this.notifyPropertyChange('queryParams'); + }, + + /** @private @@ -27596,7 +32367,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { disabled: Ember.computed(function(key, value) { if (value !== undefined) { this.set('_isDisabled', value); } - return value ? this.get('disabledClass') : false; + return value ? get(this, 'disabledClass') : false; }), /** @@ -27610,33 +32381,44 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @property active **/ active: Ember.computed(function() { + if (get(this, 'loading')) { return false; } + var router = get(this, 'router'), - params = resolvedPaths(this.parameters), - currentWhen = this.currentWhen || get(this, 'namedRoute'), + routeArgs = get(this, 'routeArgs'), + contexts = routeArgs.slice(1), + resolvedParams = get(this, 'resolvedParams'), + currentWhen = this.currentWhen || resolvedParams[0], currentWithIndex = currentWhen + '.index', - isActive = router.isActive.apply(router, [currentWhen].concat(params)) || - router.isActive.apply(router, [currentWithIndex].concat(params)); + isActive = router.isActive.apply(router, [currentWhen].concat(contexts)) || + router.isActive.apply(router, [currentWithIndex].concat(contexts)); if (isActive) { return get(this, 'activeClass'); } - }).property('namedRoute', 'router.url'), + }).property('resolvedParams', 'routeArgs', 'router.url'), + /** + Accessed as a classname binding to apply the `LinkView`'s `loadingClass` + CSS `class` to the element when the link is loading. + + A `LinkView` is considered loading when it has at least one + parameter whose value is currently null or undefined. During + this time, clicking the link will perform no transition and + emit a warning that the link is still in a loading state. + + @property loading + **/ loading: Ember.computed(function() { if (!get(this, 'routeArgs')) { return get(this, 'loadingClass'); } }).property('routeArgs'), /** - Accessed as a classname binding to apply the `LinkView`'s `activeClass` - CSS `class` to the element when the link is active. + @private - A `LinkView` is considered active when its `currentWhen` property is `true` - or the application's current route is the route the `LinkView` would trigger - transitions into. + Returns the application's main router from the container. - @property active + @property router **/ - router: Ember.computed(function() { - return this.get('controller').container.lookup('router:main'); + return get(this, 'controller').container.lookup('router:main'); }), /** @@ -27656,44 +32438,102 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (get(this, '_isDisabled')) { return false; } if (get(this, 'loading')) { - Ember.Logger.warn("This linkTo is in an inactive loading state because at least one of its parameters' presently has a null/undefined value, or the provided route name is invalid."); + Ember.Logger.warn("This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid."); return false; } var router = get(this, 'router'), routeArgs = get(this, 'routeArgs'); - if (this.get('replace')) { + if (get(this, 'replace')) { router.replaceWith.apply(router, routeArgs); } else { router.transitionTo.apply(router, routeArgs); } }, + /** + @private + + Computed property that returns the resolved parameters. + + @property + @return {Array} + */ + resolvedParams: Ember.computed(function() { + var parameters = this.parameters, + options = parameters.options, + types = options.types, + data = options.data; + + return resolveParams(parameters.context, parameters.params, { types: types, data: data }); + }).property(), + + /** + @private + + Computed property that returns the current route name and + any dynamic segments. + + @property + @return {Array} An array with the route name and any dynamic segments + */ routeArgs: Ember.computed(function() { + var resolvedParams = get(this, 'resolvedParams').slice(0), + router = get(this, 'router'), + namedRoute = resolvedParams[0]; - var router = get(this, 'router'), - namedRoute = get(this, 'namedRoute'), routeName; + if (!namedRoute) { return; } - if (!namedRoute && this.namedRouteBinding) { - // The present value of namedRoute is falsy, but since it's a binding - // and could be valid later, don't treat as error. - return; + namedRoute = fullRouteName(router, namedRoute); + resolvedParams[0] = namedRoute; + + + for (var i = 1, len = resolvedParams.length; i < len; ++i) { + var param = resolvedParams[i]; + if (param === null || typeof param === 'undefined') { + // If contexts aren't present, consider the linkView unloaded. + return; + } } + + if (Ember.FEATURES.isEnabled("query-params")) { + var queryParams = get(this, 'queryParams'); + + if (queryParams || queryParams === false) { resolvedParams.push({queryParams: queryParams}); } + } + + return resolvedParams; + }).property('resolvedParams', 'queryParams', 'router.url'), + + + _potentialQueryParams: Ember.computed(function () { + var namedRoute = get(this, 'resolvedParams')[0]; + if (!namedRoute) { return null; } + var router = get(this, 'router'); + namedRoute = fullRouteName(router, namedRoute); - Ember.assert(fmt("The attempt to linkTo route '%@' failed. The router did not find '%@' in its possible routes: '%@'", [namedRoute, namedRoute, Ember.keys(router.router.recognizer.names).join("', '")]), router.hasRoute(namedRoute)); + return router.router.queryParamsForHandler(namedRoute); + }).property('resolvedParams'), - var resolvedContexts = resolvedPaths(this.parameters), paramsPresent = true; - for (var i = 0, l = resolvedContexts.length; i < l; ++i) { - var context = resolvedContexts[i]; + queryParams: Ember.computed(function () { + var self = this, + queryParams = null, + allowedQueryParams = get(this, '_potentialQueryParams'); - // If contexts aren't present, consider the linkView unloaded. - if (context === null || typeof context === 'undefined') { return; } - } + if (!allowedQueryParams) { return null; } + allowedQueryParams.forEach(function (param) { + var value = get(self, param); + if (typeof value !== 'undefined') { + queryParams = queryParams || {}; + queryParams[param] = value; + } + }); - return [ namedRoute ].concat(resolvedContexts); - }).property('namedRoute'), + + return queryParams; + }).property('_potentialQueryParams.[]'), /** Sets the element's `href` attribute to the url for @@ -27705,7 +32545,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @property href **/ href: Ember.computed(function() { - if (this.get('tagName') !== 'a') { return false; } + if (get(this, 'tagName') !== 'a') { return false; } var router = get(this, 'router'), routeArgs = get(this, 'routeArgs'); @@ -27714,7 +32554,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }).property('routeArgs'), /** - The default href value to use while a linkTo is loading. + The default href value to use while a link-to is loading. Only applies when tagName is 'a' @property loadingHref @@ -27727,16 +32567,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { LinkView.toString = function() { return "LinkView"; }; /** - The `{{linkTo}}` helper renders a link to the supplied + The `{{link-to}}` helper renders a link to the supplied `routeName` passing an optionally supplied model to the route as its `model` context of the route. The block - for `{{linkTo}}` becomes the innerHTML of the rendered + for `{{link-to}}` becomes the innerHTML of the rendered element: ```handlebars - {{#linkTo 'photoGallery'}} + {{#link-to 'photoGallery'}} Great Hamster Photos - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27746,14 +32586,14 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ``` ### Supplying a tagName - By default `{{linkTo}}` renders an `<a>` element. This can - be overridden for a single use of `{{linkTo}}` by supplying + By default `{{link-to}}` renders an `<a>` element. This can + be overridden for a single use of `{{link-to}}` by supplying a `tagName` option: ```handlebars - {{#linkTo 'photoGallery' tagName="li"}} + {{#link-to 'photoGallery' tagName="li"}} Great Hamster Photos - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27764,25 +32604,62 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { To override this option for your entire application, see "Overriding Application-wide Defaults". - + + ### Disabling the `link-to` heper + By default `{{link-to}}` is enabled. + any passed value to `disabled` helper property will disable the `link-to` helper. + + static use: the `disabled` option: + + ```handlebars + {{#link-to 'photoGallery' disabled=true}} + Great Hamster Photos + {{/link-to}} + ``` + + dynamic use: the `disabledWhen` option: + + ```handlebars + {{#link-to 'photoGallery' disabledWhen=controller.someProperty}} + Great Hamster Photos + {{/link-to}} + ``` + + any passed value to `disabled` will disable it except `undefined`. + to ensure that only `true` disable the `link-to` helper you can + override the global behaviour of `Ember.LinkView`. + + ```javascript + Ember.LinkView.reopen({ + disabled: Ember.computed(function(key, value) { + if (value !== undefined) { + this.set('_isDisabled', value === true); + } + return value === true ? get(this, 'disabledClass') : false; + }) + }); + ``` + + see "Overriding Application-wide Defaults" for more. + ### Handling `href` - `{{linkTo}}` will use your application's Router to + `{{link-to}}` will use your application's Router to fill the element's `href` property with a url that matches the path to the supplied `routeName` for your routers's configured `Location` scheme, which defaults to Ember.HashLocation. ### Handling current route - `{{linkTo}}` will apply a CSS class name of 'active' + `{{link-to}}` will apply a CSS class name of 'active' when the application's current route matches the supplied routeName. For example, if the application's current route is 'photoGallery.recent' the following - use of `{{linkTo}}`: + use of `{{link-to}}`: ```handlebars - {{#linkTo 'photoGallery.recent'}} + {{#link-to 'photoGallery.recent'}} Great Hamster Photos from the last week - {{/linkTo}} + {{/link-to}} ``` will result in @@ -27794,13 +32671,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ``` The CSS class name used for active classes can be customized - for a single use of `{{linkTo}}` by passing an `activeClass` + for a single use of `{{link-to}}` by passing an `activeClass` option: ```handlebars - {{#linkTo 'photoGallery.recent' activeClass="current-url"}} + {{#link-to 'photoGallery.recent' activeClass="current-url"}} Great Hamster Photos from the last week - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27820,13 +32697,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```javascript App.Router.map(function() { this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); - }) + }); ``` ```handlebars - {{#linkTo 'photoGallery' aPhoto}} + {{#link-to 'photoGallery' aPhoto}} {{aPhoto.title}} - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27852,9 +32729,9 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { This argument will become the model context of the linked route: ```handlebars - {{#linkTo 'photoGallery.comment' aPhoto comment}} + {{#link-to 'photoGallery.comment' aPhoto comment}} {{comment.body}} - {{/linkTo}} + {{/link-to}} ``` ```html @@ -27863,8 +32740,50 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { </a> ``` + ### Supplying an explicit dynamic segment value + If you don't have a model object available to pass to `{{link-to}}`, + an optional string or integer argument can be passed for routes whose + paths contain dynamic segments. This argument will become the value + of the dynamic segment: + + ```javascript + App.Router.map(function() { + this.resource("photoGallery", {path: "hamster-photos/:photo_id"}); + }); + ``` + + ```handlebars + {{#link-to 'photoGallery' aPhotoId}} + {{aPhoto.title}} + {{/link-to}} + ``` + + ```html + <a href="/hamster-photos/42"> + Tomster + </a> + ``` + + When transitioning into the linked route, the `model` hook will + be triggered with parameters including this passed identifier. + + ### Overriding attributes + You can override any given property of the Ember.LinkView + that is generated by the `{{link-to}}` helper by passing + key/value pairs, like so: + + ```handlebars + {{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames=['pic', 'sweet']}} + Uh-mazing! + {{/link-to}} + ``` + + See [Ember.LinkView](/api/classes/Ember.LinkView.html) for a + complete list of overrideable properties. Be sure to also + check out inherited properties of `LinkView`. + ### Overriding Application-wide Defaults - ``{{linkTo}}`` creates an instance of Ember.LinkView + ``{{link-to}}`` creates an instance of Ember.LinkView for rendering. To override options for your entire application, reopen Ember.LinkView and supply the desired values: @@ -27885,28 +32804,18 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { }); ``` - @method linkTo + @method link-to @for Ember.Handlebars.helpers @param {String} routeName @param {Object} [context]* + @param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkView @return {String} HTML string + @see {Ember.LinkView} */ - Ember.Handlebars.registerHelper('linkTo', function(name) { + Ember.Handlebars.registerHelper('link-to', function(name) { var options = [].slice.call(arguments, -1)[0], - params = [].slice.call(arguments, 1, -1); - - var hash = options.hash; - - if (options.types[0] === "ID") { - if (Ember.ENV.HELPER_PARAM_LOOKUPS) { - hash.namedRouteBinding = name; - } else { - Ember.deprecate("You provided a quoteless destination route parameter of " + name + " to the linkTo helper. Soon, this will perform a property lookup, rather than be treated as a string. To get rid of this warning, wrap " + name + " in quotes. To opt in to this new behavior, set ENV.HELPER_PARAM_LOOKUPS = true"); - hash.namedRoute = name; - } - } else { - hash.namedRoute = name; - } + params = [].slice.call(arguments, 0, -1), + hash = options.hash; hash.disabledBinding = hash.disabledWhen; @@ -27918,6 +32827,18 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return Ember.Handlebars.helpers.view.call(this, LinkView, options); }); + + /** + See [link-to](/api/classes/Ember.Handlebars.helpers.html#method_link-to) + + @method linkTo + @for Ember.Handlebars.helpers + @deprecated + @param {String} routeName + @param {Object} [context]* + @return {String} HTML string + */ + Ember.Handlebars.registerHelper('linkTo', Ember.Handlebars.helpers['link-to']); }); @@ -27967,8 +32888,8 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { You can create custom named outlets for more control. ``` handlebars - {{outlet favoritePost}} - {{outlet posts}} + {{outlet 'favoritePost'}} + {{outlet 'posts'}} ``` Then you can define what template is rendered into each outlet in your @@ -28002,6 +32923,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { @for Ember.Handlebars.helpers @param {String} property the property on the controller that holds the view for this outlet + @return {String} HTML string */ Handlebars.registerHelper('outlet', function(property, options) { var outletSource, outletContainerClass; @@ -28012,7 +32934,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { } outletSource = options.data.view; - while (!(outletSource.get('template.isTop'))) { + while (!outletSource.get('template.isTop')) { outletSource = outletSource.get('_parentView'); } @@ -28039,33 +32961,79 @@ var get = Ember.get, set = Ember.set; Ember.onLoad('Ember.Handlebars', function(Handlebars) { /** - Renders the named template in the current context with the same-named - controller. + Calling ``{{render}}`` from within a template will insert another + template that matches the provided name. The inserted template will + access its properties on its own controller (rather than the controller + of the parent template). - If a view class with the same name exists, the view class will be used. + If a view class with the same name exists, the view class also will be used. + + Note: A given controller may only be used *once* in your app in this manner. + A singleton instance of the controller will be created for you. - The optional second argument is a property path that will be bound - to the `model` property of the controller. + Example: + + ```javascript + App.NavigationController = Ember.Controller.extend({ + who: "world" + }); + ``` + + ```handelbars + <!-- navigation.hbs --> + Hello, {{who}}. + ``` + + ```handelbars + <!-- applications.hbs --> + <h1>My great app</h1> + {{render navigaton}} + ``` + + ```html + <h1>My great app</h1> + <div class='ember-view'> + Hello, world. + </div> + ``` + + Optionally you may provide a second argument: a property path + that will be bound to the `model` property of the controller. If a `model` property path is specified, then a new instance of the - controller will be created. + controller will be created and `{{render}}` can be used multiple times + with the same name. - If no `model` property path is provided, then the helper will use the - singleton instance of the controller. A given controller may only be used - one time in your app in this manner. + For example if you had this `author` template. - The default target for `{{action}}`s in the rendered template is the - controller. + ```handlebars +<div class="author"> + Written by {{firstName}} {{lastName}}. + Total Posts: {{postCount}} +</div> + ``` + + You could render it inside the `post` template using the `render` helper. + + ```handlebars +<div class="post"> + <h1>{{title}}</h1> + <div>{{body}}</div> + {{render "author" author}} +</div> + ``` @method render @for Ember.Handlebars.helpers @param {String} name @param {Object?} contextString @param {Hash} options + @return {String} HTML string */ Ember.Handlebars.registerHelper('render', function(name, contextString, options) { - Ember.assert("You must pass a template to render", arguments.length >= 2); - var container, router, controller, view, context, lookupOptions; + + var contextProvided = arguments.length === 3, + container, router, controller, view, context, lookupOptions; if (arguments.length === 2) { options = contextString; @@ -28081,7 +33049,6 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { container = options.data.keywords.controller.container; router = container.lookup('router:main'); - Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", context || !router || !router._lookupActiveView(name)); view = container.lookup('view:' + name) || container.lookup('view:default'); @@ -28090,13 +33057,13 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { // Look up the controller by name, if provided. if (controllerName) { controller = container.lookup('controller:' + controllerName, lookupOptions); - Ember.assert("The controller name you supplied '" + controllerName + "' did not resolve to a controller.", !!controller); + } else { controller = container.lookup('controller:' + name, lookupOptions) || Ember.generateController(container, name, context); } - if (controller && context) { + if (controller && contextProvided) { controller.set('model', context); } @@ -28165,6 +33132,10 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { return isSimpleClick(event); } + if (allowedKeys.indexOf("any") >= 0) { + return true; + } + var allowed = true; forEach.call(keys, function(key) { @@ -28202,7 +33173,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { if (target.send) { target.send.apply(target, args(options.parameters, actionName)); } else { - Ember.assert("The action '" + actionName + "' did not exist on " + target, typeof target[actionName] === 'function'); + target[actionName].apply(target, args(options.parameters)); } }); @@ -28230,11 +33201,9 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { Given the following Handlebars template on the page ```handlebars - <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName}}> - click me - </div> - </script> + <div {{action 'anActionName'}}> + click me + </div> ``` And application code @@ -28288,7 +33257,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ``` If you need the default handler to trigger you should either register your - own event handler, or use event methods on your view class. See `Ember.View` + own event handler, or use event methods on your view class. See [Ember.View](/api/classes/Ember.View.html) 'Responding to Browser Events' for more information. ### Specifying DOM event type @@ -28298,7 +33267,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName on="doubleClick"}}> + <div {{action 'anActionName' on="doubleClick"}}> click me </div> </script> @@ -28320,7 +33289,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName allowedKeys="alt"}}> + <div {{action 'anActionName' allowedKeys="alt"}}> click me </div> </script> @@ -28328,6 +33297,16 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { This way the `{{action}}` will fire when clicking with the alt key pressed down. + Alternatively, supply "any" to the `allowedKeys` option to accept any combination of modifier keys. + + ```handlebars + <script type="text/x-handlebars" data-template-name='a-template'> + <div {{action 'anActionName' allowedKeys="any"}}> + click me with any key pressed + </div> + </script> + ``` + ### Specifying a Target There are several possible target objects for `{{action}}` helpers: @@ -28342,7 +33321,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action anActionName target="MyApplication.someObject"}}> + <div {{action 'anActionName' target="MyApplication.someObject"}}> click me </div> </script> @@ -28356,7 +33335,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> - <div {{action aMethodNameThatIsMissing}}> + <div {{action 'aMethodNameThatIsMissing'}}> click me </div> </script> @@ -28387,7 +33366,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) { ```handlebars <script type="text/x-handlebars" data-template-name='a-template'> {{#each person in people}} - <div {{action edit person}}> + <div {{action 'edit' person}}> click me </div> {{/each}} @@ -28455,9 +33434,30 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { var get = Ember.get, set = Ember.set; /** + `{{control}}` works like render, except it uses a new controller instance for every call, instead of reusing the singleton controller. + The control helper is currently under development and is considered experimental. To enable it, set `ENV.EXPERIMENTAL_CONTROL_HELPER = true` before requiring Ember. + For example if you had this `author` template. + + ```handlebars +<div class="author"> + Written by {{firstName}} {{lastName}}. + Total Posts: {{postCount}} +</div> + ``` + + You could render it inside the `post` template using the `control` helper. + + ```handlebars +<div class="post"> + <h1>{{title}}</h1> + <div>{{body}}</div> + {{control "author" author}} +</div> + ``` + @method control @for Ember.Handlebars.helpers @param {String} path @@ -28497,8 +33497,7 @@ if (Ember.ENV.EXPERIMENTAL_CONTROL_HELPER) { childController = subContainer.lookup('controller:' + normalizedPath), childTemplate = subContainer.lookup('template:' + path); - Ember.assert("Could not find controller for path: " + normalizedPath, childController); - Ember.assert("Could not find view for path: " + normalizedPath, childView); + set(childController, 'target', controller); set(childController, 'model', model); @@ -28577,10 +33576,32 @@ Ember.ControllerMixin.reopen({ @method transitionTo */ transitionTo: function() { - Ember.deprecate("transitionTo is deprecated. Please use transitionToRoute."); + return this.transitionToRoute.apply(this, arguments); }, + /** + Alternative to `transitionToRoute`. Transition the application into another route. The route may + be either a single route or route path: + + ```javascript + aController.replaceRoute('blogPosts'); + aController.replaceRoute('blogPosts.recentEntries'); + ``` + + Optionally supply a model for the route in question. The model + will be serialized into the URL using the `serialize` hook of + the route: + + ```javascript + aController.replaceRoute('blogPost', aPost); + ``` + + @param {String} name the name of the route + @param {...Object} models the + @for Ember.ControllerMixin + @method replaceRoute + */ replaceRoute: function() { // target may be either another controller or a router var target = get(this, 'target'), @@ -28594,7 +33615,7 @@ Ember.ControllerMixin.reopen({ @method replaceWith */ replaceWith: function() { - Ember.deprecate("replaceWith is deprecated. Please use replaceRoute."); + return this.replaceRoute.apply(this, arguments); } }); @@ -28612,11 +33633,44 @@ Ember.ControllerMixin.reopen({ var get = Ember.get, set = Ember.set; Ember.View.reopen({ + + /** + Sets the private `_outlets` object on the view. + + @method init + */ init: function() { set(this, '_outlets', {}); this._super(); }, + /** + Manually fill any of a view's `{{outlet}}` areas with the + supplied view. + + Example + + ```javascript + var MyView = Ember.View.extend({ + template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') + }); + var myView = MyView.create(); + myView.appendTo('body'); + // The html for myView now looks like: + // <div id="ember228" class="ember-view">Child view: </div> + + myView.connectOutlet('main', Ember.View.extend({ + template: Ember.Handlebars.compile('<h1>Foo</h1> ') + })); + // The html for myView now looks like: + // <div id="ember228" class="ember-view">Child view: + // <div id="ember234" class="ember-view"><h1>Foo</h1> </div> + // </div> + ``` + @method connectOutlet + @param {String} outletName A unique name for the outlet + @param {Object} view An Ember.View + */ connectOutlet: function(outletName, view) { if (this._pendingDisconnections) { delete this._pendingDisconnections[outletName]; @@ -28639,6 +33693,18 @@ Ember.View.reopen({ } }, + /** + @private + + Determines if the view has already been created by checking if + the view has the same constructor, template, and context as the + view in the `_outlets` object. + + @method _hasEquivalentView + @param {String} outletName The name of the outlet we are checking + @param {Object} view An Ember.View + @return {Boolean} + */ _hasEquivalentView: function(outletName, view) { var existingView = get(this, '_outlets.'+outletName); return existingView && @@ -28647,6 +33713,36 @@ Ember.View.reopen({ existingView.get('context') === view.get('context'); }, + /** + Removes an outlet from the view. + + Example + + ```javascript + var MyView = Ember.View.extend({ + template: Ember.Handlebars.compile('Child view: {{outlet "main"}} ') + }); + var myView = MyView.create(); + myView.appendTo('body'); + // myView's html: + // <div id="ember228" class="ember-view">Child view: </div> + + myView.connectOutlet('main', Ember.View.extend({ + template: Ember.Handlebars.compile('<h1>Foo</h1> ') + })); + // myView's html: + // <div id="ember228" class="ember-view">Child view: + // <div id="ember234" class="ember-view"><h1>Foo</h1> </div> + // </div> + + myView.disconnectOutlet('main'); + // myView's html: + // <div id="ember228" class="ember-view">Child view: </div> + ``` + + @method disconnectOutlet + @param {String} outletName The name of the outlet to be removed + */ disconnectOutlet: function(outletName) { if (!this._pendingDisconnections) { this._pendingDisconnections = {}; @@ -28655,6 +33751,14 @@ Ember.View.reopen({ Ember.run.once(this, '_finishDisconnections'); }, + /** + @private + + Gets an outlet that is pending disconnection and then + nullifys the object on the `_outlet` object. + + @method _finishDisconnections + */ _finishDisconnections: function() { var outlets = get(this, '_outlets'); var pendingDisconnections = this._pendingDisconnections; @@ -28678,7 +33782,7 @@ Ember.View.reopen({ // Add a new named queue after the 'actions' queue (where RSVP promises // resolve), which is used in router transitions to prevent unnecessary -// loading state entry if all context promises resolve on the +// loading state entry if all context promises resolve on the // 'actions' queue first. var queues = Ember.run.queues, @@ -28725,16 +33829,51 @@ var get = Ember.get, set = Ember.set; @static */ Ember.Location = { + /** + Create an instance of a an implementation of the `location` API. Requires + an options object with an `implementation` property. + + Example + + ```javascript + var hashLocation = Ember.Location.create({implementation: 'hash'}); + var historyLocation = Ember.Location.create({implementation: 'history'}); + var noneLocation = Ember.Location.create({implementation: 'none'}); + ``` + + @method create + @param {Object} options + @return {Object} an instance of an implementation of the `location` API + */ create: function(options) { var implementation = options && options.implementation; - Ember.assert("Ember.Location.create: you must specify a 'implementation' option", !!implementation); + var implementationClass = this.implementations[implementation]; - Ember.assert("Ember.Location.create: " + implementation + " is not a valid implementation", !!implementationClass); + return implementationClass.create.apply(implementationClass, arguments); }, + /** + Registers a class that implements the `location` API with an implementation + name. This implementation name can then be specified by the location property on + the application's router class. + + Example + + ```javascript + Ember.Location.registerImplementation('history', Ember.HistoryLocation); + + App.Router.reopen({ + location: 'history' + }); + ``` + + @method registerImplementation + @param {String} name + @param {Object} implementation of the `location` API + */ registerImplementation: function(name, implementation) { this.implementations[name] = implementation; }, @@ -28767,23 +33906,71 @@ var get = Ember.get, set = Ember.set; Ember.NoneLocation = Ember.Object.extend({ path: '', + /** + @private + + Returns the current path. + + @method getURL + @return {String} path + */ getURL: function() { return get(this, 'path'); }, + /** + @private + + Set the path and remembers what was set. Using this method + to change the path will not invoke the `updateURL` callback. + + @method setURL + @param path {String} + */ setURL: function(path) { set(this, 'path', path); }, + /** + @private + + Register a callback to be invoked when the path changes. These + callbacks will execute when the user presses the back or forward + button, but not after `setURL` is invoked. + + @method onUpdateURL + @param callback {Function} + */ onUpdateURL: function(callback) { this.updateCallback = callback; }, + /** + @private + + Sets the path and calls the `updateURL` callback. + + @method handleURL + @param callback {Function} + */ handleURL: function(url) { set(this, 'path', url); this.updateCallback(url); }, + /** + @private + + Given a URL, formats it to be placed into the page as part + of an element's `href` attribute. + + This is used, for example, when using the {{action}} helper + to generate a URL based on an event. + + @method formatURL + @param url {String} + @return {String} url + */ formatURL: function(url) { // The return value is not overly meaningful, but we do not want to throw // errors when test code renders templates containing {{action href=true}} @@ -28847,6 +34034,19 @@ Ember.HashLocation = Ember.Object.extend({ set(this, 'lastSetURL', path); }, + /** + @private + + Uses location.replace to update the url without a page reload + or history modification. + + @method replaceURL + @param path {String} + */ + replaceURL: function(path) { + get(this, 'location').replace('#' + path); + }, + /** @private @@ -28889,6 +34089,13 @@ Ember.HashLocation = Ember.Object.extend({ return '#'+url; }, + /** + @private + + Cleans up the HashLocation event listener. + + @method willDestroy + */ willDestroy: function() { var guid = Ember.guidFor(this); @@ -28953,6 +34160,7 @@ Ember.HistoryLocation = Ember.Object.extend({ Returns the current `location.pathname` without rootURL @method getURL + @return url {String} */ getURL: function() { var rootURL = get(this, 'rootURL'), @@ -29007,6 +34215,7 @@ Ember.HistoryLocation = Ember.Object.extend({ from a private _historyState variable @method getState + @return state {Object} */ getState: function() { return supportsHistoryState ? get(this, 'history').state : this._historyState; @@ -29086,6 +34295,7 @@ Ember.HistoryLocation = Ember.Object.extend({ @method formatURL @param url {String} + @return formatted url {String} */ formatURL: function(url) { var rootURL = get(this, 'rootURL'); @@ -29097,6 +34307,13 @@ Ember.HistoryLocation = Ember.Object.extend({ return rootURL + url; }, + /** + @private + + Cleans up the HistoryLocation event listener. + + @method willDestroy + */ willDestroy: function() { var guid = Ember.guidFor(this); @@ -29122,7 +34339,6 @@ Ember Routing @module ember @submodule ember-routing -@requires ember-states @requires ember-views */ @@ -29253,11 +34469,11 @@ var get = Ember.get, container lookups before consulting the container for registered items: - * templates are looked up on `Ember.TEMPLATES` - * other names are looked up on the application after converting - the name. For example, `controller:post` looks up - `App.PostController` by default. - * there are some nuances (see examples below) +* templates are looked up on `Ember.TEMPLATES` +* other names are looked up on the application after converting + the name. For example, `controller:post` looks up + `App.PostController` by default. +* there are some nuances (see examples below) ### How Resolving Works @@ -29279,7 +34495,7 @@ var get = Ember.get, ```javascript App = Ember.Application.create({ - resolver: Ember.DefaultResolver.extend({ + Resolver: Ember.DefaultResolver.extend({ resolveTemplate: function(parsedName) { var resolvedTemplate = this._super(parsedName); if (resolvedTemplate) { return resolvedTemplate; } @@ -29326,6 +34542,31 @@ Ember.DefaultResolver = Ember.Object.extend({ @property namespace */ namespace: null, + + normalize: function(fullName) { + var split = fullName.split(':', 2), + type = split[0], + name = split[1]; + + + if (type !== 'template') { + var result = name; + + if (result.indexOf('.') > -1) { + result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } + + if (name.indexOf('_') > -1) { + result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); + } + + return type + ':' + result; + } else { + return fullName; + } + }, + + /** This method is called via the container's resolver method. It parses the provided `fullName` and then looks up and @@ -29355,6 +34596,7 @@ Ember.DefaultResolver = Ember.Object.extend({ broken out. @protected + @param {String} fullName the lookup string @method parseName */ parseName: function(fullName) { @@ -29370,7 +34612,6 @@ Ember.DefaultResolver = Ember.Object.extend({ var namespaceName = capitalize(parts.slice(0, -1).join('.')); root = Ember.Namespace.byName(namespaceName); - Ember.assert('You are looking for a ' + name + ' ' + type + ' in the ' + namespaceName + ' namespace, but the namespace could not be found', root); } return { @@ -29386,6 +34627,8 @@ Ember.DefaultResolver = Ember.Object.extend({ Look up the template in Ember.TEMPLATES @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveTemplate */ resolveTemplate: function(parsedName) { @@ -29405,6 +34648,8 @@ Ember.DefaultResolver = Ember.Object.extend({ the conventions expected by `Ember.Router` @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method useRouterNaming */ useRouterNaming: function(parsedName) { @@ -29414,7 +34659,11 @@ Ember.DefaultResolver = Ember.Object.extend({ } }, /** + Lookup the controller using `resolveOther` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveController */ resolveController: function(parsedName) { @@ -29422,7 +34671,11 @@ Ember.DefaultResolver = Ember.Object.extend({ return this.resolveOther(parsedName); }, /** + Lookup the route using `resolveOther` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveRoute */ resolveRoute: function(parsedName) { @@ -29430,7 +34683,11 @@ Ember.DefaultResolver = Ember.Object.extend({ return this.resolveOther(parsedName); }, /** + Lookup the view using `resolveOther` + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveView */ resolveView: function(parsedName) { @@ -29439,7 +34696,11 @@ Ember.DefaultResolver = Ember.Object.extend({ }, /** + Lookup the model on the Application namespace + @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveModel */ resolveModel: function(parsedName) { @@ -29453,6 +34714,8 @@ Ember.DefaultResolver = Ember.Object.extend({ namespace (usually on the Application) @protected + @param {Object} parsedName a parseName object with the parsed + fullName lookup string @method resolveOther */ resolveOther: function(parsedName) { @@ -29461,8 +34724,18 @@ Ember.DefaultResolver = Ember.Object.extend({ if (factory) { return factory; } }, - lookupDescription: function(name) { - var parsedName = this.parseName(name); + /** + Returns a human-readable description for a fullName. Used by the + Application namespace in assertions to describe the + precise name of the class that Ember is looking for, rather than + container keys. + + @protected + @param {String} fullName the lookup string + @method lookupDescription + */ + lookupDescription: function(fullName) { + var parsedName = this.parseName(fullName); if (parsedName.type === 'template') { return "template at " + parsedName.fullNameWithoutType.replace(/\./g, '/'); @@ -29472,6 +34745,10 @@ Ember.DefaultResolver = Ember.Object.extend({ if (parsedName.type !== 'model') { description += classify(parsedName.type); } return description; + }, + + makeToString: function(factory, fullName) { + return factory.toString(); } }); @@ -29495,7 +34772,6 @@ DeprecatedContainer.deprecate = function(method) { return function() { var container = this._container; - Ember.deprecate('Using the defaultContainer is no longer supported. [defaultContainer#' + method + '] see: http://git.io/EKPpnA', false); return container[method].apply(container, arguments); }; }; @@ -29618,9 +34894,9 @@ DeprecatedContainer.prototype = { In addition to creating your application's router, `Ember.Application` is also responsible for telling the router when to start routing. Transitions - between routes can be logged with the LOG_TRANSITIONS flag, and more + between routes can be logged with the `LOG_TRANSITIONS` flag, and more detailed intra-transition logging can be logged with - the LOG_TRANSITIONS_INTERNAL flag: + the `LOG_TRANSITIONS_INTERNAL` flag: ```javascript window.App = Ember.Application.create({ @@ -29721,13 +34997,18 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin this.scheduleInitialize(); - if (Ember.LOG_VERSION) { + Ember.libraries.registerCoreLibrary('Handlebars', Ember.Handlebars.VERSION); + Ember.libraries.registerCoreLibrary('jQuery', Ember.$().jquery); + + if ( Ember.LOG_VERSION ) { Ember.LOG_VERSION = false; // we only need to see this once per Application#init - Ember.debug('-------------------------------'); - Ember.debug('Ember.VERSION : ' + Ember.VERSION); - Ember.debug('Handlebars.VERSION : ' + Ember.Handlebars.VERSION); - Ember.debug('jQuery.VERSION : ' + Ember.$().jquery); - Ember.debug('-------------------------------'); + var maxNameLength = Math.max.apply(this, Ember.A(Ember.libraries).mapBy("name.length")); + + Ember.libraries.each(function(name, version) { + var spaces = new Array(maxNameLength - name.length + 1).join(" "); + + }); + } }, @@ -29830,15 +35111,21 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin @method deferReadiness */ deferReadiness: function() { - Ember.assert("You cannot defer readiness since the `ready()` hook has already been called.", this._readinessDeferrals > 0); + + this._readinessDeferrals++; }, /** + Call `advanceReadiness` after any asynchronous setup logic has completed. + Each call to `deferReadiness` must be matched by a call to `advanceReadiness` + or the application will never become ready and routing will not begin. + @method advanceReadiness @see {Ember.Application#deferReadiness} */ advanceReadiness: function() { + this._readinessDeferrals--; if (this._readinessDeferrals === 0) { @@ -29854,19 +35141,20 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin ```javascript App = Ember.Application.create(); - App.Person = Ember.Object.extend({}); - App.Orange = Ember.Object.extend({}); - App.Email = Ember.Object.extend({}); + App.Person = Ember.Object.extend({}); + App.Orange = Ember.Object.extend({}); + App.Email = Ember.Object.extend({}); + App.Session = Ember.Object.create({}); App.register('model:user', App.Person, {singleton: false }); App.register('fruit:favorite', App.Orange); App.register('communication:main', App.Email, {singleton: false}); + App.register('session', App.Session, {instantiate: false}); ``` @method register - @param type {String} - @param name {String} - @param factory {String} + @param fullName {String} type:name (e.g., 'model:user') + @param factory {Function} (e.g., App.Person) @param options {String} (optional) **/ register: function() { @@ -29906,7 +35194,7 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin @method initialize **/ initialize: function() { - Ember.deprecate('Calling initialize manually is not supported. Please see Ember.Application#advanceReadiness and Ember.Application#deferReadiness'); + }, /** @private @@ -30042,7 +35330,7 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin graph.topsort(function (vertex) { var initializer = vertex.value; - Ember.assert("No application initializer named '"+vertex.name+"'", initializer); + initializer(container, namespace); }); }, @@ -30086,8 +35374,8 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin /** @private - If the application has a router, use it to route to the current URL, and trigger a new call to `route` whenever the URL changes. + If the application has a router, use it to route to the current URL, and @method startRouting @property router {Ember.Router} @@ -30114,12 +35402,21 @@ var Application = Ember.Application = Ember.Namespace.extend(Ember.DeferredMixin ready: Ember.K, /** + @deprecated Use 'Resolver' instead Set this to provide an alternate class to `Ember.DefaultResolver` + @property resolver */ resolver: null, + /** + Set this to provide an alternate class to `Ember.DefaultResolver` + + @property resolver + */ + Resolver: null, + willDestroy: function() { Ember.BOOTED = false; @@ -30137,9 +35434,8 @@ Ember.Application.reopenClass({ initializer: function(initializer) { var initializers = get(this, 'initializers'); - Ember.assert("The initializer '" + initializer.name + "' has already been registered", !initializers.findProperty('name', initializers.name)); - Ember.assert("An injection cannot be registered with both a before and an after", !(initializer.before && initializer.after)); - Ember.assert("An injection cannot be registered without an injection function", Ember.canInvoke(initializer, 'initialize')); + + initializers.push(initializer); }, @@ -30176,9 +35472,12 @@ Ember.Application.reopenClass({ Ember.Container.defaultContainer = new DeprecatedContainer(container); container.set = Ember.set; - container.normalize = normalize; - container.resolver = resolverFor(namespace); - container.describe = container.resolver.describe; + container.resolver = resolverFor(namespace); + container.normalize = container.resolver.normalize; + container.describe = container.resolver.describe; + container.makeToString = container.resolver.makeToString; + + container.optionsForType('component', { singleton: false }); container.optionsForType('view', { singleton: false }); container.optionsForType('template', { instantiate: false }); container.register('application:main', namespace, { instantiate: false }); @@ -30218,8 +35517,12 @@ Ember.Application.reopenClass({ @return {*} the resolved value for a given lookup */ function resolverFor(namespace) { - var resolverClass = namespace.get('resolver') || Ember.DefaultResolver; - var resolver = resolverClass.create({ + if (namespace.get('resolver')) { + + } + + var ResolverClass = namespace.get('resolver') || namespace.get('Resolver') || Ember.DefaultResolver; + var resolver = ResolverClass.create({ namespace: namespace }); @@ -30231,33 +35534,22 @@ function resolverFor(namespace) { return resolver.lookupDescription(fullName); }; + resolve.makeToString = function(factory, fullName) { + return resolver.makeToString(factory, fullName); + }; + + resolve.normalize = function(fullName) { + if (resolver.normalize) { + return resolver.normalize(fullName); + } else { + + return fullName; + } + }; + return resolve; } -function normalize(fullName) { - var split = fullName.split(':', 2), - type = split[0], - name = split[1]; - - Ember.assert("Tried to normalize a container name without a colon (:) in it. You probably tried to lookup a name that did not contain a type, a colon, and a name. A proper lookup name would be `view:post`.", split.length === 2); - - if (type !== 'template') { - var result = name; - - if (result.indexOf('.') > -1) { - result = result.replace(/\.(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); - } - - if (name.indexOf('_') > -1) { - result = result.replace(/_(.)/g, function(m) { return m.charAt(1).toUpperCase(); }); - } - - return type + ':' + result; - } else { - return fullName; - } -} - Ember.runLoadHooks('Ember.Application', Ember.Application); })(); @@ -30277,44 +35569,27 @@ Ember.runLoadHooks('Ember.Application', Ember.Application); */ var get = Ember.get, set = Ember.set; -var ControllersProxy = Ember.Object.extend({ - controller: null, - unknownProperty: function(controllerName) { - var controller = get(this, 'controller'), - needs = get(controller, 'needs'), - container = controller.get('container'), - dependency; +function verifyNeedsDependencies(controller, container, needs) { + var dependency, i, l; - for (var i=0, l=needs.length; i<l; i++) { - dependency = needs[i]; - if (dependency === controllerName) { - return container.lookup('controller:' + controllerName); - } - } - } -}); - -function verifyDependencies(controller) { - var needs = get(controller, 'needs'), - container = get(controller, 'container'), - dependency, satisfied = true; - - for (var i=0, l=needs.length; i<l; i++) { + for (i=0, l=needs.length; i<l; i++) { dependency = needs[i]; if (dependency.indexOf(':') === -1) { dependency = "controller:" + dependency; } + // Structure assert to still do verification but not string concat in production if (!container.has(dependency)) { - satisfied = false; - Ember.assert(controller + " needs " + dependency + " but it does not exist", false); + } } - - return satisfied; } +/** + @class ControllerMixin + @namespace Ember +*/ Ember.ControllerMixin.reopen({ concatenatedProperties: ['needs'], @@ -30347,22 +35622,64 @@ Ember.ControllerMixin.reopen({ needs: [], init: function() { - this._super.apply(this, arguments); + var needs = get(this, 'needs'), + length = get(needs, 'length'); - // Structure asserts to still do verification but not string concat in production - if (!verifyDependencies(this)) { - Ember.assert("Missing dependencies", false); + if (length > 0) { + verifyNeedsDependencies(this, this.container, needs); + + // if needs then initialize controllers proxy + get(this, 'controllers'); } + + this._super.apply(this, arguments); }, controllerFor: function(controllerName) { - Ember.deprecate("Controller#controllerFor is deprecated, please use Controller#needs instead"); + return Ember.controllerFor(get(this, 'container'), controllerName); }, + /** + Stores the instances of other controllers available from within + this controller. Any controller listed by name in the `needs` + property will be accessible by name through this property. + + ```javascript + App.CommentsController = Ember.ArrayController.extend({ + needs: ['post'], + postTitle: function(){ + var currentPost = this.get('controllers.post'); // instance of App.PostController + return currentPost.get('title'); + }.property('controllers.post.title') + }); + ``` + + @see {Ember.ControllerMixin#needs} + @property {Object} controllers + @default null + */ controllers: Ember.computed(function() { - return ControllersProxy.create({ controller: this }); - }) + var controller = this; + + return { + needs: get(controller, 'needs'), + container: get(controller, 'container'), + unknownProperty: function(controllerName) { + var needs = this.needs, + dependency, i, l; + for (i=0, l=needs.length; i<l; i++) { + dependency = needs[i]; + if (dependency === controllerName) { + return this.container.lookup('controller:' + controllerName); + } + } + + var errorMessage = Ember.inspect(controller) + '#needs does not include `' + controllerName + '`. To access the ' + controllerName + ' controller from ' + Ember.inspect(controller) + ', ' + Ember.inspect(controller) + ' should have a `needs` property that is an array of the controllers it has access to.'; + throw new ReferenceError(errorMessage); + } + }; + }).readOnly() }); })(); @@ -30381,1892 +35698,454 @@ Ember Application @module ember @submodule ember-application -@requires ember-views, ember-states, ember-routing +@requires ember-views, ember-routing */ })(); (function() { -var get = Ember.get, set = Ember.set; - /** @module ember -@submodule ember-states +@submodule ember-extension-support */ - /** - @class State + The `DataAdapter` helps a data persistence library + interface with tools that debug Ember such + as the Chrome Ember Extension. + + This class will be extended by a persistence library + which will override some of the methods with + library-specific code. + + The methods likely to be overriden are + `getFilters`, `detect`, `columnsForType`, + `getRecords`, `getRecordColumnValues`, + `getRecordKeywords`, `getRecordFilterValues`, + `getRecordColor`, `observeRecord` + + The adapter will need to be registered + in the application's container as `dataAdapter:main` + + Example: + ```javascript + Application.initializer({ + name: "dataAdapter", + + initialize: function(container, application) { + application.register('dataAdapter:main', DS.DataAdapter); + } + }); + ``` + + @class DataAdapter @namespace Ember @extends Ember.Object - @uses Ember.Evented */ -Ember.State = Ember.Object.extend(Ember.Evented, -/** @scope Ember.State.prototype */{ - /** - A reference to the parent state. - - @property parentState - @type Ember.State - */ - parentState: null, - start: null, - - /** - The name of this state. - - @property name - @type String - */ - name: null, - - /** - The full path to this state. - - @property path - @type String - */ - path: Ember.computed(function() { - var parentPath = get(this, 'parentState.path'), - path = get(this, 'name'); - - if (parentPath) { - path = parentPath + '.' + path; - } - - return path; - }), - - /** - @private - - Override the default event firing from `Ember.Evented` to - also call methods with the given name. - - @method trigger - @param name - */ - trigger: function(name) { - if (this[name]) { - this[name].apply(this, [].slice.call(arguments, 1)); - } - this._super.apply(this, arguments); - }, - - init: function() { - var states = get(this, 'states'); - set(this, 'childStates', Ember.A()); - set(this, 'eventTransitions', get(this, 'eventTransitions') || {}); - - var name, value, transitionTarget; - - // As a convenience, loop over the properties - // of this state and look for any that are other - // Ember.State instances or classes, and move them - // to the `states` hash. This avoids having to - // create an explicit separate hash. - - if (!states) { - states = {}; - - for (name in this) { - if (name === "constructor") { continue; } - - if (value = this[name]) { - if (transitionTarget = value.transitionTarget) { - this.eventTransitions[name] = transitionTarget; - } - - this.setupChild(states, name, value); - } - } - - set(this, 'states', states); - } else { - for (name in states) { - this.setupChild(states, name, states[name]); - } - } - - // pathsCaches is a nested hash of the form: - // pathsCaches[stateManagerTypeGuid][path] == transitions_hash - set(this, 'pathsCaches', {}); - }, - - setPathsCache: function(stateManager, path, transitions) { - var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), - pathsCaches = get(this, 'pathsCaches'), - pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; - - pathsCacheForManager[path] = transitions; - pathsCaches[stateManagerTypeGuid] = pathsCacheForManager; - }, - - getPathsCache: function(stateManager, path) { - var stateManagerTypeGuid = Ember.guidFor(stateManager.constructor), - pathsCaches = get(this, 'pathsCaches'), - pathsCacheForManager = pathsCaches[stateManagerTypeGuid] || {}; - - return pathsCacheForManager[path]; - }, - - setupChild: function(states, name, value) { - if (!value) { return false; } - var instance; - - if (value instanceof Ember.State) { - set(value, 'name', name); - instance = value; - instance.container = this.container; - } else if (Ember.State.detect(value)) { - instance = value.create({ - name: name, - container: this.container - }); - } - - if (instance instanceof Ember.State) { - set(instance, 'parentState', this); - get(this, 'childStates').pushObject(instance); - states[name] = instance; - return instance; - } - }, - - lookupEventTransition: function(name) { - var path, state = this; - - while(state && !path) { - path = state.eventTransitions[name]; - state = state.get('parentState'); - } - - return path; - }, - - /** - A Boolean value indicating whether the state is a leaf state - in the state hierarchy. This is `false` if the state has child - states; otherwise it is true. - - @property isLeaf - @type Boolean - */ - isLeaf: Ember.computed(function() { - return !get(this, 'childStates').length; - }), - - /** - A boolean value indicating whether the state takes a context. - By default we assume all states take contexts. - - @property hasContext - @default true - */ - hasContext: true, - - /** - This is the default transition event. - - @event setup - @param {Ember.StateManager} manager - @param context - @see Ember.StateManager#transitionEvent - */ - setup: Ember.K, - - /** - This event fires when the state is entered. - - @event enter - @param {Ember.StateManager} manager - */ - enter: Ember.K, - - /** - This event fires when the state is exited. - - @event exit - @param {Ember.StateManager} manager - */ - exit: Ember.K -}); - -Ember.State.reopenClass({ - - /** - Creates an action function for transitioning to the named state while - preserving context. - - The following example StateManagers are equivalent: - - ```javascript - aManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: Ember.State.transitionTo('stateTwo') - }), - stateTwo: Ember.State.create({}) - }) - - bManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: function(manager, context) { - manager.transitionTo('stateTwo', context) - } - }), - stateTwo: Ember.State.create({}) - }) - ``` - - @method transitionTo - @static - @param {String} target - */ - - transitionTo: function(target) { - - var transitionFunction = function(stateManager, contextOrEvent) { - var contexts = [], - Event = Ember.$ && Ember.$.Event; - - if (contextOrEvent && (Event && contextOrEvent instanceof Event)) { - if (contextOrEvent.hasOwnProperty('contexts')) { - contexts = contextOrEvent.contexts.slice(); - } - } - else { - contexts = [].slice.call(arguments, 1); - } - - contexts.unshift(target); - stateManager.transitionTo.apply(stateManager, contexts); - }; - - transitionFunction.transitionTarget = target; - - return transitionFunction; - } - -}); - -})(); - - - -(function() { -/** -@module ember -@submodule ember-states -*/ - -var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; -var arrayForEach = Ember.ArrayPolyfills.forEach; -/** - A Transition takes the enter, exit and resolve states and normalizes - them: - - * takes any passed in contexts into consideration - * adds in `initialState`s - - @class Transition - @private -*/ -var Transition = function(raw) { - this.enterStates = raw.enterStates.slice(); - this.exitStates = raw.exitStates.slice(); - this.resolveState = raw.resolveState; - - this.finalState = raw.enterStates[raw.enterStates.length - 1] || raw.resolveState; -}; - -Transition.prototype = { - /** - Normalize the passed in enter, exit and resolve states. - - This process also adds `finalState` and `contexts` to the Transition object. - - @method normalize - @param {Ember.StateManager} manager the state manager running the transition - @param {Array} contexts a list of contexts passed into `transitionTo` - */ - normalize: function(manager, contexts) { - this.matchContextsToStates(contexts); - this.addInitialStates(); - this.removeUnchangedContexts(manager); - return this; - }, - - /** - Match each of the contexts passed to `transitionTo` to a state. - This process may also require adding additional enter and exit - states if there are more contexts than enter states. - - @method matchContextsToStates - @param {Array} contexts a list of contexts passed into `transitionTo` - */ - matchContextsToStates: function(contexts) { - var stateIdx = this.enterStates.length - 1, - matchedContexts = [], - state, - context; - - // Next, we will match the passed in contexts to the states they - // represent. - // - // First, assign a context to each enter state in reverse order. If - // any contexts are left, add a parent state to the list of states - // to enter and exit, and assign a context to the parent state. - // - // If there are still contexts left when the state manager is - // reached, raise an exception. - // - // This allows the following: - // - // |- root - // | |- post - // | | |- comments - // | |- about (* current state) - // - // For `transitionTo('post.comments', post, post.get('comments')`, - // the first context (`post`) will be assigned to `root.post`, and - // the second context (`post.get('comments')`) will be assigned - // to `root.post.comments`. - // - // For the following: - // - // |- root - // | |- post - // | | |- index (* current state) - // | | |- comments - // - // For `transitionTo('post.comments', otherPost, otherPost.get('comments')`, - // the `<root.post>` state will be added to the list of enter and exit - // states because its context has changed. - - while (contexts.length > 0) { - if (stateIdx >= 0) { - state = this.enterStates[stateIdx--]; - } else { - if (this.enterStates.length) { - state = get(this.enterStates[0], 'parentState'); - if (!state) { throw "Cannot match all contexts to states"; } - } else { - // If re-entering the current state with a context, the resolve - // state will be the current state. - state = this.resolveState; - } - - this.enterStates.unshift(state); - this.exitStates.unshift(state); - } - - // in routers, only states with dynamic segments have a context - if (get(state, 'hasContext')) { - context = contexts.pop(); - } else { - context = null; - } - - matchedContexts.unshift(context); - } - - this.contexts = matchedContexts; - }, - - /** - Add any `initialState`s to the list of enter states. - - @method addInitialStates - */ - addInitialStates: function() { - var finalState = this.finalState, initialState; - - while(true) { - initialState = get(finalState, 'initialState') || 'start'; - finalState = get(finalState, 'states.' + initialState); - - if (!finalState) { break; } - - this.finalState = finalState; - this.enterStates.push(finalState); - this.contexts.push(undefined); - } - }, - - /** - Remove any states that were added because the number of contexts - exceeded the number of explicit enter states, but the context has - not changed since the last time the state was entered. - - @method removeUnchangedContexts - @param {Ember.StateManager} manager passed in to look up the last - context for a states - */ - removeUnchangedContexts: function(manager) { - // Start from the beginning of the enter states. If the state was added - // to the list during the context matching phase, make sure the context - // has actually changed since the last time the state was entered. - while (this.enterStates.length > 0) { - if (this.enterStates[0] !== this.exitStates[0]) { break; } - - if (this.enterStates.length === this.contexts.length) { - if (manager.getStateMeta(this.enterStates[0], 'context') !== this.contexts[0]) { break; } - this.contexts.shift(); - } - - this.resolveState = this.enterStates.shift(); - this.exitStates.shift(); - } - } -}; - -var sendRecursively = function(event, currentState, isUnhandledPass) { - var log = this.enableLogging, - eventName = isUnhandledPass ? 'unhandledEvent' : event, - action = currentState[eventName], - contexts, sendRecursiveArguments, actionArguments; - - contexts = [].slice.call(arguments, 3); - - // Test to see if the action is a method that - // can be invoked. Don't blindly check just for - // existence, because it is possible the state - // manager has a child state of the given name, - // and we should still raise an exception in that - // case. - if (typeof action === 'function') { - if (log) { - if (isUnhandledPass) { - Ember.Logger.log(fmt("STATEMANAGER: Unhandled event '%@' being sent to state %@.", [event, get(currentState, 'path')])); - } else { - Ember.Logger.log(fmt("STATEMANAGER: Sending event '%@' to state %@.", [event, get(currentState, 'path')])); - } - } - - actionArguments = contexts; - if (isUnhandledPass) { - actionArguments.unshift(event); - } - actionArguments.unshift(this); - - return action.apply(currentState, actionArguments); - } else { - var parentState = get(currentState, 'parentState'); - if (parentState) { - - sendRecursiveArguments = contexts; - sendRecursiveArguments.unshift(event, parentState, isUnhandledPass); - - return sendRecursively.apply(this, sendRecursiveArguments); - } else if (!isUnhandledPass) { - return sendEvent.call(this, event, contexts, true); - } - } -}; - -var sendEvent = function(eventName, sendRecursiveArguments, isUnhandledPass) { - sendRecursiveArguments.unshift(eventName, get(this, 'currentState'), isUnhandledPass); - return sendRecursively.apply(this, sendRecursiveArguments); -}; - -/** - StateManager is part of Ember's implementation of a finite state machine. A - StateManager instance manages a number of properties that are instances of - `Ember.State`, - tracks the current active state, and triggers callbacks when states have changed. - - ## Defining States - - The states of StateManager can be declared in one of two ways. First, you can - define a `states` property that contains all the states: - - ```javascript - managerA = Ember.StateManager.create({ - states: { - stateOne: Ember.State.create(), - stateTwo: Ember.State.create() - } - }) - - managerA.get('states') - // { - // stateOne: Ember.State.create(), - // stateTwo: Ember.State.create() - // } - ``` - - You can also add instances of `Ember.State` (or an `Ember.State` subclass) - directly as properties of a StateManager. These states will be collected into - the `states` property for you. - - ```javascript - managerA = Ember.StateManager.create({ - stateOne: Ember.State.create(), - stateTwo: Ember.State.create() - }) - - managerA.get('states') - // { - // stateOne: Ember.State.create(), - // stateTwo: Ember.State.create() - // } - ``` - - ## The Initial State - - When created a StateManager instance will immediately enter into the state - defined as its `start` property or the state referenced by name in its - `initialState` property: - - ```javascript - managerA = Ember.StateManager.create({ - start: Ember.State.create({}) - }) - - managerA.get('currentState.name') // 'start' - - managerB = Ember.StateManager.create({ - initialState: 'beginHere', - beginHere: Ember.State.create({}) - }) - - managerB.get('currentState.name') // 'beginHere' - ``` - - Because it is a property you may also provide a computed function if you wish - to derive an `initialState` programmatically: - - ```javascript - managerC = Ember.StateManager.create({ - initialState: function() { - if (someLogic) { - return 'active'; - } else { - return 'passive'; - } - }.property(), - active: Ember.State.create({}), - passive: Ember.State.create({}) - }) - ``` - - ## Moving Between States - - A StateManager can have any number of `Ember.State` objects as properties - and can have a single one of these states as its current state. - - Calling `transitionTo` transitions between states: - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({}), - poweredUp: Ember.State.create({}) - }) - - robotManager.get('currentState.name') // 'poweredDown' - robotManager.transitionTo('poweredUp') - robotManager.get('currentState.name') // 'poweredUp' - ``` - - Before transitioning into a new state the existing `currentState` will have - its `exit` method called with the StateManager instance as its first argument - and an object representing the transition as its second argument. - - After transitioning into a new state the new `currentState` will have its - `enter` method called with the StateManager instance as its first argument - and an object representing the transition as its second argument. - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - exit: function(stateManager) { - console.log("exiting the poweredDown state") - } - }), - poweredUp: Ember.State.create({ - enter: function(stateManager) { - console.log("entering the poweredUp state. Destroy all humans.") - } - }) - }) - - robotManager.get('currentState.name') // 'poweredDown' - robotManager.transitionTo('poweredUp') - - // will log - // 'exiting the poweredDown state' - // 'entering the poweredUp state. Destroy all humans.' - ``` - - Once a StateManager is already in a state, subsequent attempts to enter that - state will not trigger enter or exit method calls. Attempts to transition - into a state that the manager does not have will result in no changes in the - StateManager's current state: - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - exit: function(stateManager) { - console.log("exiting the poweredDown state") - } - }), - poweredUp: Ember.State.create({ - enter: function(stateManager) { - console.log("entering the poweredUp state. Destroy all humans.") - } - }) - }) - - robotManager.get('currentState.name') // 'poweredDown' - robotManager.transitionTo('poweredUp') - // will log - // 'exiting the poweredDown state' - // 'entering the poweredUp state. Destroy all humans.' - robotManager.transitionTo('poweredUp') // no logging, no state change - - robotManager.transitionTo('someUnknownState') // silently fails - robotManager.get('currentState.name') // 'poweredUp' - ``` - - Each state property may itself contain properties that are instances of - `Ember.State`. The StateManager can transition to specific sub-states in a - series of transitionTo method calls or via a single transitionTo with the - full path to the specific state. The StateManager will also keep track of the - full path to its currentState - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - charging: Ember.State.create(), - charged: Ember.State.create() - }), - poweredUp: Ember.State.create({ - mobile: Ember.State.create(), - stationary: Ember.State.create() - }) - }) - - robotManager.get('currentState.name') // 'poweredDown' - - robotManager.transitionTo('poweredUp') - robotManager.get('currentState.name') // 'poweredUp' - - robotManager.transitionTo('mobile') - robotManager.get('currentState.name') // 'mobile' - - // transition via a state path - robotManager.transitionTo('poweredDown.charging') - robotManager.get('currentState.name') // 'charging' - - robotManager.get('currentState.path') // 'poweredDown.charging' - ``` - - Enter transition methods will be called for each state and nested child state - in their hierarchical order. Exit methods will be called for each state and - its nested states in reverse hierarchical order. - - Exit transitions for a parent state are not called when entering into one of - its child states, only when transitioning to a new section of possible states - in the hierarchy. - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown', - poweredDown: Ember.State.create({ - enter: function() {}, - exit: function() { - console.log("exited poweredDown state") - }, - charging: Ember.State.create({ - enter: function() {}, - exit: function() {} - }), - charged: Ember.State.create({ - enter: function() { - console.log("entered charged state") - }, - exit: function() { - console.log("exited charged state") - } - }) - }), - poweredUp: Ember.State.create({ - enter: function() { - console.log("entered poweredUp state") - }, - exit: function() {}, - mobile: Ember.State.create({ - enter: function() { - console.log("entered mobile state") - }, - exit: function() {} - }), - stationary: Ember.State.create({ - enter: function() {}, - exit: function() {} - }) - }) - }) - - - robotManager.get('currentState.path') // 'poweredDown' - robotManager.transitionTo('charged') - // logs 'entered charged state' - // but does *not* log 'exited poweredDown state' - robotManager.get('currentState.name') // 'charged - - robotManager.transitionTo('poweredUp.mobile') - // logs - // 'exited charged state' - // 'exited poweredDown state' - // 'entered poweredUp state' - // 'entered mobile state' - ``` - - During development you can set a StateManager's `enableLogging` property to - `true` to receive console messages of state transitions. - - ```javascript - robotManager = Ember.StateManager.create({ - enableLogging: true - }) - ``` - - ## Managing currentState with Actions - - To control which transitions are possible for a given state, and - appropriately handle external events, the StateManager can receive and - route action messages to its states via the `send` method. Calling to - `send` with an action name will begin searching for a method with the same - name starting at the current state and moving up through the parent states - in a state hierarchy until an appropriate method is found or the StateManager - instance itself is reached. - - If an appropriately named method is found it will be called with the state - manager as the first argument and an optional `context` object as the second - argument. - - ```javascript - managerA = Ember.StateManager.create({ - initialState: 'stateOne.substateOne.subsubstateOne', - stateOne: Ember.State.create({ - substateOne: Ember.State.create({ - anAction: function(manager, context) { - console.log("an action was called") - }, - subsubstateOne: Ember.State.create({}) - }) - }) - }) - - managerA.get('currentState.name') // 'subsubstateOne' - managerA.send('anAction') - // 'stateOne.substateOne.subsubstateOne' has no anAction method - // so the 'anAction' method of 'stateOne.substateOne' is called - // and logs "an action was called" - // with managerA as the first argument - // and no second argument - - someObject = {} - managerA.send('anAction', someObject) - // the 'anAction' method of 'stateOne.substateOne' is called again - // with managerA as the first argument and - // someObject as the second argument. - ``` - - If the StateManager attempts to send an action but does not find an appropriately named - method in the current state or while moving upwards through the state hierarchy, it will - repeat the process looking for a `unhandledEvent` method. If an `unhandledEvent` method is - found, it will be called with the original event name as the second argument. If an - `unhandledEvent` method is not found, the StateManager will throw a new Ember.Error. - - ```javascript - managerB = Ember.StateManager.create({ - initialState: 'stateOne.substateOne.subsubstateOne', - stateOne: Ember.State.create({ - substateOne: Ember.State.create({ - subsubstateOne: Ember.State.create({}), - unhandledEvent: function(manager, eventName, context) { - console.log("got an unhandledEvent with name " + eventName); - } - }) - }) - }) - - managerB.get('currentState.name') // 'subsubstateOne' - managerB.send('anAction') - // neither `stateOne.substateOne.subsubstateOne` nor any of it's - // parent states have a handler for `anAction`. `subsubstateOne` - // also does not have a `unhandledEvent` method, but its parent - // state, `substateOne`, does, and it gets fired. It will log - // "got an unhandledEvent with name anAction" - ``` - - Action detection only moves upwards through the state hierarchy from the current state. - It does not search in other portions of the hierarchy. - - ```javascript - managerC = Ember.StateManager.create({ - initialState: 'stateOne.substateOne.subsubstateOne', - stateOne: Ember.State.create({ - substateOne: Ember.State.create({ - subsubstateOne: Ember.State.create({}) - }) - }), - stateTwo: Ember.State.create({ - anAction: function(manager, context) { - // will not be called below because it is - // not a parent of the current state - } - }) - }) - - managerC.get('currentState.name') // 'subsubstateOne' - managerC.send('anAction') - // Error: <Ember.StateManager:ember132> could not - // respond to event anAction in state stateOne.substateOne.subsubstateOne. - ``` - - Inside of an action method the given state should delegate `transitionTo` calls on its - StateManager. - - ```javascript - robotManager = Ember.StateManager.create({ - initialState: 'poweredDown.charging', - poweredDown: Ember.State.create({ - charging: Ember.State.create({ - chargeComplete: function(manager, context) { - manager.transitionTo('charged') - } - }), - charged: Ember.State.create({ - boot: function(manager, context) { - manager.transitionTo('poweredUp') - } - }) - }), - poweredUp: Ember.State.create({ - beginExtermination: function(manager, context) { - manager.transitionTo('rampaging') - }, - rampaging: Ember.State.create() - }) - }) - - robotManager.get('currentState.name') // 'charging' - robotManager.send('boot') // throws error, no boot action - // in current hierarchy - robotManager.get('currentState.name') // remains 'charging' - - robotManager.send('beginExtermination') // throws error, no beginExtermination - // action in current hierarchy - robotManager.get('currentState.name') // remains 'charging' - - robotManager.send('chargeComplete') - robotManager.get('currentState.name') // 'charged' - - robotManager.send('boot') - robotManager.get('currentState.name') // 'poweredUp' - - robotManager.send('beginExtermination', allHumans) - robotManager.get('currentState.name') // 'rampaging' - ``` - - Transition actions can also be created using the `transitionTo` method of the `Ember.State` class. The - following example StateManagers are equivalent: - - ```javascript - aManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: Ember.State.transitionTo('stateTwo') - }), - stateTwo: Ember.State.create({}) - }) - - bManager = Ember.StateManager.create({ - stateOne: Ember.State.create({ - changeToStateTwo: function(manager, context) { - manager.transitionTo('stateTwo', context) - } - }), - stateTwo: Ember.State.create({}) - }) - ``` - - @class StateManager - @namespace Ember - @extends Ember.State -**/ -Ember.StateManager = Ember.State.extend({ - /** - @private - - When creating a new statemanager, look for a default state to transition - into. This state can either be named `start`, or can be specified using the - `initialState` property. - - @method init - */ +Ember.DataAdapter = Ember.Object.extend({ init: function() { this._super(); - - set(this, 'stateMeta', Ember.Map.create()); - - var initialState = get(this, 'initialState'); - - if (!initialState && get(this, 'states.start')) { - initialState = 'start'; - } - - if (initialState) { - this.transitionTo(initialState); - Ember.assert('Failed to transition to initial state "' + initialState + '"', !!get(this, 'currentState')); - } - }, - - stateMetaFor: function(state) { - var meta = get(this, 'stateMeta'), - stateMeta = meta.get(state); - - if (!stateMeta) { - stateMeta = {}; - meta.set(state, stateMeta); - } - - return stateMeta; - }, - - setStateMeta: function(state, key, value) { - return set(this.stateMetaFor(state), key, value); - }, - - getStateMeta: function(state, key) { - return get(this.stateMetaFor(state), key); + this.releaseMethods = Ember.A(); }, /** - The current state from among the manager's possible states. This property should - not be set directly. Use `transitionTo` to move between states by name. - - @property currentState - @type Ember.State + The container of the application being debugged. + This property will be injected + on creation. */ - currentState: null, + container: null, /** - The path of the current state. Returns a string representation of the current - state. + @private - @property currentPath - @type String + Number of attributes to send + as columns. (Enough to make the record + identifiable). */ - currentPath: Ember.computed.alias('currentState.path'), + attributeLimit: 3, /** - The name of transitionEvent that this stateManager will dispatch + @private - @property transitionEvent - @type String - @default 'setup' + Stores all methods that clear observers. + These methods will be called on destruction. */ - transitionEvent: 'setup', + releaseMethods: Ember.A(), /** - If set to true, `errorOnUnhandledEvents` will cause an exception to be - raised if you attempt to send an event to a state manager that is not - handled by the current state or any of its parent states. + @public - @property errorOnUnhandledEvents - @type Boolean - @default true + Specifies how records can be filtered. + Records returned will need to have a `filterValues` + property with a key for every name in the returned array. + + @method getFilters + @return {Array} List of objects defining filters. + The object should have a `name` and `desc` property. */ - errorOnUnhandledEvent: true, - - send: function(event) { - var contexts = [].slice.call(arguments, 1); - Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState')); - return sendEvent.call(this, event, contexts, false); - }, - unhandledEvent: function(manager, event) { - if (get(this, 'errorOnUnhandledEvent')) { - throw new Ember.Error(this.toString() + " could not respond to event " + event + " in state " + get(this, 'currentState.path') + "."); - } + getFilters: function() { + return Ember.A(); }, /** - Finds a state by its state path. + @public - Example: + Fetch the model types and observe them for changes. - ```javascript - manager = Ember.StateManager.create({ - root: Ember.State.create({ - dashboard: Ember.State.create() - }) + @method watchModelTypes + + @param {Function} typesAdded Callback to call to add types. + Takes an array of objects containing wrapped types (returned from `wrapModelType`). + + @param {Function} typesUpdated Callback to call when a type has changed. + Takes an array of objects containing wrapped types. + + @return {Function} Method to call to remove all observers + */ + watchModelTypes: function(typesAdded, typesUpdated) { + var modelTypes = this.getModelTypes(), + self = this, typesToSend, releaseMethods = Ember.A(); + + typesToSend = modelTypes.map(function(type) { + var wrapped = self.wrapModelType(type); + releaseMethods.push(self.observeModelType(type, typesUpdated)); + return wrapped; }); - manager.getStateByPath(manager, "root.dashboard") + typesAdded(typesToSend); - // returns the dashboard state - ``` - - @method getStateByPath - @param {Ember.State} root the state to start searching from - @param {String} path the state path to follow - @return {Ember.State} the state at the end of the path - */ - getStateByPath: function(root, path) { - var parts = path.split('.'), - state = root; - - for (var i=0, len=parts.length; i<len; i++) { - state = get(get(state, 'states'), parts[i]); - if (!state) { break; } - } - - return state; - }, - - findStateByPath: function(state, path) { - var possible; - - while (!possible && state) { - possible = this.getStateByPath(state, path); - state = get(state, 'parentState'); - } - - return possible; + var release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + self.releaseMethods.removeObject(release); + }; + this.releaseMethods.pushObject(release); + return release; }, /** - A state stores its child states in its `states` hash. - This code takes a path like `posts.show` and looks - up `root.states.posts.states.show`. + @public - It returns a list of all of the states from the - root, which is the list of states to call `enter` - on. + Fetch the records of a given type and observe them for changes. - @method getStatesInPath - @param root - @param path + @method watchRecords + + @param {Function} recordsAdded Callback to call to add records. + Takes an array of objects containing wrapped records. + The object should have the following properties: + columnValues: {Object} key and value of a table cell + object: {Object} the actual record object + + @param {Function} recordsUpdated Callback to call when a record has changed. + Takes an array of objects containing wrapped records. + + @param {Function} recordsRemoved Callback to call when a record has removed. + Takes the following parameters: + index: the array index where the records were removed + count: the number of records removed + + @return {Function} Method to call to remove all observers */ - getStatesInPath: function(root, path) { - if (!path || path === "") { return undefined; } - var parts = path.split('.'), - result = [], - states, - state; + watchRecords: function(type, recordsAdded, recordsUpdated, recordsRemoved) { + var self = this, releaseMethods = Ember.A(), records = this.getRecords(type), release; - for (var i=0, len=parts.length; i<len; i++) { - states = get(root, 'states'); - if (!states) { return undefined; } - state = get(states, parts[i]); - if (state) { root = state; result.push(state); } - else { return undefined; } - } - - return result; - }, - - goToState: function() { - // not deprecating this yet so people don't constantly need to - // make trivial changes for little reason. - return this.transitionTo.apply(this, arguments); - }, - - transitionTo: function(path, context) { - // XXX When is transitionTo called with no path - if (Ember.isEmpty(path)) { return; } - - // The ES6 signature of this function is `path, ...contexts` - var contexts = context ? Array.prototype.slice.call(arguments, 1) : [], - currentState = get(this, 'currentState') || this; - - // First, get the enter, exit and resolve states for the current state - // and specified path. If possible, use an existing cache. - var hash = this.contextFreeTransition(currentState, path); - - // Next, process the raw state information for the contexts passed in. - var transition = new Transition(hash).normalize(this, contexts); - - this.enterState(transition); - this.triggerSetupContext(transition); - }, - - contextFreeTransition: function(currentState, path) { - var cache = currentState.getPathsCache(this, path); - if (cache) { return cache; } - - var enterStates = this.getStatesInPath(currentState, path), - exitStates = [], - resolveState = currentState; - - // Walk up the states. For each state, check whether a state matching - // the `path` is nested underneath. This will find the closest - // parent state containing `path`. - // - // This allows the user to pass in a relative path. For example, for - // the following state hierarchy: - // - // | |root - // | |- posts - // | | |- show (* current) - // | |- comments - // | | |- show - // - // If the current state is `<root.posts.show>`, an attempt to - // transition to `comments.show` will match `<root.comments.show>`. - // - // First, this code will look for root.posts.show.comments.show. - // Next, it will look for root.posts.comments.show. Finally, - // it will look for `root.comments.show`, and find the state. - // - // After this process, the following variables will exist: - // - // * resolveState: a common parent state between the current - // and target state. In the above example, `<root>` is the - // `resolveState`. - // * enterStates: a list of all of the states represented - // by the path from the `resolveState`. For example, for - // the path `root.comments.show`, `enterStates` would have - // `[<root.comments>, <root.comments.show>]` - // * exitStates: a list of all of the states from the - // `resolveState` to the `currentState`. In the above - // example, `exitStates` would have - // `[<root.posts>`, `<root.posts.show>]`. - while (resolveState && !enterStates) { - exitStates.unshift(resolveState); - - resolveState = get(resolveState, 'parentState'); - if (!resolveState) { - enterStates = this.getStatesInPath(this, path); - if (!enterStates) { - Ember.assert('Could not find state for path: "'+path+'"'); - return; - } - } - enterStates = this.getStatesInPath(resolveState, path); - } - - // If the path contains some states that are parents of both the - // current state and the target state, remove them. - // - // For example, in the following hierarchy: - // - // |- root - // | |- post - // | | |- index (* current) - // | | |- show - // - // If the `path` is `root.post.show`, the three variables will - // be: - // - // * resolveState: `<state manager>` - // * enterStates: `[<root>, <root.post>, <root.post.show>]` - // * exitStates: `[<root>, <root.post>, <root.post.index>]` - // - // The goal of this code is to remove the common states, so we - // have: - // - // * resolveState: `<root.post>` - // * enterStates: `[<root.post.show>]` - // * exitStates: `[<root.post.index>]` - // - // This avoid unnecessary calls to the enter and exit transitions. - while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { - resolveState = enterStates.shift(); - exitStates.shift(); - } - - // Cache the enterStates, exitStates, and resolveState for the - // current state and the `path`. - var transitions = { - exitStates: exitStates, - enterStates: enterStates, - resolveState: resolveState + var recordUpdated = function(updatedRecord) { + recordsUpdated([updatedRecord]); }; - currentState.setPathsCache(this, path, transitions); - - return transitions; - }, - - triggerSetupContext: function(transitions) { - var contexts = transitions.contexts, - offset = transitions.enterStates.length - contexts.length, - enterStates = transitions.enterStates, - transitionEvent = get(this, 'transitionEvent'); - - Ember.assert("More contexts provided than states", offset >= 0); - - arrayForEach.call(enterStates, function(state, idx) { - state.trigger(transitionEvent, this, contexts[idx-offset]); - }, this); - }, - - getState: function(name) { - var state = get(this, name), - parentState = get(this, 'parentState'); - - if (state) { - return state; - } else if (parentState) { - return parentState.getState(name); - } - }, - - enterState: function(transition) { - var log = this.enableLogging; - - var exitStates = transition.exitStates.slice(0).reverse(); - arrayForEach.call(exitStates, function(state) { - state.trigger('exit', this); - }, this); - - arrayForEach.call(transition.enterStates, function(state) { - if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); } - state.trigger('enter', this); - }, this); - - set(this, 'currentState', transition.finalState); - } -}); - -})(); - - - -(function() { -/** -Ember States - -@module ember -@submodule ember-states -@requires ember-runtime -*/ - -})(); - -(function() { -var slice = [].slice, - helpers = {}, - originalMethods = {}, - injectHelpersCallbacks = []; - -/** - @class Test - @namespace Ember -*/ -Ember.Test = { - - /** - @public - - `registerHelper` is used to register a - test helper that will be injected when - `App.injectTestHelpers` is called. - - The helper method will always be called - with the current Application as the first - parameter. - - For example: - ```javascript - Ember.Test.registerHelper('boot', function(app) { - Ember.run(app, app.deferReadiness); - }); - ``` - - This helper can later be called without arguments - because it will be called with `app` as the - first parameter. - - ```javascript - App = Ember.Application.create(); - App.injectTestHelpers(); - boot(); - ``` - - Whenever you register a helper that - performs async operations, - make sure you `return wait();` at the - end of the helper. - - If an async helper also needs to return a value, - pass it to the `wait` helper as a first argument: - `return wait(val);` - - @method registerHelper - @param name {String} - @param helperMethod {Function} - */ - registerHelper: function(name, helperMethod) { - helpers[name] = helperMethod; - }, - /** - @public - @method unregisterHelper - @param name {String} - */ - unregisterHelper: function(name) { - delete helpers[name]; - if (originalMethods[name]) { - window[name] = originalMethods[name]; - } - delete originalMethods[name]; - }, - - /** - @public - - Used to register callbacks to be fired - whenever `App.injectTestHelpers` is called - - The callback will receive the current application - as an argument. - - @method unregisterHelper - @param name {String} - */ - onInjectHelpers: function(callback) { - injectHelpersCallbacks.push(callback); - }, - - /** - @public - - This returns a thenable tailored - for testing. It catches failed - `onSuccess` callbacks and invokes - the `Ember.Test.adapter.exception` - callback in the last chained then. - - This method should be returned - by async helpers such as `wait`. - - @method promise - @param resolver {Function} - */ - promise: function(resolver) { - var promise = new Ember.RSVP.Promise(resolver); - var thenable = { - chained: false - }; - thenable.then = function(onSuccess, onFailure) { - var thenPromise, nextPromise; - thenable.chained = true; - thenPromise = promise.then(onSuccess, onFailure); - // this is to ensure all downstream fulfillment - // handlers are wrapped in the error handling - nextPromise = Ember.Test.promise(function(resolve) { - resolve(thenPromise); - }); - thenPromise.then(null, function(reason) { - // ensure this is the last promise in the chain - // if not, ignore and the exception will propagate - // this prevents the same error from being fired multiple times - if (!nextPromise.chained) { - Ember.Test.adapter.exception(reason); - } - }); - return nextPromise; - }; - return thenable; - }, - - /** - @public - - Used to allow ember-testing - to communicate with a specific - testing framework. - - You can manually set it before calling - `App.setupForTesting()`. - - Example: - 'Ember.Test.adapter = MyCustomAdapter.create()' - - If you do not set it, ember-testing - will default to `Ember.Test.QUnitAdapter`. - */ - adapter: null -}; - -function curry(app, fn) { - return function() { - var args = slice.call(arguments); - args.unshift(app); - return fn.apply(app, args); - }; -} - -Ember.Application.reopen({ - testHelpers: {}, - - setupForTesting: function() { - Ember.testing = true; - - this.deferReadiness(); - - this.Router.reopen({ - location: 'none' + var recordsToSend = records.map(function(record) { + releaseMethods.push(self.observeRecord(record, recordUpdated)); + return self.wrapRecord(record); }); - // if adapter is not manually set - // default to QUnit - if (!Ember.Test.adapter) { - Ember.Test.adapter = Ember.Test.QUnitAdapter.create(); - } - }, - injectTestHelpers: function() { - this.testHelpers = {}; - for (var name in helpers) { - originalMethods[name] = window[name]; - this.testHelpers[name] = window[name] = curry(this, helpers[name]); - } - - for(var i = 0, l = injectHelpersCallbacks.length; i < l; i++) { - injectHelpersCallbacks[i](this); - } - }, - - removeTestHelpers: function() { - for (var name in helpers) { - window[name] = originalMethods[name]; - delete this.testHelpers[name]; - delete originalMethods[name]; - } - } -}); - -})(); - - - -(function() { -/** - * @module ember - * @sub-module ember-testing - */ - -var $ = Ember.$; - -function testCheckboxClick(handler) { - $('<input type="checkbox">') - .css({ position: 'absolute', left: '-1000px', top: '-1000px' }) - .appendTo('body') - .on('click', handler) - .click() - .remove(); -} - -$(function() { - /** - * Determine whether a checkbox checked using jQuery's "click" method will have - * the correct value for its checked property. In some old versions of jQuery - * (e.g. 1.8.3) this does not behave correctly. - * - * If we determine that the current jQuery version exhibits this behavior, - * patch it to work correctly as in the commit for the actual fix: - * https://github.com/jquery/jquery/commit/1fb2f92. - */ - testCheckboxClick(function() { - if (!this.checked && !$.event.special.click) { - $.event.special.click = { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ($.nodeName( this, "input" ) && this.type === "checkbox" && this.click) { - this.click(); - return false; - } - } - }; - } - }); - - /** - * Try again to verify that the patch took effect or blow up. - */ - testCheckboxClick(function() { - Ember.warn("clicked checkboxes should be checked! the jQuery patch didn't work", this.checked); - }); -}); - -})(); - - - -(function() { -var Test = Ember.Test; - -/** - @class Adapter - @namespace Ember.Test -*/ -Test.Adapter = Ember.Object.extend({ - /** - @public - - This callback will be called - whenever an async operation - is about to start. - - Override this to call your - framework's methods - that handle async operations - - @method asyncStart - */ - asyncStart: Ember.K, - - /** - @public - - This callback will be called - whenever an async operation - has completed. - - @method asyncEnd - */ - asyncEnd: Ember.K, - - /** - @public - - Override this method with your - testing framework's false assertion - This function is called whenever - an exception occurs causing the testing - promise to fail. - - QUnit example: - - ```javascript - exception: function(error) { - ok(false, error); - } - ``` - - @method exception - @param reason {String} - */ - exception: function(error) { - setTimeout(function() { - throw error; - }); - } -}); - -/** - @class QUnitAdapter - @namespace Ember.Test -*/ -Test.QUnitAdapter = Test.Adapter.extend({ - asyncStart: function() { - stop(); - }, - asyncEnd: function() { - start(); - }, - exception: function(error) { - ok(false, Ember.inspect(error)); - } -}); - -})(); - - - -(function() { -/** -* @module ember -* @sub-module ember-testing -*/ - -var get = Ember.get, - Test = Ember.Test, - helper = Test.registerHelper, - countAsync = 0; - -Test.pendingAjaxRequests = 0; - -Test.onInjectHelpers(function() { - Ember.$(document).ajaxStart(function() { - Test.pendingAjaxRequests++; - }); - - Ember.$(document).ajaxStop(function() { - Test.pendingAjaxRequests--; - }); -}); - - -function visit(app, url) { - app.__container__.lookup('router:main').location.setURL(url); - Ember.run(app, app.handleURL, url); - return wait(app); -} - -function click(app, selector, context) { - var $el = findWithAssert(app, selector, context); - Ember.run($el, 'mousedown'); - - if ($el.is(':input')) { - var type = $el.prop('type'); - if (type !== 'checkbox' && type !== 'radio' && type !== 'hidden') { - Ember.run($el, 'focus'); - } - } - - Ember.run($el, 'mouseup'); - Ember.run($el, 'click'); - - return wait(app); -} - -function keyEvent(app, selector, context, type, keyCode) { - var $el; - if (typeof keyCode === 'undefined') { - keyCode = type; - type = context; - context = null; - } - $el = findWithAssert(app, selector, context); - var event = Ember.$.Event(type, { keyCode: keyCode }); - Ember.run($el, 'trigger', event); - return wait(app); -} - -function fillIn(app, selector, context, text) { - var $el; - if (typeof text === 'undefined') { - text = context; - context = null; - } - $el = findWithAssert(app, selector, context); - Ember.run(function() { - $el.val(text).change(); - }); - return wait(app); -} - -function findWithAssert(app, selector, context) { - var $el = find(app, selector, context); - if ($el.length === 0) { - throw new Error("Element " + selector + " not found."); - } - return $el; -} - -function find(app, selector, context) { - var $el; - context = context || get(app, 'rootElement'); - $el = app.$(selector, context); - - return $el; -} - -function wait(app, value) { - var promise; - - promise = Test.promise(function(resolve) { - if (++countAsync === 1) { - Test.adapter.asyncStart(); - } - var watcher = setInterval(function() { - var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; - if (routerIsLoading) { return; } - if (Test.pendingAjaxRequests) { return; } - if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } - - clearInterval(watcher); - - if (--countAsync === 0) { - Test.adapter.asyncEnd(); + var contentDidChange = function(array, idx, removedCount, addedCount) { + for (var i = idx; i < idx + addedCount; i++) { + var record = array.objectAt(i); + var wrapped = self.wrapRecord(record); + releaseMethods.push(self.observeRecord(record, recordUpdated)); + recordsAdded([wrapped]); } - Ember.run(null, resolve, value); - }, 10); - }); + if (removedCount) { + recordsRemoved(idx, removedCount); + } + }; - return buildChainObject(app, promise); -} + var observer = { didChange: contentDidChange, willChange: Ember.K }; + records.addArrayObserver(self, observer); -/** - Builds an object that contains - all helper methods. This object will be - returned by helpers and then-promises. + release = function() { + releaseMethods.forEach(function(fn) { fn(); }); + records.removeArrayObserver(self, observer); + self.releaseMethods.removeObject(release); + }; - This allows us to chain helpers: + recordsAdded(recordsToSend); - ```javascript - visit('posts/new') - .click('.add-btn') - .fillIn('.title', 'Post') - .click('.submit') - .then(function() { - equal('.post-title', 'Post'); - }) - .visit('comments') - .then(function() { - equal(find('.comments'),length, 0); - }); - ``` -*/ -function buildChainObject(app, promise) { - var helperName, obj = {}; - for(helperName in app.testHelpers) { - obj[helperName] = chain(app, promise, app.testHelpers[helperName]); - } - obj.then = function(fn) { - var thenPromise = promise.then(fn); - return buildChainObject(app, thenPromise); - }; - return obj; -} + this.releaseMethods.pushObject(release); + return release; + }, -function chain(app, promise, fn) { - return function() { - var args = arguments, chainedPromise; - chainedPromise = promise.then(function() { - return fn.apply(null, args); + /** + @private + + Clear all observers before destruction + */ + willDestroy: function() { + this._super(); + this.releaseMethods.forEach(function(fn) { + fn(); }); - return buildChainObject(app, chainedPromise); - }; -} + }, -/** -* Loads a route, sets up any controllers, and renders any templates associated -* with the route as though a real user had triggered the route change while -* using your app. -* -* Example: -* -* ``` -* visit('posts/index').then(function() { -* // assert something -* }); -* ``` -* -* @method visit -* @param {String} url the name of the route -* @returns {RSVP.Promise} -*/ -helper('visit', visit); + /** + @private -/** -* Clicks an element and triggers any actions triggered by the element's `click` -* event. -* -* Example: -* -* ``` -* click('.some-jQuery-selector').then(function() { -* // assert something -* }); -* ``` -* -* @method click -* @param {String} selector jQuery selector for finding element on the DOM -* @returns {RSVP.Promise} -*/ -helper('click', click); + Detect whether a class is a model. -/** -* Simulates a key event, e.g. `keypress`, `keydown`, `keyup` with the desired keyCode -* -* Example: -* -* ``` -* keyEvent('.some-jQuery-selector', 'keypress', 13).then(function() { -* // assert something -* }); -* ``` -* -* @method keyEvent -* @param {String} selector jQuery selector for finding element on the DOM -* @param {String} the type of key event, e.g. `keypress`, `keydown`, `keyup` -* @param {Number} the keyCode of the simulated key event -* @returns {RSVP.Promise} -*/ -helper('keyEvent', keyEvent); + Test that against the model class + of your persistence library -/** -* Fills in an input element with some text. -* -* Example: -* -* ``` -* fillIn('#email', 'you@example.com').then(function() { -* // assert something -* }); -* ``` -* -* @method fillIn -* @param {String} selector jQuery selector finding an input element on the DOM -* to fill text with -* @param {String} text text to place inside the input element -* @returns {RSVP.Promise} -*/ -helper('fillIn', fillIn); + @method detect + @param {Class} klass The class to test + @return boolean Whether the class is a model class or not + */ + detect: function(klass) { + return false; + }, -/** -* Finds an element in the context of the app's container element. A simple alias -* for `app.$(selector)`. -* -* Example: -* -* ``` -* var $el = find('.my-selector); -* ``` -* -* @method find -* @param {String} selector jQuery string selector for element lookup -* @returns {Object} jQuery object representing the results of the query -*/ -helper('find', find); + /** + @private + + Get the columns for a given model type. + + @method columnsForType + @param {Class} type The model type + @return {Array} An array of columns of the following format: + name: {String} name of the column + desc: {String} Humanized description (what would show in a table column name) + */ + columnsForType: function(type) { + return Ember.A(); + }, + + /** + @private + + Adds observers to a model type class. + + @method observeModelType + @param {Class} type The model type class + @param {Function} typesUpdated Called when a type is modified. + @return {Function} The function to call to remove observers + */ + + observeModelType: function(type, typesUpdated) { + var self = this, records = this.getRecords(type); + + var onChange = function() { + typesUpdated([self.wrapModelType(type)]); + }; + var observer = { + didChange: function() { + Ember.run.scheduleOnce('actions', this, onChange); + }, + willChange: Ember.K + }; + + records.addArrayObserver(this, observer); + + var release = function() { + records.removeArrayObserver(self, observer); + }; + + return release; + }, + + + /** + @private + + Wraps a given model type and observes changes to it. + + @method wrapModelType + @param {Class} type A model class + @param {Function} typesUpdated callback to call when the type changes + @return {Object} contains the wrapped type and the function to remove observers + Format: + type: {Object} the wrapped type + The wrapped type has the following format: + name: {String} name of the type + count: {Integer} number of records available + columns: {Columns} array of columns to describe the record + object: {Class} the actual Model type class + release: {Function} The function to remove observers + */ + wrapModelType: function(type, typesUpdated) { + var release, records = this.getRecords(type), + typeToSend, self = this; + + typeToSend = { + name: type.toString(), + count: Ember.get(records, 'length'), + columns: this.columnsForType(type), + object: type + }; + + + return typeToSend; + }, + + + /** + @private + + Fetches all models defined in the application. + TODO: Use the resolver instead of looping over namespaces. + + @method getModelTypes + @return {Array} Array of model types + */ + getModelTypes: function() { + var namespaces = Ember.A(Ember.Namespace.NAMESPACES), types = Ember.A(), self = this; + + namespaces.forEach(function(namespace) { + for (var key in namespace) { + if (!namespace.hasOwnProperty(key)) { continue; } + var klass = namespace[key]; + if (self.detect(klass)) { + types.push(klass); + } + } + }); + return types; + }, + + /** + @private + + Fetches all loaded records for a given type. + + @method getRecords + @return {Array} array of records. + This array will be observed for changes, + so it should update when new records are added/removed. + */ + getRecords: function(type) { + return Ember.A(); + }, + + /** + @private + + Wraps a record and observers changes to it + + @method wrapRecord + @param {Object} record The record instance + @return {Object} the wrapped record. Format: + columnValues: {Array} + searchKeywords: {Array} + */ + wrapRecord: function(record) { + var recordToSend = { object: record }, columnValues = {}, self = this; + + recordToSend.columnValues = this.getRecordColumnValues(record); + recordToSend.searchKeywords = this.getRecordKeywords(record); + recordToSend.filterValues = this.getRecordFilterValues(record); + recordToSend.color = this.getRecordColor(record); + + return recordToSend; + }, + + /** + @private + + Gets the values for each column. + + @method getRecordColumnValues + @return {Object} Keys should match column names defined + by the model type. + */ + getRecordColumnValues: function(record) { + return {}; + }, + + /** + @private + + Returns keywords to match when searching records. + + @method getRecordKeywords + @return {Array} Relevant keywords for search. + */ + getRecordKeywords: function(record) { + return Ember.A(); + }, + + /** + @private + + Returns the values of filters defined by `getFilters`. + + @method getRecordFilterValues + @param {Object} record The record instance + @return {Object} The filter values + */ + getRecordFilterValues: function(record) { + return {}; + }, + + /** + @private + + Each record can have a color that represents its state. + + @method getRecordColor + @param {Object} record The record instance + @return {String} The record's color + Possible options: black, red, blue, green + */ + getRecordColor: function(record) { + return null; + }, + + /** + @private + + Observes all relevant properties and re-sends the wrapped record + when a change occurs. + + @method observerRecord + @param {Object} record The record instance + @param {Function} recordUpdated The callback to call when a record is updated. + @return {Function} The function to call to remove all observers. + */ + observeRecord: function(record, recordUpdated) { + return function(){}; + } + +}); -/** -* -* Like `find`, but throws an error if the element selector returns no results -* -* Example: -* -* ``` -* var $el = findWithAssert('.doesnt-exist'); // throws error -* ``` -* -* @method findWithAssert -* @param {String} selector jQuery selector string for finding an element within -* the DOM -* @return {Object} jQuery object representing the results of the query -* @throws {Error} throws error if jQuery object returned has a length of 0 -*/ -helper('findWithAssert', findWithAssert); -helper('wait', wait); })(); -(function() { - -})(); - (function() { /** -Ember +Ember Extension Support @module ember +@submodule ember-extension-support +@requires ember-application */ })(); })(); + + +if (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) { + Ember.Logger.warn("You are running a production build of Ember on localhost and won't receive detailed error messages. "+ + "If you want full error messages please use the non-minified build provided on the Ember website."); +} diff --git a/app/assets/javascripts/external_production/jquery-2.0.3.min.js b/vendor/assets/javascripts/production/jquery-2.0.3.min.js similarity index 100% rename from app/assets/javascripts/external_production/jquery-2.0.3.min.js rename to vendor/assets/javascripts/production/jquery-2.0.3.min.js diff --git a/vendor/assets/javascripts/production/list-view.js b/vendor/assets/javascripts/production/list-view.js new file mode 100755 index 000000000..d3445acb1 --- /dev/null +++ b/vendor/assets/javascripts/production/list-view.js @@ -0,0 +1,1304 @@ +(function() { +var get = Ember.get, set = Ember.set; + +function samePosition(a, b) { + return a && b && a.x === b.x && a.y === b.y; +} + +function positionElement() { + var element, position, _position; + + Ember.instrument('view.updateContext.positionElement', this, function() { + element = get(this, 'element'); + position = get(this, 'position'); + _position = this._position; + + if (!position || !element) { return; } + + // TODO: avoid needing this by avoiding unnecessary + // calls to this method in the first place + if (samePosition(position, _position)) { return; } + this._parentView.applyTransform(element, position.x, position.y); + + this._position = position; + }, this); +} + +Ember.ListItemViewMixin = Ember.Mixin.create({ + init: function(){ + this._super(); + this.one('didInsertElement', positionElement); + }, + classNames: ['ember-list-item-view'], + _position: null, + _positionDidChange: Ember.observer(positionElement, 'position'), + _positionElement: positionElement +}); + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set; + +var backportedInnerString = function(buffer) { + var content = [], childBuffers = buffer.childBuffers; + + Ember.ArrayPolyfills.forEach.call(childBuffers, function(buffer) { + var stringy = typeof buffer === 'string'; + if (stringy) { + content.push(buffer); + } else { + buffer.array(content); + } + }); + + return content.join(''); +}; + +function willInsertElementIfNeeded(view) { + if (view.willInsertElement) { + view.willInsertElement(); + } +} + +function didInsertElementIfNeeded(view) { + if (view.didInsertElement) { + view.didInsertElement(); + } +} + +function rerender() { + var element, buffer, context, hasChildViews; + element = get(this, 'element'); + + if (!element) { return; } + + context = get(this, 'context'); + + // releases action helpers in contents + // this means though that the ListViewItem itself can't use classBindings or attributeBindings + // need support for rerender contents in ember + this.triggerRecursively('willClearRender'); + + if (this.lengthAfterRender > this.lengthBeforeRender) { + this.clearRenderedChildren(); + this._childViews.length = this.lengthBeforeRender; // triage bug in ember + } + + if (context) { + buffer = Ember.RenderBuffer(); + buffer = this.renderToBuffer(buffer); + + // check again for childViews, since rendering may have added some + hasChildViews = this._childViews.length > 0; + + if (hasChildViews) { + this.invokeRecursively(willInsertElementIfNeeded, false); + } + + element.innerHTML = buffer.innerString ? buffer.innerString() : backportedInnerString(buffer); + + set(this, 'element', element); + + this.transitionTo('inDOM'); + + if (hasChildViews) { + this.invokeRecursively(didInsertElementIfNeeded, false); + } + } else { + element.innerHTML = ''; // when there is no context, this view should be completely empty + } +} + +/** + The `Ember.ListViewItem` view class renders a + [div](https://developer.mozilla.org/en/HTML/Element/div) HTML element + with `ember-list-item-view` class. It allows you to specify a custom item + handlebars template for `Ember.ListView`. + + Example: + + ```handlebars + <script type="text/x-handlebars" data-template-name="row_item"> + {{name}} + </script> + ``` + + ```javascript + App.ListView = Ember.ListView.extend({ + height: 500, + rowHeight: 20, + itemViewClass: Ember.ListItemView.extend({templateName: "row_item"}) + }); + ``` + + @extends Ember.View + @class ListItemView + @namespace Ember +*/ +Ember.ListItemView = Ember.View.extend(Ember.ListItemViewMixin, { + updateContext: function(newContext){ + var context = get(this, 'context'); + Ember.instrument('view.updateContext.render', this, function() { + if (context !== newContext) { + this.set('context', newContext); + if (newContext instanceof Ember.ObjectController) { + this.set('controller', newContext); + } + } + }, this); + }, + rerender: function () { Ember.run.scheduleOnce('render', this, rerender); }, + _contextDidChange: Ember.observer(rerender, 'context', 'controller') +}); + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set; + +Ember.ReusableListItemView = Ember.View.extend(Ember.ListItemViewMixin, { + init: function(){ + this._super(); + this.set('context', Ember.ObjectProxy.create()); + }, + isVisible: Ember.computed('context.content', function(){ + return !!this.get('context.content'); + }), + updateContext: function(newContext){ + var context = get(this, 'context.content'); + if (context !== newContext) { + if (this.state === 'inDOM') { + this.prepareForReuse(newContext); + } + set(this, 'context.content', newContext); + } + }, + prepareForReuse: Ember.K +}); + +})(); + + + +(function() { +var el = document.createElement('div'), style = el.style; + +var propPrefixes = ['Webkit', 'Moz', 'O', 'ms']; + +function testProp(prop) { + if (prop in style) return prop; + var uppercaseProp = prop.charAt(0).toUpperCase() + prop.slice(1); + for (var i=0; i<propPrefixes.length; i++) { + var prefixedProp = propPrefixes[i] + uppercaseProp; + if (prefixedProp in style) { + return prefixedProp; + } + } + return null; +} + +var transformProp = testProp('transform'); +var perspectiveProp = testProp('perspective'); + +var supports2D = transformProp !== null; +var supports3D = perspectiveProp !== null; + +Ember.ListViewHelper = { + transformProp: transformProp, + applyTransform: (function(){ + if (supports2D) { + return function(element, x, y){ + element.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px)'; + }; + } else { + return function(element, x, y){ + element.style.top = y + 'px'; + element.style.left = x + 'px'; + }; + } + })(), + apply3DTransform: (function(){ + if (supports3D) { + return function(element, x, y){ + element.style[transformProp] = 'translate3d(' + x + 'px, ' + y + 'px, 0)'; + }; + } else if (supports2D) { + return function(element, x, y){ + element.style[transformProp] = 'translate(' + x + 'px, ' + y + 'px)'; + }; + } else { + return function(element, x, y){ + element.style.top = y + 'px'; + element.style.left = x + 'px'; + }; + } + })() +}; + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set, +min = Math.min, max = Math.max, floor = Math.floor, +ceil = Math.ceil, +forEach = Ember.ArrayPolyfills.forEach; + +function addContentArrayObserver() { + var content = get(this, 'content'); + if (content) { + content.addArrayObserver(this); + } +} + +function removeAndDestroy(object){ + this.removeObject(object); + object.destroy(); +} + +function syncChildViews(){ + Ember.run.once(this, '_syncChildViews'); +} + +function sortByContentIndex (viewOne, viewTwo){ + return get(viewOne, 'contentIndex') - get(viewTwo, 'contentIndex'); +} + +function notifyMutationListeners() { + if (Ember.View.notifyMutationListeners) { + Ember.run.once(Ember.View, 'notifyMutationListeners'); + } +} + +var domManager = Ember.create(Ember.ContainerView.proto().domManager); + +domManager.prepend = function(view, html) { + view.$('.ember-list-container').prepend(html); + notifyMutationListeners(); +}; + +function syncListContainerWidth(){ + var elementWidth, columnCount, containerWidth, element; + + elementWidth = get(this, 'elementWidth'); + columnCount = get(this, 'columnCount'); + containerWidth = elementWidth * columnCount; + element = this.$('.ember-list-container'); + + if (containerWidth && element) { + element.css('width', containerWidth); + } +} + +function enableProfilingOutput() { + function before(name, time, payload) { + console.time(name); + } + + function after (name, time, payload) { + console.timeEnd(name); + } + + if (Ember.ENABLE_PROFILING) { + Ember.subscribe('view._scrollContentTo', { + before: before, + after: after + }); + Ember.subscribe('view.updateContext', { + before: before, + after: after + }); + } +} + +/** + @class Ember.ListViewMixin + @namespace Ember +*/ +Ember.ListViewMixin = Ember.Mixin.create({ + itemViewClass: Ember.ListItemView, + emptyViewClass: Ember.View, + classNames: ['ember-list-view'], + attributeBindings: ['style'], + domManager: domManager, + scrollTop: 0, + bottomPadding: 0, + _lastEndingIndex: 0, + paddingCount: 1, + + /** + @private + + Setup a mixin. + - adding observer to content array + - creating child views based on height and length of the content array + + @method init + */ + init: function() { + this._super(); + this.on('didInsertElement', syncListContainerWidth); + this.columnCountDidChange(); + this._syncChildViews(); + this._addContentArrayObserver(); + }, + + _addContentArrayObserver: Ember.beforeObserver(function() { + addContentArrayObserver.call(this); + }, 'content'), + + /** + Called on your view when it should push strings of HTML into a + `Ember.RenderBuffer`. + + Adds a [div](https://developer.mozilla.org/en-US/docs/HTML/Element/div) + with a required `ember-list-container` class. + + @method render + @param {Ember.RenderBuffer} buffer The render buffer + */ + render: function(buffer) { + buffer.push('<div class="ember-list-container">'); + this._super(buffer); + buffer.push('</div>'); + }, + + willInsertElement: function() { + if (!this.get("height") || !this.get("rowHeight")) { + throw "A ListView must be created with a height and a rowHeight."; + } + this._super(); + }, + + /** + @private + + Sets inline styles of the view: + - height + - width + - position + - overflow + - -webkit-overflow + - overflow-scrolling + + Called while attributes binding. + + @property {Ember.ComputedProperty} style + */ + style: Ember.computed('height', 'width', function() { + var height, width, style, css; + + height = get(this, 'height'); + width = get(this, 'width'); + css = get(this, 'css'); + + style = ''; + + if (height) { style += 'height:' + height + 'px;'; } + if (width) { style += 'width:' + width + 'px;'; } + + for ( var rule in css ){ + if (css.hasOwnProperty(rule)) { + style += rule + ':' + css[rule] + ';'; + } + } + + return style; + }), + + /** + @private + + Performs visual scrolling. Is overridden in Ember.ListView. + + @method scrollTo + */ + scrollTo: function(y) { + throw 'must override to perform the visual scroll and effectively delegate to _scrollContentTo'; + }, + + /** + @private + + Internal method used to force scroll position + + @method scrollTo + */ + _scrollTo: Ember.K, + + /** + @private + @method _scrollContentTo + */ + _scrollContentTo: function(y) { + var startingIndex, endingIndex, + contentIndex, visibleEndingIndex, maxContentIndex, + contentIndexEnd, contentLength, scrollTop; + + scrollTop = max(0, y); + + Ember.instrument('view._scrollContentTo', { + scrollTop: scrollTop, + content: get(this, 'content'), + startingIndex: this._startingIndex(), + endingIndex: min(max(get(this, 'content.length') - 1, 0), this._startingIndex() + this._numChildViewsForViewport()) + }, function () { + contentLength = get(this, 'content.length'); + set(this, 'scrollTop', scrollTop); + + maxContentIndex = max(contentLength - 1, 0); + + startingIndex = this._startingIndex(); + visibleEndingIndex = startingIndex + this._numChildViewsForViewport(); + + endingIndex = min(maxContentIndex, visibleEndingIndex); + + this.trigger('scrollYChanged', y); + + if (startingIndex === this._lastStartingIndex && + endingIndex === this._lastEndingIndex) { + return; + } + + this._reuseChildren(); + + this._lastStartingIndex = startingIndex; + this._lastEndingIndex = endingIndex; + }, this); + }, + + /** + @private + + Computes the height for a `Ember.ListView` scrollable container div. + You must specify `rowHeight` parameter for the height to be computed properly. + + @property {Ember.ComputedProperty} totalHeight + */ + totalHeight: Ember.computed('content.length', 'rowHeight', 'columnCount', 'bottomPadding', function() { + var contentLength, rowHeight, columnCount, bottomPadding; + + contentLength = get(this, 'content.length'); + rowHeight = get(this, 'rowHeight'); + columnCount = get(this, 'columnCount'); + bottomPadding = get(this, 'bottomPadding'); + + return ((ceil(contentLength / columnCount)) * rowHeight) + bottomPadding; + }), + + /** + @private + @method _prepareChildForReuse + */ + _prepareChildForReuse: function(childView) { + childView.prepareForReuse(); + }, + + /** + @private + @method _reuseChildForContentIndex + */ + _reuseChildForContentIndex: function(childView, contentIndex) { + var content, context, newContext, childsCurrentContentIndex, position, enableProfiling; + + content = get(this, 'content'); + enableProfiling = get(this, 'enableProfiling'); + position = this.positionForIndex(contentIndex); + set(childView, 'position', position); + + set(childView, 'contentIndex', contentIndex); + + if (enableProfiling) { + Ember.instrument('view._reuseChildForContentIndex', position, function(){}, this); + } + + newContext = content.objectAt(contentIndex); + childView.updateContext(newContext); + }, + + /** + @private + @method positionForIndex + */ + positionForIndex: function(index){ + var elementWidth, width, columnCount, rowHeight, y, x; + + elementWidth = get(this, 'elementWidth') || 1; + width = get(this, 'width') || 1; + columnCount = get(this, 'columnCount'); + rowHeight = get(this, 'rowHeight'); + + y = (rowHeight * floor(index/columnCount)); + x = (index % columnCount) * elementWidth; + + return { + y: y, + x: x + }; + }, + + /** + @private + @method _childViewCount + */ + _childViewCount: function() { + var contentLength, childViewCountForHeight; + + contentLength = get(this, 'content.length'); + childViewCountForHeight = this._numChildViewsForViewport(); + + return min(contentLength, childViewCountForHeight); + }, + + /** + @private + + Returns a number of columns in the Ember.ListView (for grid layout). + + If you want to have a multi column layout, you need to specify both + `width` and `elementWidth`. + + If no `elementWidth` is specified, it returns `1`. Otherwise, it will + try to fit as many columns as possible for a given `width`. + + @property {Ember.ComputedProperty} columnCount + */ + columnCount: Ember.computed('width', 'elementWidth', function() { + var elementWidth, width, count; + + elementWidth = get(this, 'elementWidth'); + width = get(this, 'width'); + + if (elementWidth) { + count = floor(width / elementWidth); + } else { + count = 1; + } + + return count; + }), + + /** + @private + + Fires every time column count is changed. + + @event columnCountDidChange + */ + columnCountDidChange: Ember.observer(function(){ + var ratio, currentScrollTop, proposedScrollTop, maxScrollTop, + scrollTop, lastColumnCount, newColumnCount, element; + + lastColumnCount = this._lastColumnCount; + + currentScrollTop = get(this, 'scrollTop'); + newColumnCount = get(this, 'columnCount'); + maxScrollTop = get(this, 'maxScrollTop'); + element = get(this, 'element'); + + this._lastColumnCount = newColumnCount; + + if (lastColumnCount) { + ratio = (lastColumnCount / newColumnCount); + proposedScrollTop = currentScrollTop * ratio; + scrollTop = min(maxScrollTop, proposedScrollTop); + + this._scrollTo(scrollTop); + set(this, 'scrollTop', scrollTop); + } + + if (arguments.length > 0) { + // invoked by observer + Ember.run.schedule('afterRender', this, syncListContainerWidth); + } + }, 'columnCount'), + + /** + @private + + Computes max possible scrollTop value given the visible viewport + and scrollable container div height. + + @property {Ember.ComputedProperty} maxScrollTop + */ + maxScrollTop: Ember.computed('height', 'totalHeight', function(){ + var totalHeight, viewportHeight; + + totalHeight = get(this, 'totalHeight'); + viewportHeight = get(this, 'height'); + + return max(0, totalHeight - viewportHeight); + }), + + /** + @private + + Computes the number of views that would fit in the viewport area. + You must specify `height` and `rowHeight` parameters for the number of + views to be computed properly. + + @method _numChildViewsForViewport + */ + _numChildViewsForViewport: function() { + var height, rowHeight, paddingCount, columnCount; + + height = get(this, 'height'); + rowHeight = get(this, 'rowHeight'); + paddingCount = get(this, 'paddingCount'); + columnCount = get(this, 'columnCount'); + + return (ceil(height / rowHeight) * columnCount) + (paddingCount * columnCount); + }, + + /** + @private + + Computes the starting index of the item views array. + Takes `scrollTop` property of the element into account. + + Is used in `_syncChildViews`. + + @method _startingIndex + */ + _startingIndex: function() { + var scrollTop, rowHeight, columnCount, calculatedStartingIndex, + contentLength, largestStartingIndex; + + contentLength = get(this, 'content.length'); + scrollTop = get(this, 'scrollTop'); + rowHeight = get(this, 'rowHeight'); + columnCount = get(this, 'columnCount'); + + calculatedStartingIndex = floor(scrollTop / rowHeight) * columnCount; + + largestStartingIndex = max(contentLength - 1, 0); + + return min(calculatedStartingIndex, largestStartingIndex); + }, + + /** + @private + @event contentWillChange + */ + contentWillChange: Ember.beforeObserver(function() { + var content; + + content = get(this, 'content'); + + if (content) { + content.removeArrayObserver(this); + } + }, 'content'), + + /**), + @private + @event contentDidChange + */ + contentDidChange: Ember.observer(function() { + addContentArrayObserver.call(this); + syncChildViews.call(this); + }, 'content'), + + /** + @private + @property {Function} needsSyncChildViews + */ + needsSyncChildViews: Ember.observer(syncChildViews, 'height', 'width', 'columnCount'), + + /** + @private + + Returns a new item view. Takes `contentIndex` to set the context + of the returned view properly. + + @param {Number} contentIndex item index in the content array + @method _addItemView + */ + _addItemView: function(contentIndex){ + var itemViewClass, childView; + + itemViewClass = get(this, 'itemViewClass'); + childView = this.createChildView(itemViewClass); + + this.pushObject(childView); + }, + + /** + @private + + Intelligently manages the number of childviews. + + @method _syncChildViews + **/ + _syncChildViews: function(){ + var itemViewClass, startingIndex, childViewCount, + endingIndex, numberOfChildViews, numberOfChildViewsNeeded, + childViews, count, delta, index, childViewsLength, contentIndex; + + if (get(this, 'isDestroyed') || get(this, 'isDestroying')) { + return; + } + + childViewCount = this._childViewCount(); + childViews = this.positionOrderedChildViews(); + + startingIndex = this._startingIndex(); + endingIndex = startingIndex + childViewCount; + + numberOfChildViewsNeeded = childViewCount; + numberOfChildViews = childViews.length; + + delta = numberOfChildViewsNeeded - numberOfChildViews; + + if (delta === 0) { + // no change + } else if (delta > 0) { + // more views are needed + contentIndex = this._lastEndingIndex; + + for (count = 0; count < delta; count++, contentIndex++) { + this._addItemView(contentIndex); + } + + } else { + // less views are needed + forEach.call( + childViews.splice(numberOfChildViewsNeeded, numberOfChildViews), + removeAndDestroy, + this + ); + } + + this._scrollContentTo(get(this, 'scrollTop')); + + // if _scrollContentTo short-circuits, we still need + // to call _reuseChildren to get new views positioned + // and rendered correctly + this._reuseChildren(); + + this._lastStartingIndex = startingIndex; + this._lastEndingIndex = this._lastEndingIndex + delta; + }, + + /** + @private + @method _reuseChildren + */ + _reuseChildren: function(){ + var contentLength, childViews, childViewsLength, + startingIndex, endingIndex, childView, attrs, + contentIndex, visibleEndingIndex, maxContentIndex, + contentIndexEnd, scrollTop; + + scrollTop = get(this, 'scrollTop'); + contentLength = get(this, 'content.length'); + maxContentIndex = max(contentLength - 1, 0); + childViews = this._childViews; + childViewsLength = childViews.length; + + startingIndex = this._startingIndex(); + visibleEndingIndex = startingIndex + this._numChildViewsForViewport(); + + endingIndex = min(maxContentIndex, visibleEndingIndex); + + this.trigger('scrollContentTo', scrollTop); + + contentIndexEnd = min(visibleEndingIndex, startingIndex + childViewsLength); + + for (contentIndex = startingIndex; contentIndex < contentIndexEnd; contentIndex++) { + childView = childViews[contentIndex % childViewsLength]; + this._reuseChildForContentIndex(childView, contentIndex); + } + }, + + /** + @private + @method positionOrderedChildViews + */ + positionOrderedChildViews: function() { + return this._childViews.sort(sortByContentIndex); + }, + + arrayWillChange: Ember.K, + + /** + @private + @event arrayDidChange + */ + // TODO: refactor + arrayDidChange: function(content, start, removedCount, addedCount) { + var index, contentIndex; + + if (this.state === 'inDOM') { + // ignore if all changes are out of the visible change + if( start >= this._lastStartingIndex || start < this._lastEndingIndex) { + index = 0; + // ignore all changes not in the visible range + // this can re-position many, rather then causing a cascade of re-renders + forEach.call( + this.positionOrderedChildViews(), + function(childView) { + contentIndex = this._lastStartingIndex + index; + this._reuseChildForContentIndex(childView, contentIndex); + index++; + }, + this + ); + } + + syncChildViews.call(this); + } + } +}); + +})(); + + + +(function() { +var get = Ember.get, set = Ember.set; + +/** + The `Ember.ListView` view class renders a + [div](https://developer.mozilla.org/en/HTML/Element/div) HTML element, + with `ember-list-view` class. + + The context of each item element within the `Ember.ListView` are populated + from the objects in the `Element.ListView`'s `content` property. + + ### `content` as an Array of Objects + + The simplest version of an `Ember.ListView` takes an array of object as its + `content` property. The object will be used as the `context` each item element + inside the rendered `div`. + + Example: + + ```javascript + App.contributors = [{ name: 'Stefan Penner' }, { name: 'Alex Navasardyan' }, { name: 'Rey Cohen'}]; + ``` + + ```handlebars + {{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50}} + {{name}} + {{/collection}} + ``` + + Would result in the following HTML: + + ```html + <div id="ember181" class="ember-view ember-list-view" style="height:500px;width:500px;position:relative;overflow:scroll;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;"> + <div class="ember-list-container"> + <div id="ember186" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 0px, 0);"> + <script id="metamorph-0-start" type="text/x-placeholder"></script>Stefan Penner<script id="metamorph-0-end" type="text/x-placeholder"></script> + </div> + <div id="ember187" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 50px, 0);"> + <script id="metamorph-1-start" type="text/x-placeholder"></script>Alex Navasardyan<script id="metamorph-1-end" type="text/x-placeholder"></script> + </div> + <div id="ember188" class="ember-view ember-list-item-view" style="-webkit-transform: translate3d(0px, 100px, 0);"> + <script id="metamorph-2-start" type="text/x-placeholder"></script>Rey Cohen<script id="metamorph-2-end" type="text/x-placeholder"></script> + </div> + <div id="ember189" class="ember-view ember-list-scrolling-view" style="height: 150px"></div> + </div> + </div> + ``` + + By default `Ember.ListView` provides support for `height`, + `rowHeight`, `width`, `elementWidth`, `scrollTop` parameters. + + Note, that `height` and `rowHeight` are required parameters. + + ```handlebars + {{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50}} + {{name}} + {{/collection}} + ``` + + If you would like to have multiple columns in your view layout, you can + set `width` and `elementWidth` parameters respectively. + + ```handlebars + {{#collection Ember.ListView contentBinding="App.contributors" height=500 rowHeight=50 width=500 elementWidth=80}} + {{name}} + {{/collection}} + ``` + + ### extending `Ember.ListView` + + Example: + + ```handlebars + {{view App.ListView contentBinding="content"}} + + <script type="text/x-handlebars" data-template-name="row_item"> + {{name}} + </script> + ``` + + ```javascript + App.ListView = Ember.ListView.extend({ + height: 500, + width: 500, + elementWidth: 80, + rowHeight: 20, + itemViewClass: Ember.ListItemView.extend({templateName: "row_item"}) + }); + ``` + + @extends Ember.ContainerView + @class ListView + @namespace Ember +*/ +Ember.ListView = Ember.ContainerView.extend(Ember.ListViewMixin, { + css: { + position: 'relative', + overflow: 'scroll', + '-webkit-overflow-scrolling': 'touch', + 'overflow-scrolling': 'touch' + }, + + applyTransform: Ember.ListViewHelper.applyTransform, + + _scrollTo: function(scrollTop) { + var element = get(this, 'element'); + + if (element) { element.scrollTop = scrollTop; } + }, + + didInsertElement: function() { + var that, element; + + that = this, + element = get(this, 'element'); + + this._updateScrollableHeight(); + + this._scroll = function(e) { that.scroll(e); }; + + Ember.$(element).on('scroll', this._scroll); + }, + + willDestroyElement: function() { + var element; + + element = get(this, 'element'); + + Ember.$(element).off('scroll', this._scroll); + }, + + scroll: function(e) { + Ember.run(this, this.scrollTo, e.target.scrollTop); + }, + + scrollTo: function(y){ + var element = get(this, 'element'); + this._scrollTo(y); + this._scrollContentTo(y); + }, + + totalHeightDidChange: Ember.observer(function () { + Ember.run.scheduleOnce('afterRender', this, this._updateScrollableHeight); + }, 'totalHeight'), + + _updateScrollableHeight: function () { + if (this.state === 'inDOM') { + this.$('.ember-list-container').css({ + height: get(this, 'totalHeight') + }); + } + } +}); + +})(); + + + +(function() { +var fieldRegex = /input|textarea|select/i, + hasTouch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch, + handleStart, handleMove, handleEnd, handleCancel, + startEvent, moveEvent, endEvent, cancelEvent; +if (hasTouch) { + startEvent = 'touchstart'; + handleStart = function (e) { + var touch = e.touches[0], + target = touch && touch.target; + // avoid e.preventDefault() on fields + if (target && fieldRegex.test(target.tagName)) { + return; + } + bindWindow(this.scrollerEventHandlers); + this.willBeginScroll(e.touches, e.timeStamp); + e.preventDefault(); + }; + moveEvent = 'touchmove'; + handleMove = function (e) { + this.continueScroll(e.touches, e.timeStamp); + }; + endEvent = 'touchend'; + handleEnd = function (e) { + // if we didn't end up scrolling we need to + // synthesize click since we did e.preventDefault() + // on touchstart + if (!this._isScrolling) { + synthesizeClick(e); + } + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; + cancelEvent = 'touchcancel'; + handleCancel = function (e) { + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; +} else { + startEvent = 'mousedown'; + handleStart = function (e) { + if (e.which !== 1) return; + var target = e.target; + // avoid e.preventDefault() on fields + if (target && fieldRegex.test(target.tagName)) { + return; + } + bindWindow(this.scrollerEventHandlers); + this.willBeginScroll([e], e.timeStamp); + e.preventDefault(); + }; + moveEvent = 'mousemove'; + handleMove = function (e) { + this.continueScroll([e], e.timeStamp); + }; + endEvent = 'mouseup'; + handleEnd = function (e) { + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; + cancelEvent = 'mouseout'; + handleCancel = function (e) { + if (e.relatedTarget) return; + unbindWindow(this.scrollerEventHandlers); + this.endScroll(e.timeStamp); + }; +} + +function handleWheel(e) { + this.mouseWheel(e); + e.preventDefault(); +} + +function bindElement(el, handlers) { + el.addEventListener(startEvent, handlers.start, false); + el.addEventListener('mousewheel', handlers.wheel, false); +} + +function unbindElement(el, handlers) { + el.removeEventListener(startEvent, handlers.start, false); + el.removeEventListener('mousewheel', handlers.wheel, false); +} + +function bindWindow(handlers) { + window.addEventListener(moveEvent, handlers.move, true); + window.addEventListener(endEvent, handlers.end, true); + window.addEventListener(cancelEvent, handlers.cancel, true); +} + +function unbindWindow(handlers) { + window.removeEventListener(moveEvent, handlers.move, true); + window.removeEventListener(endEvent, handlers.end, true); + window.removeEventListener(cancelEvent, handlers.cancel, true); +} + +Ember.VirtualListScrollerEvents = Ember.Mixin.create({ + init: function() { + this.on('didInsertElement', this, 'bindScrollerEvents'); + this.on('willDestroyElement', this, 'unbindScrollerEvents'); + this.scrollerEventHandlers = { + start: bind(this, handleStart), + move: bind(this, handleMove), + end: bind(this, handleEnd), + cancel: bind(this, handleCancel), + wheel: bind(this, handleWheel) + }; + return this._super(); + }, + bindScrollerEvents: function() { + var el = this.get('element'), + handlers = this.scrollerEventHandlers; + bindElement(el, handlers); + }, + unbindScrollerEvents: function() { + var el = this.get('element'), + handlers = this.scrollerEventHandlers; + unbindElement(el, handlers); + unbindWindow(handlers); + } +}); + +function bind(view, handler) { + return function (evt) { + handler.call(view, evt); + }; +} + +function synthesizeClick(e) { + var point = e.changedTouches[0], + target = point.target, + ev; + if (target && fieldRegex.test(target.tagName)) { + ev = document.createEvent('MouseEvents'); + ev.initMouseEvent('click', true, true, e.view, 1, point.screenX, point.screenY, point.clientX, point.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null); + return target.dispatchEvent(ev); + } +} + +})(); + + + +(function() { +/*global Scroller*/ +var max = Math.max, get = Ember.get, set = Ember.set; + +function updateScrollerDimensions(target) { + var width, height, totalHeight; + + target = target || this; + + width = get(target, 'width'); + height = get(target, 'height'); + totalHeight = get(target, 'totalHeight'); + + target.scroller.setDimensions(width, height, width, totalHeight); + target.trigger('scrollerDimensionsDidChange'); +} + +/** + VirtualListView + + @class VirtualListView + @namespace Ember +*/ +Ember.VirtualListView = Ember.ContainerView.extend(Ember.ListViewMixin, Ember.VirtualListScrollerEvents, { + _isScrolling: false, + _mouseWheel: null, + css: { + position: 'relative', + overflow: 'hidden' + }, + + init: function(){ + this._super(); + this.setupScroller(); + }, + _scrollerTop: 0, + applyTransform: Ember.ListViewHelper.apply3DTransform, + + setupScroller: function(){ + var view, y; + + view = this; + + view.scroller = new Scroller(function(left, top, zoom) { + if (view.state !== 'inDOM') { return; } + + if (view.listContainerElement) { + view.applyTransform(view.listContainerElement, 0, -top); + view._scrollerTop = top; + view._scrollContentTo(top); + } + }, { + scrollingX: false, + scrollingComplete: function(){ + view.trigger('scrollingDidComplete'); + } + }); + + view.trigger('didInitializeScroller'); + updateScrollerDimensions(view); + }, + + scrollerDimensionsNeedToChange: Ember.observer(function() { + Ember.run.once(this, updateScrollerDimensions); + }, 'width', 'height', 'totalHeight'), + + didInsertElement: function() { + this.listContainerElement = this.$('> .ember-list-container')[0]; + }, + + willBeginScroll: function(touches, timeStamp) { + this._isScrolling = false; + this.trigger('scrollingDidStart'); + + this.scroller.doTouchStart(touches, timeStamp); + }, + + continueScroll: function(touches, timeStamp) { + var startingScrollTop, endingScrollTop, event; + + if (this._isScrolling) { + this.scroller.doTouchMove(touches, timeStamp); + } else { + startingScrollTop = this._scrollerTop; + + this.scroller.doTouchMove(touches, timeStamp); + + endingScrollTop = this._scrollerTop; + + if (startingScrollTop !== endingScrollTop) { + event = Ember.$.Event("scrollerstart"); + Ember.$(touches[0].target).trigger(event); + + this._isScrolling = true; + } + } + }, + + endScroll: function(timeStamp) { + this.scroller.doTouchEnd(timeStamp); + }, + + // api + scrollTo: function(y, animate) { + if (animate === undefined) { + animate = true; + } + + this.scroller.scrollTo(0, y, animate, 1); + }, + + // events + mouseWheel: function(e){ + var inverted, delta, candidatePosition; + + inverted = e.webkitDirectionInvertedFromDevice; + delta = e.wheelDeltaY * (inverted ? 0.8 : -0.8); + candidatePosition = this.scroller.__scrollTop + delta; + + if ((candidatePosition >= 0) && (candidatePosition <= this.scroller.__maxScrollTop)) { + this.scroller.scrollBy(0, delta, true); + } + + return false; + } +}); + +})(); + + + +(function() { + +})(); + + + +if (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1')) { + Ember.Logger.warn("You are running a production build of Ember on localhost and won't receive detailed error messages. "+ + "If you want full error messages please use the non-minified build provided on the Ember website."); +} diff --git a/app/assets/javascripts/external/rsvp.js b/vendor/assets/javascripts/rsvp.js similarity index 100% rename from app/assets/javascripts/external/rsvp.js rename to vendor/assets/javascripts/rsvp.js diff --git a/app/assets/javascripts/external/show-html.js b/vendor/assets/javascripts/show-html.js similarity index 100% rename from app/assets/javascripts/external/show-html.js rename to vendor/assets/javascripts/show-html.js