diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-06-09 12:13:56 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-06-09 16:20:22 +0200 |
commit | e9ac473ec7dc0433cadf3f64f5d7980e38e0b93c (patch) | |
tree | f641493172b0f5a82b6207e1bb3c058f6f2d4914 /server/sonar-web/src/main/js | |
parent | 6e85ae2bcf98c12c634e792742a72b8e02a04d33 (diff) | |
download | sonarqube-e9ac473ec7dc0433cadf3f64f5d7980e38e0b93c.tar.gz sonarqube-e9ac473ec7dc0433cadf3f64f5d7980e38e0b93c.zip |
upgrade backbone.marionette
Diffstat (limited to 'server/sonar-web/src/main/js')
100 files changed, 3836 insertions, 2562 deletions
diff --git a/server/sonar-web/src/main/js/apps/api-documentation/actions-view.js b/server/sonar-web/src/main/js/apps/api-documentation/actions-view.js index 099e1ea73d0..683485c101b 100644 --- a/server/sonar-web/src/main/js/apps/api-documentation/actions-view.js +++ b/server/sonar-web/src/main/js/apps/api-documentation/actions-view.js @@ -24,7 +24,7 @@ define([ var $ = jQuery; return Marionette.CollectionView.extend({ - itemView: ActionView, + childView: ActionView, scrollToTop: function () { var parent = this.$el.scrollParent(); diff --git a/server/sonar-web/src/main/js/apps/api-documentation/layout.js b/server/sonar-web/src/main/js/apps/api-documentation/layout.js index 64f17b220ed..ae4ce851798 100644 --- a/server/sonar-web/src/main/js/apps/api-documentation/layout.js +++ b/server/sonar-web/src/main/js/apps/api-documentation/layout.js @@ -23,7 +23,7 @@ define([ var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['api-documentation-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/api-documentation/list-view.js b/server/sonar-web/src/main/js/apps/api-documentation/list-view.js index a833cf7b48b..b90ba7f725a 100644 --- a/server/sonar-web/src/main/js/apps/api-documentation/list-view.js +++ b/server/sonar-web/src/main/js/apps/api-documentation/list-view.js @@ -23,9 +23,9 @@ define([ return Marionette.CollectionView.extend({ className: 'list-group', - itemView: ItemView, + childView: ItemView, - itemViewOptions: function (model) { + childViewOptions: function (model) { return { collectionView: this, highlighted: model.get('path') === this.highlighted, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js b/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js index b9c2a7f4921..cf9e8d92bc5 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/facets-view.js @@ -68,7 +68,7 @@ define([ return FacetsView.extend({ - getItemView: function (model) { + getChildView: function (model) { var view = viewsMapping[model.get('property')]; return view ? view : BaseFacet; } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/layout.js b/server/sonar-web/src/main/js/apps/coding-rules/layout.js index 87ec56e9694..238db774707 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/layout.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/layout.js @@ -23,7 +23,7 @@ define([ var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['coding-rules-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js index 941cfc42912..05856342aa4 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js @@ -41,7 +41,7 @@ define([ var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ className: 'coding-rule-details', template: Templates['coding-rules-rule-details'], @@ -99,7 +99,7 @@ define([ this.$el.scrollParent().scrollTop(0); }, - onClose: function () { + onDestroy: function () { this.unbindShortcuts(); }, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js index 5c3829854a6..e15a1149e69 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-creation-view.js @@ -51,7 +51,7 @@ define([ 'keydown @ui.customRuleCreationKey': 'flagKey', 'keyup @ui.customRuleCreationKey': 'flagKey', - 'click #coding-rules-custom-rule-creation-cancel': 'close', + 'click #coding-rules-custom-rule-creation-cancel': 'destroy', 'click @ui.customRuleCreationCreate': 'create', 'click @ui.customRuleCreationReactivate': 'reactivate' }); @@ -168,7 +168,7 @@ define([ } else { that.options.app.controller.showDetails(that.model); } - that.close(); + that.destroy(); }).fail(function (jqXHR) { if (jqXHR.status === 409) { that.existingRule = jqXHR.responseJSON.rule; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js index 98d9634516e..e50b7c11005 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rule-view.js @@ -45,7 +45,7 @@ define([ options = { key: that.model.id }; $.post(url, options).done(function () { that.model.collection.remove(that.model); - that.close(); + that.destroy(); }); } }); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js index 3785b8ed6b6..56ae2a95833 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/custom-rules-view.js @@ -25,10 +25,10 @@ define([ return Marionette.CompositeView.extend({ template: Templates['coding-rules-custom-rules'], - itemView: CustomRuleView, - itemViewContainer: '#coding-rules-detail-custom-rules', + childView: CustomRuleView, + childViewContainer: '#coding-rules-detail-custom-rules', - itemViewOptions: function () { + childViewOptions: function () { return { app: this.options.app, templateRule: this.model diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/manual-rule-creation-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/manual-rule-creation-view.js index 6417beb3c8a..66617fc33ed 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/manual-rule-creation-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/manual-rule-creation-view.js @@ -115,7 +115,7 @@ define([ r = JSON.parse(r); } that.options.app.controller.showDetails(r.rule.key); - that.close(); + that.destroy(); }).fail(function (jqXHR) { if (jqXHR.status === 409) { that.existingRule = jqXHR.responseJSON.rule; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js index e12c4730469..a310ce97674 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/profile-activation-view.js @@ -111,7 +111,7 @@ define([ 400: null } }).done(function () { - that.close(); + that.destroy(); that.trigger('profileActivated', severity, params); }).fail(function (jqXHR) { that.enableForm(); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js index 839e1743af0..5bbe6a2d3cd 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-filter-mixin.js @@ -38,7 +38,7 @@ define([ var obj = {}; obj[property] = '' + value; that.options.app.state.updateFilter(obj); - popup.close(); + popup.destroy(); }); popup.render(); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js index 1a4cb4c3f7e..d8565818c52 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-meta-view.js @@ -51,7 +51,7 @@ define([ }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js index 7c3dc3df275..fc7662e5073 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-profiles-view.js @@ -25,10 +25,10 @@ define([ return Marionette.CompositeView.extend({ template: Templates['coding-rules-rule-profiles'], - itemView: ProfileView, - itemViewContainer: '#coding-rules-detail-quality-profiles', + childView: ProfileView, + childViewContainer: '#coding-rules-detail-quality-profiles', - itemViewOptions: function () { + childViewOptions: function () { return { app: this.options.app, rule: this.model diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js index e5f1ac02eb2..e77a504fa7f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js @@ -26,8 +26,8 @@ define([ return WorkspaceListView.extend({ template: Templates['coding-rules-workspace-list'], - itemView: WorkspaceListItemView, - itemViewContainer: '.js-list', + childView: WorkspaceListItemView, + childViewContainer: '.js-list', emptyView: WorkspaceListEmptyView, bindShortcuts: function () { diff --git a/server/sonar-web/src/main/js/apps/computation/layout.js b/server/sonar-web/src/main/js/apps/computation/layout.js index f747efea06c..2e7edcd22bf 100644 --- a/server/sonar-web/src/main/js/apps/computation/layout.js +++ b/server/sonar-web/src/main/js/apps/computation/layout.js @@ -2,7 +2,7 @@ define([ './templates' ], function () { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['computation-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/computation/list-item-view.js b/server/sonar-web/src/main/js/apps/computation/list-item-view.js index e457b39c2f3..afcf2f15d3f 100644 --- a/server/sonar-web/src/main/js/apps/computation/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/computation/list-item-view.js @@ -13,7 +13,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/computation/list-view.js b/server/sonar-web/src/main/js/apps/computation/list-view.js index 138c36b7619..24878864d30 100644 --- a/server/sonar-web/src/main/js/apps/computation/list-view.js +++ b/server/sonar-web/src/main/js/apps/computation/list-view.js @@ -5,7 +5,7 @@ define([ return Marionette.CollectionView.extend({ tagName: 'ul', - itemView: ListItemView + childView: ListItemView }); }); diff --git a/server/sonar-web/src/main/js/apps/groups/create-view.js b/server/sonar-web/src/main/js/apps/groups/create-view.js index 8d5cfce55aa..cddde867a99 100644 --- a/server/sonar-web/src/main/js/apps/groups/create-view.js +++ b/server/sonar-web/src/main/js/apps/groups/create-view.js @@ -19,7 +19,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/groups/delete-view.js b/server/sonar-web/src/main/js/apps/groups/delete-view.js index 05e07c0d032..85b33a632b5 100644 --- a/server/sonar-web/src/main/js/apps/groups/delete-view.js +++ b/server/sonar-web/src/main/js/apps/groups/delete-view.js @@ -22,7 +22,7 @@ define([ } }).done(function () { collection.total--; - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); }); diff --git a/server/sonar-web/src/main/js/apps/groups/form-view.js b/server/sonar-web/src/main/js/apps/groups/form-view.js index e79ea6eec65..7e3c26b98ee 100644 --- a/server/sonar-web/src/main/js/apps/groups/form-view.js +++ b/server/sonar-web/src/main/js/apps/groups/form-view.js @@ -11,7 +11,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this._super(); this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/groups/layout.js b/server/sonar-web/src/main/js/apps/groups/layout.js index a60fb06f35f..18f6c7738d1 100644 --- a/server/sonar-web/src/main/js/apps/groups/layout.js +++ b/server/sonar-web/src/main/js/apps/groups/layout.js @@ -2,7 +2,7 @@ define([ './templates' ], function () { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['groups-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/groups/list-item-view.js b/server/sonar-web/src/main/js/apps/groups/list-item-view.js index 43eaa5b0d24..c09af127003 100644 --- a/server/sonar-web/src/main/js/apps/groups/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/groups/list-item-view.js @@ -21,7 +21,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/groups/list-view.js b/server/sonar-web/src/main/js/apps/groups/list-view.js index 138c36b7619..24878864d30 100644 --- a/server/sonar-web/src/main/js/apps/groups/list-view.js +++ b/server/sonar-web/src/main/js/apps/groups/list-view.js @@ -5,7 +5,7 @@ define([ return Marionette.CollectionView.extend({ tagName: 'ul', - itemView: ListItemView + childView: ListItemView }); }); diff --git a/server/sonar-web/src/main/js/apps/groups/update-view.js b/server/sonar-web/src/main/js/apps/groups/update-view.js index 71383a1793d..850ddb7510f 100644 --- a/server/sonar-web/src/main/js/apps/groups/update-view.js +++ b/server/sonar-web/src/main/js/apps/groups/update-view.js @@ -18,7 +18,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/groups/users-view.js b/server/sonar-web/src/main/js/apps/groups/users-view.js index 25db7e80158..de5901fc5f1 100644 --- a/server/sonar-web/src/main/js/apps/groups/users-view.js +++ b/server/sonar-web/src/main/js/apps/groups/users-view.js @@ -33,7 +33,7 @@ define([ }); }, - onClose: function () { + onDestroy: function () { this.model.collection.refresh(); this._super(); } diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js index f138a860d30..1e79d92e45d 100644 --- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js +++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js @@ -73,8 +73,8 @@ define([ return key.deleteScope('componentViewer'); }, - onClose: function () { - SourceViewer.prototype.onClose.apply(this, arguments); + onDestroy: function () { + SourceViewer.prototype.onDestroy.apply(this, arguments); this.unbindScrollEvents(); return this.unbindShortcuts(); }, diff --git a/server/sonar-web/src/main/js/apps/issues/facets-view.js b/server/sonar-web/src/main/js/apps/issues/facets-view.js index eb5b4e127fa..26477d4788c 100644 --- a/server/sonar-web/src/main/js/apps/issues/facets-view.js +++ b/server/sonar-web/src/main/js/apps/issues/facets-view.js @@ -41,7 +41,7 @@ define([ }; return FacetsView.extend({ - getItemView: function (model) { + getChildView: function (model) { var view = viewsMapping[model.get('property')]; return view ? view : BaseFacet; } diff --git a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js index 1376e8b6611..4f7d9d8ea55 100644 --- a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js +++ b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js @@ -11,7 +11,7 @@ define([ return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); }, - onClose: function () { + onDestroy: function () { return this.$('[data-toggle="tooltip"]').tooltip('destroy'); } }); diff --git a/server/sonar-web/src/main/js/apps/issues/layout.js b/server/sonar-web/src/main/js/apps/issues/layout.js index 06b2f893af6..13a71301a24 100644 --- a/server/sonar-web/src/main/js/apps/issues/layout.js +++ b/server/sonar-web/src/main/js/apps/issues/layout.js @@ -3,7 +3,7 @@ define([ ], function () { var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['issues-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js index c301c40e872..928b4b42c5f 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js +++ b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js @@ -23,7 +23,7 @@ define([ window.onBulkIssues = _.bind(this.afterBulkChange, this); }, - onClose: function () { + onDestroy: function () { this._super(); window.onBulkIssues = this._onBulkIssues; }, diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js index 510f26c63ea..28a6eb7e433 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js +++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js @@ -68,7 +68,7 @@ define([ }); } that.options.app.state.updateFilter(obj); - that.popup.close(); + that.popup.destroy(); }); this.popup.render(); }, diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js index d4bdad5f65b..5734479fc36 100644 --- a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js +++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js @@ -12,10 +12,18 @@ define([ return WorkspaceListView.extend({ template: Templates['issues-workspace-list'], componentTemplate: Templates['issues-workspace-list-component'], - itemView: IssueView, - itemViewContainer: '.js-list', + childView: IssueView, + childViewContainer: '.js-list', emptyView: EmptyView, + collectionEvents: { + 'all': 'log' + }, + + log: function () { + console.log(arguments); + }, + bindShortcuts: function () { var that = this; var doAction = function (action) { @@ -82,8 +90,8 @@ define([ } }, - appendHtml: function (compositeView, itemView, index) { - var $container = this.getItemViewContainer(compositeView), + attachHtml: function (compositeView, childView, index) { + var $container = this.getChildViewContainer(compositeView), model = this.collection.at(index); if (model != null) { var prev = this.collection.at(index - 1), @@ -99,11 +107,11 @@ define([ $container.append(this.componentTemplate(model.toJSON())); } } - $container.append(itemView.el); + $container.append(childView.el); }, - closeChildren: function () { - WorkspaceListView.prototype.closeChildren.apply(this, arguments); + destroyChildren: function () { + WorkspaceListView.prototype.destroyChildren.apply(this, arguments); this.$('.issues-workspace-list-component').remove(); } }); diff --git a/server/sonar-web/src/main/js/apps/metrics/create-view.js b/server/sonar-web/src/main/js/apps/metrics/create-view.js index 0db9fa70f11..5be34dac1ec 100644 --- a/server/sonar-web/src/main/js/apps/metrics/create-view.js +++ b/server/sonar-web/src/main/js/apps/metrics/create-view.js @@ -22,7 +22,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors([{ msg: jqXHR.responseJSON.err_msg }]); diff --git a/server/sonar-web/src/main/js/apps/metrics/delete-view.js b/server/sonar-web/src/main/js/apps/metrics/delete-view.js index 0acc3c055a9..8719b9fdb3a 100644 --- a/server/sonar-web/src/main/js/apps/metrics/delete-view.js +++ b/server/sonar-web/src/main/js/apps/metrics/delete-view.js @@ -22,7 +22,7 @@ define([ } }).done(function () { collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); }); diff --git a/server/sonar-web/src/main/js/apps/metrics/form-view.js b/server/sonar-web/src/main/js/apps/metrics/form-view.js index 77b5f2a54a5..03e66f715b6 100644 --- a/server/sonar-web/src/main/js/apps/metrics/form-view.js +++ b/server/sonar-web/src/main/js/apps/metrics/form-view.js @@ -35,7 +35,7 @@ define([ this.$('#create-metric-type').select2({ width: '250px' }); }, - onClose: function () { + onDestroy: function () { this._super(); this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/metrics/layout.js b/server/sonar-web/src/main/js/apps/metrics/layout.js index 812212a42fa..9575307d96f 100644 --- a/server/sonar-web/src/main/js/apps/metrics/layout.js +++ b/server/sonar-web/src/main/js/apps/metrics/layout.js @@ -2,7 +2,7 @@ define([ './templates' ], function () { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['metrics-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/metrics/list-item-view.js b/server/sonar-web/src/main/js/apps/metrics/list-item-view.js index 6328aea11d5..c6989e3b7b3 100644 --- a/server/sonar-web/src/main/js/apps/metrics/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/metrics/list-item-view.js @@ -21,7 +21,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/metrics/list-view.js b/server/sonar-web/src/main/js/apps/metrics/list-view.js index 27060bbe7d4..b015c65d966 100644 --- a/server/sonar-web/src/main/js/apps/metrics/list-view.js +++ b/server/sonar-web/src/main/js/apps/metrics/list-view.js @@ -5,9 +5,9 @@ define([ return Marionette.CollectionView.extend({ tagName: 'ul', - itemView: ListItemView, + childView: ListItemView, - itemViewOptions: function () { + childViewOptions: function () { return { types: this.options.types, domains: this.options.domains diff --git a/server/sonar-web/src/main/js/apps/metrics/update-view.js b/server/sonar-web/src/main/js/apps/metrics/update-view.js index c4edec81a8d..3dfea4a584e 100644 --- a/server/sonar-web/src/main/js/apps/metrics/update-view.js +++ b/server/sonar-web/src/main/js/apps/metrics/update-view.js @@ -21,7 +21,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js b/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js index 07eb0cd159a..2ff9d41d518 100644 --- a/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js +++ b/server/sonar-web/src/main/js/apps/nav/context-navbar-view.js @@ -68,7 +68,7 @@ define([ return href.indexOf(url) !== -1; }), isOverviewActive = !isMoreActive && href.indexOf('/dashboard') !== -1 && search.indexOf('did=') === -1; - return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), { + return _.extend(Marionette.LayoutView.prototype.serializeData.apply(this, arguments), { canManageContextDashboards: !!window.SS.user, contextKeyEncoded: encodeURIComponent(this.model.get('componentKey')), diff --git a/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js b/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js index 5a0915e7b20..b4d8dd76f95 100644 --- a/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js +++ b/server/sonar-web/src/main/js/apps/nav/global-navbar-view.js @@ -23,7 +23,7 @@ define([ './templates' ], function (SearchView, ShortcutsHelpView) { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['nav-global-navbar'], modelEvents: { @@ -82,7 +82,7 @@ define([ }, serializeData: function () { - return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), { + return _.extend(Marionette.LayoutView.prototype.serializeData.apply(this, arguments), { user: window.SS.user, userName: window.SS.userName, userEmail: window.SS.userEmail, diff --git a/server/sonar-web/src/main/js/apps/nav/search-view.js b/server/sonar-web/src/main/js/apps/nav/search-view.js index 09c7468851e..da26004aa57 100644 --- a/server/sonar-web/src/main/js/apps/nav/search-view.js +++ b/server/sonar-web/src/main/js/apps/nav/search-view.js @@ -55,11 +55,11 @@ define([ SearchResultsView = SelectableCollectionView.extend({ className: 'menu', tagName: 'ul', - itemView: SearchItemView, + childView: SearchItemView, emptyView: SearchEmptyView }); - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ className: 'navbar-search', tagName: 'form', template: Templates['nav-search'], diff --git a/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js b/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js index 91d09a12ee7..731e3e163d2 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js @@ -21,7 +21,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); }); diff --git a/server/sonar-web/src/main/js/apps/provisioning/create-view.js b/server/sonar-web/src/main/js/apps/provisioning/create-view.js index 2f7d3b3965c..2aba2786c3c 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/create-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/create-view.js @@ -20,7 +20,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors([{ msg: jqXHR.responseJSON.err_msg }]); diff --git a/server/sonar-web/src/main/js/apps/provisioning/delete-view.js b/server/sonar-web/src/main/js/apps/provisioning/delete-view.js index 7797227aa40..dd503c4b1a4 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/delete-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/delete-view.js @@ -22,7 +22,7 @@ define([ } }).done(function () { collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); }); diff --git a/server/sonar-web/src/main/js/apps/provisioning/form-view.js b/server/sonar-web/src/main/js/apps/provisioning/form-view.js index ce359c6fa07..bb0fc0bcef5 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/form-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/form-view.js @@ -11,7 +11,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this._super(); this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/provisioning/layout.js b/server/sonar-web/src/main/js/apps/provisioning/layout.js index d0627a1e5b8..31b67e8b774 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/layout.js +++ b/server/sonar-web/src/main/js/apps/provisioning/layout.js @@ -2,7 +2,7 @@ define([ './templates' ], function () { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['provisioning-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js b/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js index 2b03660b698..ead9b009ca3 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js @@ -22,7 +22,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/provisioning/list-view.js b/server/sonar-web/src/main/js/apps/provisioning/list-view.js index 138c36b7619..24878864d30 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/list-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/list-view.js @@ -5,7 +5,7 @@ define([ return Marionette.CollectionView.extend({ tagName: 'ul', - itemView: ListItemView + childView: ListItemView }); }); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/delete-view.js b/server/sonar-web/src/main/js/apps/quality-gates/delete-view.js index 793934460ca..d142b883c84 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/delete-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/delete-view.js @@ -22,7 +22,7 @@ define([ }; return this.model.destroy(options) .done(function () { - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/details-view.js b/server/sonar-web/src/main/js/apps/quality-gates/details-view.js index df054717447..9af1b38e65e 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/details-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/details-view.js @@ -5,7 +5,7 @@ define([ './templates' ], function (Conditions, DetailConditionsView, ProjectsView) { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['quality-gate-detail'], regions: { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/form-view.js b/server/sonar-web/src/main/js/apps/quality-gates/form-view.js index 5da9dfd05a2..9b61f400bf3 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/form-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/form-view.js @@ -24,7 +24,7 @@ define([ }); return Backbone.ajax(opts) .done(function () { - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/gate-condition-view.js b/server/sonar-web/src/main/js/apps/quality-gates/gate-condition-view.js index 49797716adf..0dfbea0ab17 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/gate-condition-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/gate-condition-view.js @@ -68,7 +68,7 @@ define([ }, cancelAddCondition: function () { - this.close(); + this.destroy(); }, enableUpdate: function () { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-delete-view.js b/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-delete-view.js index 83e32ee2455..c8ed23a2883 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-delete-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-delete-view.js @@ -22,7 +22,7 @@ define([ }; return this.model.destroy(options) .done(function () { - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-view.js b/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-view.js index 5ec13c6734a..b0c0857cc95 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/gate-conditions-view.js @@ -7,9 +7,9 @@ define([ return Marionette.CompositeView.extend({ template: Templates['quality-gate-detail-conditions'], - itemView: ConditionView, + childView: ConditionView, emptyView: ConditionsEmptyView, - itemViewContainer: '.js-conditions', + childViewContainer: '.js-conditions', ui: { metricSelect: '#quality-gate-new-condition-metric' @@ -20,7 +20,7 @@ define([ 'change @ui.metricSelect': 'addCondition' }, - itemViewOptions: function () { + childViewOptions: function () { return { canEdit: this.options.canEdit, gate: this.model, diff --git a/server/sonar-web/src/main/js/apps/quality-gates/gates-view.js b/server/sonar-web/src/main/js/apps/quality-gates/gates-view.js index 6c05b728d55..cd36c381a39 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/gates-view.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/gates-view.js @@ -6,10 +6,10 @@ define([ return Marionette.CompositeView.extend({ className: 'list-group', template: Templates['quality-gates-gates'], - itemView: ItemView, - itemViewContainer: '.js-list', + childView: ItemView, + childViewContainer: '.js-list', - itemViewOptions: function (model) { + childViewOptions: function (model) { return { collectionView: this, highlighted: model.id === this.highlighted diff --git a/server/sonar-web/src/main/js/apps/quality-gates/layout.js b/server/sonar-web/src/main/js/apps/quality-gates/layout.js index e9ae08550c4..791ba30ae83 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/layout.js +++ b/server/sonar-web/src/main/js/apps/quality-gates/layout.js @@ -5,7 +5,7 @@ define([ var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['quality-gates-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js index e3c30699fe7..a0813a661c9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js @@ -60,7 +60,7 @@ define([ }).done(function () { that.model.collection.fetch(); that.model.trigger('select', that.model); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); that.enableForm(); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js index 446254c1c4c..307a904e192 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js @@ -52,7 +52,7 @@ define([ } }).done(function (r) { that.addProfile(r); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js index ec5a0923d4b..531edc0f342 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js @@ -74,7 +74,7 @@ define([ that.showErrors(r.errors, r.warnings); } else { that.addProfile(r.profile); - that.close(); + that.destroy(); } }); }, diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js index 3b2ccee277a..e58f870c672 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js @@ -28,7 +28,7 @@ define([ template: Templates['quality-profiles-delete-profile'], modelEvents: { - 'destroy': 'close' + 'destroy': 'destroy' }, onFormSubmit: function () { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/layout.js b/server/sonar-web/src/main/js/apps/quality-profiles/layout.js index 9099a11fa78..7ec62799470 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/layout.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/layout.js @@ -24,7 +24,7 @@ define([ var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['quality-profiles-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js index ede00f14d73..3f9d16f7ea2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js @@ -28,7 +28,7 @@ define([ var $ = jQuery; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['quality-profiles-profile-details'], regions: { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js index 03e5352e2d0..34de0a2d0c0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js @@ -41,7 +41,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js index 2a433476dcd..ae40ab623a5 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js @@ -26,14 +26,14 @@ define([ className: 'list-group', template: Templates['quality-profiles-profiles'], languageTemplate: Templates['quality-profiles-profiles-language'], - itemView: ProfileView, - itemViewContainer: '.js-list', + childView: ProfileView, + childViewContainer: '.js-list', collectionEvents: { 'filter': 'filterByLanguage' }, - itemViewOptions: function (model) { + childViewOptions: function (model) { return { collectionView: this, highlighted: model.get('key') === this.highlighted @@ -45,8 +45,8 @@ define([ this.render(); }, - appendHtml: function (compositeView, itemView, index) { - var $container = this.getItemViewContainer(compositeView), + attachHtml: function (compositeView, childView, index) { + var $container = this.getChildViewContainer(compositeView), model = this.collection.at(index); if (model != null) { var prev = this.collection.at(index - 1), @@ -62,7 +62,7 @@ define([ $container.append(this.languageTemplate(model.toJSON())); } } - return $container.append(itemView.el); + compositeView._insertAfter(childView); }, closeChildren: function () { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js index b61d9d8a794..1091198d747 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js @@ -50,7 +50,7 @@ define([ } }).done(function () { that.model.set({ name: name }); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js index 645197f7b55..bd223c261f3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js @@ -58,7 +58,7 @@ define([ }).done(function () { that.collection.fetch({ reset: true }); that.collection.trigger('destroy'); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); that.enableForm(); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js index fca5b66476f..09b8feeb812 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js @@ -37,7 +37,7 @@ define([ that.showErrors(r.errors, r.warnings); } else { that.addProfile(r.profile); - that.close(); + that.destroy(); } }); }, diff --git a/server/sonar-web/src/main/js/apps/users/change-password-view.js b/server/sonar-web/src/main/js/apps/users/change-password-view.js index 6187333c9e6..659d3012a79 100644 --- a/server/sonar-web/src/main/js/apps/users/change-password-view.js +++ b/server/sonar-web/src/main/js/apps/users/change-password-view.js @@ -26,7 +26,7 @@ define([ 400: null } }).done(function () { - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/users/create-view.js b/server/sonar-web/src/main/js/apps/users/create-view.js index 026f8095056..562e41e8d5d 100644 --- a/server/sonar-web/src/main/js/apps/users/create-view.js +++ b/server/sonar-web/src/main/js/apps/users/create-view.js @@ -22,7 +22,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/apps/users/deactivate-view.js b/server/sonar-web/src/main/js/apps/users/deactivate-view.js index 37c71d4a94b..000a350ea57 100644 --- a/server/sonar-web/src/main/js/apps/users/deactivate-view.js +++ b/server/sonar-web/src/main/js/apps/users/deactivate-view.js @@ -22,7 +22,7 @@ define([ } }).done(function () { collection.total--; - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); }); diff --git a/server/sonar-web/src/main/js/apps/users/form-view.js b/server/sonar-web/src/main/js/apps/users/form-view.js index f1c7f602d14..50b18c1d237 100644 --- a/server/sonar-web/src/main/js/apps/users/form-view.js +++ b/server/sonar-web/src/main/js/apps/users/form-view.js @@ -19,7 +19,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this._super(); this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/users/groups-view.js b/server/sonar-web/src/main/js/apps/users/groups-view.js index 8fb864add9d..09a127fc6fd 100644 --- a/server/sonar-web/src/main/js/apps/users/groups-view.js +++ b/server/sonar-web/src/main/js/apps/users/groups-view.js @@ -34,9 +34,9 @@ define([ }); }, - onClose: function () { + onDestroy: function () { this.model.collection.refresh(); - Modal.prototype.onClose.apply(this, arguments); + Modal.prototype.onDestroy.apply(this, arguments); } }); diff --git a/server/sonar-web/src/main/js/apps/users/layout.js b/server/sonar-web/src/main/js/apps/users/layout.js index d2b625162e0..9acb054bdad 100644 --- a/server/sonar-web/src/main/js/apps/users/layout.js +++ b/server/sonar-web/src/main/js/apps/users/layout.js @@ -2,7 +2,7 @@ define([ './templates' ], function () { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ template: Templates['users-layout'], regions: { diff --git a/server/sonar-web/src/main/js/apps/users/list-item-view.js b/server/sonar-web/src/main/js/apps/users/list-item-view.js index 99aa973579b..b11e39e2766 100644 --- a/server/sonar-web/src/main/js/apps/users/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/users/list-item-view.js @@ -30,7 +30,7 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/apps/users/list-view.js b/server/sonar-web/src/main/js/apps/users/list-view.js index 138c36b7619..24878864d30 100644 --- a/server/sonar-web/src/main/js/apps/users/list-view.js +++ b/server/sonar-web/src/main/js/apps/users/list-view.js @@ -5,7 +5,7 @@ define([ return Marionette.CollectionView.extend({ tagName: 'ul', - itemView: ListItemView + childView: ListItemView }); }); diff --git a/server/sonar-web/src/main/js/apps/users/update-view.js b/server/sonar-web/src/main/js/apps/users/update-view.js index 81497a3a75d..b276b79ba42 100644 --- a/server/sonar-web/src/main/js/apps/users/update-view.js +++ b/server/sonar-web/src/main/js/apps/users/update-view.js @@ -19,7 +19,7 @@ define([ } }).done(function () { that.collection.refresh(); - that.close(); + that.destroy(); }).fail(function (jqXHR) { that.enableForm(); that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); diff --git a/server/sonar-web/src/main/js/components/common/modals.js b/server/sonar-web/src/main/js/components/common/modals.js index 243ddb04948..b4812096177 100644 --- a/server/sonar-web/src/main/js/components/common/modals.js +++ b/server/sonar-web/src/main/js/components/common/modals.js @@ -41,7 +41,7 @@ define(function () { this.keyScope = key.getScope(); key.setScope('modal'); key('escape', 'modal', function () { - that.close(); + that.destroy(); return false; }); this.show(); @@ -58,7 +58,7 @@ define(function () { }, 0); }, - onClose: function () { + onDestroy: function () { $('html').removeClass(this.htmlClassName); this.removeOverlay(); key.deleteScope('modal'); @@ -67,7 +67,7 @@ define(function () { onCloseClick: function (e) { e.preventDefault(); - this.close(); + this.destroy(); }, renderOverlay: function () { @@ -85,7 +85,7 @@ define(function () { var that = this; $('body').on('click.' + EVENT_SCOPE, function () { $('body').off('click.' + EVENT_SCOPE); - that.close(); + that.destroy(); }); } }); diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js index f978b7ab380..a3d15c1bbb8 100644 --- a/server/sonar-web/src/main/js/components/common/popup.js +++ b/server/sonar-web/src/main/js/components/common/popup.js @@ -31,20 +31,20 @@ define(function () { attachCloseEvents: function () { var that = this; key('escape', function () { - that.close(); + that.destroy(); }); $('body').on('click.bubble-popup', function () { $('body').off('click.bubble-popup'); - that.close(); + that.destroy(); }); this.options.triggerEl.on('click.bubble-popup', function (e) { that.options.triggerEl.off('click.bubble-popup'); e.stopPropagation(); - that.close(); + that.destroy(); }); }, - onClose: function () { + onDestroy: function () { $('body').off('click.bubble-popup'); this.options.triggerEl.off('click.bubble-popup'); } diff --git a/server/sonar-web/src/main/js/components/common/selectable-collection-view.js b/server/sonar-web/src/main/js/components/common/selectable-collection-view.js index b025ba925a5..c7250855946 100644 --- a/server/sonar-web/src/main/js/components/common/selectable-collection-view.js +++ b/server/sonar-web/src/main/js/components/common/selectable-collection-view.js @@ -26,7 +26,7 @@ define(function () { this.listenTo(this.collection, 'reset', this.resetSelectedIndex); }, - itemViewOptions: function (model, index) { + childViewOptions: function (model, index) { return { index: index }; }, diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js index d7c90ddb777..d211ab65eda 100644 --- a/server/sonar-web/src/main/js/components/issue/issue-view.js +++ b/server/sonar-web/src/main/js/components/issue/issue-view.js @@ -72,7 +72,7 @@ define([ data: { issue: this.model.get('key') } }).done(function () { if (that.popup) { - that.popup.close(); + that.popup.destroy(); } that.popup = new ChangeLogView({ triggerEl: t, @@ -86,7 +86,7 @@ define([ updateAfterAction: function (fetch) { if (this.popup) { - this.popup.close(); + this.popup.destroy(); } if (fetch) { this.resetIssue(); @@ -176,7 +176,7 @@ define([ triggerEl: $('body') }); view.submit(window.SS.user, window.SS.userName); - view.close(); + view.destroy(); }, plan: function (e) { diff --git a/server/sonar-web/src/main/js/components/issue/manual-issue-view.js b/server/sonar-web/src/main/js/components/issue/manual-issue-view.js index 1b043544a44..2f819f62f2b 100644 --- a/server/sonar-web/src/main/js/components/issue/manual-issue-view.js +++ b/server/sonar-web/src/main/js/components/issue/manual-issue-view.js @@ -37,7 +37,7 @@ define([ } }, - onClose: function () { + onDestroy: function () { if (key != null && this.key != null) { key.setScope(this.key); } @@ -61,13 +61,13 @@ define([ var that = this; return issue.fetch().done(function () { that.trigger('add', issue); - that.close(); + that.destroy(); }); }, cancel: function (e) { e.preventDefault(); - this.close(); + this.destroy(); }, serializeData: function () { diff --git a/server/sonar-web/src/main/js/components/issue/views/action-options-view.js b/server/sonar-web/src/main/js/components/issue/views/action-options-view.js index 402a0cc6ec6..57b5c95d0a9 100644 --- a/server/sonar-web/src/main/js/components/issue/views/action-options-view.js +++ b/server/sonar-web/src/main/js/components/issue/views/action-options-view.js @@ -75,7 +75,7 @@ define([ return that.selectActiveOption(); }); key('escape', this.keyScope, function () { - return that.close(); + return that.destroy(); }); key('backspace', this.keyScope, function () { return false; @@ -96,8 +96,8 @@ define([ key.setScope(this.currentKeyScope); }, - onClose: function () { - PopupView.prototype.onClose.apply(this, arguments); + onDestroy: function () { + PopupView.prototype.onDestroy.apply(this, arguments); this.unbindShortcuts(); this.$('[data-toggle="tooltip"]').tooltip('destroy'); $('.tooltip').remove(); @@ -105,7 +105,7 @@ define([ selectOption: function (e) { e.preventDefault(); - this.close(); + this.destroy(); }, selectActiveOption: function () { diff --git a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js index f2894e865af..bca6a296365 100644 --- a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js +++ b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js @@ -98,7 +98,7 @@ define([ this.selectActiveOption(); } if (e.keyCode === 27) { - this.close(); + this.destroy(); } if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) { return false; diff --git a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js b/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js index 10e9c713e09..52cd3917a2c 100644 --- a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js +++ b/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js @@ -41,7 +41,7 @@ define([ onKeydown: function (e) { if (e.keyCode === 27) { - this.close(); + this.destroy(); } }, diff --git a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js b/server/sonar-web/src/main/js/components/issue/views/issue-popup.js index 575fb50a52a..9c36ad104b5 100644 --- a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js +++ b/server/sonar-web/src/main/js/components/issue/views/issue-popup.js @@ -11,7 +11,7 @@ define([ events: function () { return { - 'click .js-issue-form-cancel': 'close' + 'click .js-issue-form-cancel': 'destroy' }; }, @@ -21,8 +21,8 @@ define([ this.options.view.render(); }, - onClose: function () { - this.options.view.close(); + onDestroy: function () { + this.options.view.destroy(); }, attachCloseEvents: function () { diff --git a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js b/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js index ee04d7103c9..868230ccd88 100644 --- a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js +++ b/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js @@ -142,7 +142,7 @@ define([ this.selectActiveOption(); } if (e.keyCode === 27) { - this.close(); + this.destroy(); } if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) { return false; diff --git a/server/sonar-web/src/main/js/components/navigator/facets-view.js b/server/sonar-web/src/main/js/components/navigator/facets-view.js index b137c39e81c..bf881e018fa 100644 --- a/server/sonar-web/src/main/js/components/navigator/facets-view.js +++ b/server/sonar-web/src/main/js/components/navigator/facets-view.js @@ -24,13 +24,13 @@ define([ return Marionette.CollectionView.extend({ className: 'search-navigator-facets-list', - itemViewOptions: function () { + childViewOptions: function () { return { app: this.options.app }; }, - getItemView: function () { + getChildView: function () { return BaseFacet; }, diff --git a/server/sonar-web/src/main/js/components/navigator/filters/filter-bar.js b/server/sonar-web/src/main/js/components/navigator/filters/filter-bar.js index b87da53c1d6..28ee2ceb836 100644 --- a/server/sonar-web/src/main/js/components/navigator/filters/filter-bar.js +++ b/server/sonar-web/src/main/js/components/navigator/filters/filter-bar.js @@ -26,7 +26,7 @@ define( function (BaseFilters, MoreCriteriaFilters) { return Marionette.CompositeView.extend({ - itemViewContainer: '.navigator-filters-list', + childViewContainer: '.navigator-filters-list', collectionEvents: { @@ -34,12 +34,12 @@ define( }, - getItemView: function (item) { + getChildView: function (item) { return item.get('type') || BaseFilters.BaseFilterView; }, - itemViewOptions: function () { + childViewOptions: function () { return { filterBarView: this, app: this.options.app @@ -94,7 +94,7 @@ define( getEnabledFilters: function() { - return this.$(this.itemViewContainer).children() + return this.$(this.childViewContainer).children() .not('.navigator-filter-disabled') .not('.navigator-filter-inactive') .not('.navigator-filter-favorite'); @@ -146,8 +146,8 @@ define( }, - onAfterItemAdded: function (itemView) { - if (itemView.model.get('type') === MoreCriteriaFilters.FavoriteFilterView) { + onAddChild: function (childView) { + if (childView.model.get('type') === MoreCriteriaFilters.FavoriteFilterView) { jQuery('.navigator-header').addClass('navigator-header-favorite'); } }, diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js index 524346fa080..75e34798616 100644 --- a/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js +++ b/server/sonar-web/src/main/js/components/navigator/workspace-header-view.js @@ -48,7 +48,7 @@ define(function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js index 0296b1589aa..a9c1831c835 100644 --- a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js +++ b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js @@ -27,7 +27,7 @@ define(function () { loadMore: '.js-more' }, - itemViewOptions: function () { + childViewOptions: function () { return { app: this.options.app }; @@ -44,7 +44,7 @@ define(function () { this.bindShortcuts(); }, - onClose: function () { + onDestroy: function () { this.unbindScrollEvents(); this.unbindShortcuts(); }, diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js index a007145f354..947d6582af0 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/main.js +++ b/server/sonar-web/src/main/js/components/source-viewer/main.js @@ -42,7 +42,7 @@ define([ var $ = jQuery, HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted'; - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ className: 'source-viewer', template: Templates['source-viewer'], @@ -106,9 +106,9 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); }, - onClose: function () { + onDestroy: function () { this.issueViews.forEach(function (view) { - return view.close(); + return view.destroy(); }); this.issueViews = []; this.clearTooltips(); diff --git a/server/sonar-web/src/main/js/components/source-viewer/more-actions.js b/server/sonar-web/src/main/js/components/source-viewer/more-actions.js index c3e7698b8f9..a1b5ff87f99 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/more-actions.js +++ b/server/sonar-web/src/main/js/components/source-viewer/more-actions.js @@ -39,7 +39,7 @@ define([ var that = this; $('body').on('click.component-viewer-more-actions', function () { $('body').off('click.component-viewer-more-actions'); - that.close(); + that.destroy(); }); }, diff --git a/server/sonar-web/src/main/js/components/workspace/main.js b/server/sonar-web/src/main/js/components/workspace/main.js index 5f766ce00fa..7aa7f901aa6 100644 --- a/server/sonar-web/src/main/js/components/workspace/main.js +++ b/server/sonar-web/src/main/js/components/workspace/main.js @@ -89,7 +89,7 @@ define([ var that = this; if (this.viewerView != null) { this.viewerView.model.trigger('hideViewer'); - this.viewerView.close(); + this.viewerView.destroy(); } $('html').addClass('with-workspace'); model.trigger('showViewer'); @@ -112,7 +112,7 @@ define([ closeComponentViewer: function () { if (this.viewerView != null) { - this.viewerView.close(); + this.viewerView.destroy(); $('.with-workspace').removeClass('with-workspace'); } }, diff --git a/server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js b/server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js index 761b9887184..c917fbc9f65 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js @@ -21,11 +21,11 @@ define([ './viewer-header-view' ], function (HeaderView) { - return Marionette.Layout.extend({ + return Marionette.LayoutView.extend({ className: 'workspace-viewer', modelEvents: { - 'destroy': 'close' + 'destroy': 'destroy' }, regions: { diff --git a/server/sonar-web/src/main/js/components/workspace/views/items-view.js b/server/sonar-web/src/main/js/components/workspace/views/items-view.js index a83eeb0f3e8..36aa5679097 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/items-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/items-view.js @@ -25,10 +25,10 @@ define([ return Marionette.CompositeView.extend({ className: 'workspace-nav', template: Templates['workspace-items'], - itemViewContainer: '.workspace-nav-list', - itemView: ItemView, + childViewContainer: '.workspace-nav-list', + childView: ItemView, - itemViewOptions: function () { + childViewOptions: function () { return { collectionView: this }; } }); diff --git a/server/sonar-web/src/main/js/components/workspace/views/rule-view.js b/server/sonar-web/src/main/js/components/workspace/views/rule-view.js index 4b4a498244b..b6ca04fee6f 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/rule-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/rule-view.js @@ -26,7 +26,7 @@ define([ template: Templates['workspace-rule'], modelEvents: { - 'destroy': 'close', + 'destroy': 'destroy', 'change': 'render' }, @@ -35,12 +35,12 @@ define([ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, serializeData: function () { - return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), { + return _.extend(Marionette.LayoutView.prototype.serializeData.apply(this, arguments), { allTags: _.union(this.model.get('sysTags'), this.model.get('tags')) }); } diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js index b0705fb724a..6ef5b47f375 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js @@ -44,7 +44,7 @@ define([ this.$('.js-normal-size').addClass('hidden'); }, - onClose: function () { + onDestroy: function () { this.$('[data-toggle="tooltip"]').tooltip('destroy'); $('.tooltip').remove(); }, diff --git a/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js b/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js index a7dfbc340fc..d0e801ddcfa 100644 --- a/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js +++ b/server/sonar-web/src/main/js/libs/third-party/backbone.marionette.js @@ -1,14 +1,13 @@ // MarionetteJS (Backbone.Marionette) // ---------------------------------- -// v1.6.3 +// v2.4.1 // -// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. +// Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://marionettejs.com - /*! * Includes BabySitter * https://github.com/marionettejs/backbone.babysitter/ @@ -17,2537 +16,3804 @@ * https://github.com/marionettejs/backbone.wreqr/ */ -// Backbone.BabySitter -// ------------------- -// v0.1.0 -// -// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://github.com/marionettejs/backbone.babysitter - -// Backbone.ChildViewContainer -// --------------------------- -// -// Provide a container to store, retrieve and -// shut down child views. - -Backbone.ChildViewContainer = (function(Backbone, _){ - - // Container Constructor - // --------------------- - - var Container = function(views){ - this._views = {}; - this._indexByModel = {}; - this._indexByCustom = {}; - this._updateLength(); - - _.each(views, this.add, this); - }; - - // Container Methods - // ----------------- - - _.extend(Container.prototype, { - - // Add a view to this container. Stores the view - // by `cid` and makes it searchable by the model - // cid (and model itself). Optionally specify - // a custom key to store an retrieve the view. - add: function(view, customIndex){ - var viewCid = view.cid; - // store the view - this._views[viewCid] = view; - - // index it by model - if (view.model){ - this._indexByModel[view.model.cid] = viewCid; - } - - // index by custom - if (customIndex){ - this._indexByCustom[customIndex] = viewCid; - } - - this._updateLength(); - return this; - }, - - // Find a view by the model that was attached to - // it. Uses the model's `cid` to find it. - findByModel: function(model){ - return this.findByModelCid(model.cid); - }, - - // Find a view by the `cid` of the model that was attached to - // it. Uses the model's `cid` to find the view `cid` and - // retrieve the view using it. - findByModelCid: function(modelCid){ - var viewCid = this._indexByModel[modelCid]; - return this.findByCid(viewCid); - }, - - // Find a view by a custom indexer. - findByCustom: function(index){ - var viewCid = this._indexByCustom[index]; - return this.findByCid(viewCid); - }, - - // Find by index. This is not guaranteed to be a - // stable index. - findByIndex: function(index){ - return _.values(this._views)[index]; - }, +(function(root, factory) { - // retrieve a view by its `cid` directly - findByCid: function(cid){ - return this._views[cid]; - }, - - // Remove a view - remove: function(view){ - var viewCid = view.cid; - - // delete model index - if (view.model){ - delete this._indexByModel[view.model.cid]; - } - - // delete custom index - _.any(this._indexByCustom, function(cid, key) { - if (cid === viewCid) { - delete this._indexByCustom[key]; - return true; - } - }, this); - - // remove the view from the container - delete this._views[viewCid]; - - // update the length - this._updateLength(); - return this; - }, + /* istanbul ignore next */ + if (typeof define === 'function' && define.amd) { + define(['backbone', 'underscore'], function(Backbone, _) { + return (root.Marionette = root.Mn = factory(root, Backbone, _)); + }); + } else if (typeof exports !== 'undefined') { + var Backbone = require('backbone'); + var _ = require('underscore'); + module.exports = factory(root, Backbone, _); + } else { + root.Marionette = root.Mn = factory(root, root.Backbone, root._); + } - // Call a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.call`. - call: function(method){ - this.apply(method, _.tail(arguments)); - }, +}(this, function(root, Backbone, _) { + 'use strict'; - // Apply a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.apply`. - apply: function(method, args){ - _.each(this._views, function(view){ - if (_.isFunction(view[method])){ - view[method].apply(view, args || []); + /* istanbul ignore next */ + // Backbone.BabySitter + // ------------------- + // v0.1.6 + // + // Copyright (c)2015 Derick Bailey, Muted Solutions, LLC. + // Distributed under MIT license + // + // http://github.com/marionettejs/backbone.babysitter + (function(Backbone, _) { + "use strict"; + var previousChildViewContainer = Backbone.ChildViewContainer; + // BabySitter.ChildViewContainer + // ----------------------------- + // + // Provide a container to store, retrieve and + // shut down child views. + Backbone.ChildViewContainer = function(Backbone, _) { + // Container Constructor + // --------------------- + var Container = function(views) { + this._views = {}; + this._indexByModel = {}; + this._indexByCustom = {}; + this._updateLength(); + _.each(views, this.add, this); + }; + // Container Methods + // ----------------- + _.extend(Container.prototype, { + // Add a view to this container. Stores the view + // by `cid` and makes it searchable by the model + // cid (and model itself). Optionally specify + // a custom key to store an retrieve the view. + add: function(view, customIndex) { + var viewCid = view.cid; + // store the view + this._views[viewCid] = view; + // index it by model + if (view.model) { + this._indexByModel[view.model.cid] = viewCid; + } + // index by custom + if (customIndex) { + this._indexByCustom[customIndex] = viewCid; + } + this._updateLength(); + return this; + }, + // Find a view by the model that was attached to + // it. Uses the model's `cid` to find it. + findByModel: function(model) { + return this.findByModelCid(model.cid); + }, + // Find a view by the `cid` of the model that was attached to + // it. Uses the model's `cid` to find the view `cid` and + // retrieve the view using it. + findByModelCid: function(modelCid) { + var viewCid = this._indexByModel[modelCid]; + return this.findByCid(viewCid); + }, + // Find a view by a custom indexer. + findByCustom: function(index) { + var viewCid = this._indexByCustom[index]; + return this.findByCid(viewCid); + }, + // Find by index. This is not guaranteed to be a + // stable index. + findByIndex: function(index) { + return _.values(this._views)[index]; + }, + // retrieve a view by its `cid` directly + findByCid: function(cid) { + return this._views[cid]; + }, + // Remove a view + remove: function(view) { + var viewCid = view.cid; + // delete model index + if (view.model) { + delete this._indexByModel[view.model.cid]; + } + // delete custom index + _.any(this._indexByCustom, function(cid, key) { + if (cid === viewCid) { + delete this._indexByCustom[key]; + return true; + } + }, this); + // remove the view from the container + delete this._views[viewCid]; + // update the length + this._updateLength(); + return this; + }, + // Call a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.call`. + call: function(method) { + this.apply(method, _.tail(arguments)); + }, + // Apply a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.apply`. + apply: function(method, args) { + _.each(this._views, function(view) { + if (_.isFunction(view[method])) { + view[method].apply(view, args || []); + } + }); + }, + // Update the `.length` attribute on this container + _updateLength: function() { + this.length = _.size(this._views); } }); - }, - - // Update the `.length` attribute on this container - _updateLength: function(){ - this.length = _.size(this._views); - } - }); + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-106 + // + // Mix in methods from Underscore, for iteration, and other + // collection related features. + var methods = [ "forEach", "each", "map", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "contains", "invoke", "toArray", "first", "initial", "rest", "last", "without", "isEmpty", "pluck", "reduce" ]; + _.each(methods, function(method) { + Container.prototype[method] = function() { + var views = _.values(this._views); + var args = [ views ].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + // return the public API + return Container; + }(Backbone, _); + Backbone.ChildViewContainer.VERSION = "0.1.6"; + Backbone.ChildViewContainer.noConflict = function() { + Backbone.ChildViewContainer = previousChildViewContainer; + return this; + }; + return Backbone.ChildViewContainer; + })(Backbone, _); - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 + /* istanbul ignore next */ + // Backbone.Wreqr (Backbone.Marionette) + // ---------------------------------- + // v1.3.1 // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - Container.prototype[method] = function() { - var views = _.values(this._views); - var args = [views].concat(_.toArray(arguments)); - return _[method].apply(_, args); + // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. + // Distributed under MIT license + // + // http://github.com/marionettejs/backbone.wreqr + (function(Backbone, _) { + "use strict"; + var previousWreqr = Backbone.Wreqr; + var Wreqr = Backbone.Wreqr = {}; + Backbone.Wreqr.VERSION = "1.3.1"; + Backbone.Wreqr.noConflict = function() { + Backbone.Wreqr = previousWreqr; + return this; }; - }); - - // return the public API - return Container; -})(Backbone, _); - -// Backbone.Wreqr (Backbone.Marionette) -// ---------------------------------- -// v1.0.0 -// -// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://github.com/marionettejs/backbone.wreqr - - -Backbone.Wreqr = (function(Backbone, Marionette, _){ - "use strict"; - var Wreqr = {}; - - // Handlers -// -------- -// A registry of functions to call, given a name - -Wreqr.Handlers = (function(Backbone, _){ - "use strict"; - - // Constructor - // ----------- - - var Handlers = function(options){ - this.options = options; - this._wreqrHandlers = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - Handlers.extend = Backbone.Model.extend; - - // Instance Members - // ---------------- - - _.extend(Handlers.prototype, Backbone.Events, { - - // Add multiple handlers using an object literal configuration - setHandlers: function(handlers){ - _.each(handlers, function(handler, name){ - var context = null; - - if (_.isObject(handler) && !_.isFunction(handler)){ - context = handler.context; - handler = handler.callback; + // Handlers + // -------- + // A registry of functions to call, given a name + Wreqr.Handlers = function(Backbone, _) { + "use strict"; + // Constructor + // ----------- + var Handlers = function(options) { + this.options = options; + this._wreqrHandlers = {}; + if (_.isFunction(this.initialize)) { + this.initialize(options); } - - this.setHandler(name, handler, context); - }, this); - }, - - // Add a handler for the given name, with an - // optional context to run the handler within - setHandler: function(name, handler, context){ - var config = { - callback: handler, - context: context }; - - this._wreqrHandlers[name] = config; - - this.trigger("handler:add", name, handler, context); - }, - - // Determine whether or not a handler is registered - hasHandler: function(name){ - return !! this._wreqrHandlers[name]; - }, - - // Get the currently registered handler for - // the specified name. Throws an exception if - // no handler is found. - getHandler: function(name){ - var config = this._wreqrHandlers[name]; - - if (!config){ - throw new Error("Handler not found for '" + name + "'"); - } - - return function(){ - var args = Array.prototype.slice.apply(arguments); - return config.callback.apply(config.context, args); + Handlers.extend = Backbone.Model.extend; + // Instance Members + // ---------------- + _.extend(Handlers.prototype, Backbone.Events, { + // Add multiple handlers using an object literal configuration + setHandlers: function(handlers) { + _.each(handlers, function(handler, name) { + var context = null; + if (_.isObject(handler) && !_.isFunction(handler)) { + context = handler.context; + handler = handler.callback; + } + this.setHandler(name, handler, context); + }, this); + }, + // Add a handler for the given name, with an + // optional context to run the handler within + setHandler: function(name, handler, context) { + var config = { + callback: handler, + context: context + }; + this._wreqrHandlers[name] = config; + this.trigger("handler:add", name, handler, context); + }, + // Determine whether or not a handler is registered + hasHandler: function(name) { + return !!this._wreqrHandlers[name]; + }, + // Get the currently registered handler for + // the specified name. Throws an exception if + // no handler is found. + getHandler: function(name) { + var config = this._wreqrHandlers[name]; + if (!config) { + return; + } + return function() { + var args = Array.prototype.slice.apply(arguments); + return config.callback.apply(config.context, args); + }; + }, + // Remove a handler for the specified name + removeHandler: function(name) { + delete this._wreqrHandlers[name]; + }, + // Remove all handlers from this registry + removeAllHandlers: function() { + this._wreqrHandlers = {}; + } + }); + return Handlers; + }(Backbone, _); + // Wreqr.CommandStorage + // -------------------- + // + // Store and retrieve commands for execution. + Wreqr.CommandStorage = function() { + "use strict"; + // Constructor function + var CommandStorage = function(options) { + this.options = options; + this._commands = {}; + if (_.isFunction(this.initialize)) { + this.initialize(options); + } }; - }, - - // Remove a handler for the specified name - removeHandler: function(name){ - delete this._wreqrHandlers[name]; - }, - - // Remove all handlers from this registry - removeAllHandlers: function(){ - this._wreqrHandlers = {}; - } - }); - - return Handlers; -})(Backbone, _); - - // Wreqr.CommandStorage -// -------------------- -// -// Store and retrieve commands for execution. -Wreqr.CommandStorage = (function(){ - "use strict"; - - // Constructor function - var CommandStorage = function(options){ - this.options = options; - this._commands = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - // Instance methods - _.extend(CommandStorage.prototype, Backbone.Events, { - - // Get an object literal by command name, that contains - // the `commandName` and the `instances` of all commands - // represented as an array of arguments to process - getCommands: function(commandName){ - var commands = this._commands[commandName]; - - // we don't have it, so add it - if (!commands){ - - // build the configuration - commands = { - command: commandName, - instances: [] - }; - - // store it - this._commands[commandName] = commands; - } - - return commands; - }, - - // Add a command by name, to the storage and store the - // args for the command - addCommand: function(commandName, args){ - var command = this.getCommands(commandName); - command.instances.push(args); - }, - - // Clear all commands for the given `commandName` - clearCommands: function(commandName){ - var command = this.getCommands(commandName); - command.instances = []; - } - }); - - return CommandStorage; -})(); - - // Wreqr.Commands -// -------------- -// -// A simple command pattern implementation. Register a command -// handler and execute it. -Wreqr.Commands = (function(Wreqr){ - "use strict"; - - return Wreqr.Handlers.extend({ - // default storage type - storageType: Wreqr.CommandStorage, - - constructor: function(options){ - this.options = options || {}; - - this._initializeStorage(this.options); - this.on("handler:add", this._executeCommands, this); - - var args = Array.prototype.slice.call(arguments); - Wreqr.Handlers.prototype.constructor.apply(this, args); - }, - - // Execute a named command with the supplied args - execute: function(name, args){ - name = arguments[0]; - args = Array.prototype.slice.call(arguments, 1); - - if (this.hasHandler(name)){ - this.getHandler(name).apply(this, args); - } else { - this.storage.addCommand(name, args); - } - - }, - - // Internal method to handle bulk execution of stored commands - _executeCommands: function(name, handler, context){ - var command = this.storage.getCommands(name); - - // loop through and execute all the stored command instances - _.each(command.instances, function(args){ - handler.apply(context, args); + // Instance methods + _.extend(CommandStorage.prototype, Backbone.Events, { + // Get an object literal by command name, that contains + // the `commandName` and the `instances` of all commands + // represented as an array of arguments to process + getCommands: function(commandName) { + var commands = this._commands[commandName]; + // we don't have it, so add it + if (!commands) { + // build the configuration + commands = { + command: commandName, + instances: [] + }; + // store it + this._commands[commandName] = commands; + } + return commands; + }, + // Add a command by name, to the storage and store the + // args for the command + addCommand: function(commandName, args) { + var command = this.getCommands(commandName); + command.instances.push(args); + }, + // Clear all commands for the given `commandName` + clearCommands: function(commandName) { + var command = this.getCommands(commandName); + command.instances = []; + } }); + return CommandStorage; + }(); + // Wreqr.Commands + // -------------- + // + // A simple command pattern implementation. Register a command + // handler and execute it. + Wreqr.Commands = function(Wreqr) { + "use strict"; + return Wreqr.Handlers.extend({ + // default storage type + storageType: Wreqr.CommandStorage, + constructor: function(options) { + this.options = options || {}; + this._initializeStorage(this.options); + this.on("handler:add", this._executeCommands, this); + var args = Array.prototype.slice.call(arguments); + Wreqr.Handlers.prototype.constructor.apply(this, args); + }, + // Execute a named command with the supplied args + execute: function(name, args) { + name = arguments[0]; + args = Array.prototype.slice.call(arguments, 1); + if (this.hasHandler(name)) { + this.getHandler(name).apply(this, args); + } else { + this.storage.addCommand(name, args); + } + }, + // Internal method to handle bulk execution of stored commands + _executeCommands: function(name, handler, context) { + var command = this.storage.getCommands(name); + // loop through and execute all the stored command instances + _.each(command.instances, function(args) { + handler.apply(context, args); + }); + this.storage.clearCommands(name); + }, + // Internal method to initialize storage either from the type's + // `storageType` or the instance `options.storageType`. + _initializeStorage: function(options) { + var storage; + var StorageType = options.storageType || this.storageType; + if (_.isFunction(StorageType)) { + storage = new StorageType(); + } else { + storage = StorageType; + } + this.storage = storage; + } + }); + }(Wreqr); + // Wreqr.RequestResponse + // --------------------- + // + // A simple request/response implementation. Register a + // request handler, and return a response from it + Wreqr.RequestResponse = function(Wreqr) { + "use strict"; + return Wreqr.Handlers.extend({ + request: function() { + var name = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + if (this.hasHandler(name)) { + return this.getHandler(name).apply(this, args); + } + } + }); + }(Wreqr); + // Event Aggregator + // ---------------- + // A pub-sub object that can be used to decouple various parts + // of an application through event-driven architecture. + Wreqr.EventAggregator = function(Backbone, _) { + "use strict"; + var EA = function() {}; + // Copy the `extend` function used by Backbone's classes + EA.extend = Backbone.Model.extend; + // Copy the basic Backbone.Events on to the event aggregator + _.extend(EA.prototype, Backbone.Events); + return EA; + }(Backbone, _); + // Wreqr.Channel + // -------------- + // + // An object that wraps the three messaging systems: + // EventAggregator, RequestResponse, Commands + Wreqr.Channel = function(Wreqr) { + "use strict"; + var Channel = function(channelName) { + this.vent = new Backbone.Wreqr.EventAggregator(); + this.reqres = new Backbone.Wreqr.RequestResponse(); + this.commands = new Backbone.Wreqr.Commands(); + this.channelName = channelName; + }; + _.extend(Channel.prototype, { + // Remove all handlers from the messaging systems of this channel + reset: function() { + this.vent.off(); + this.vent.stopListening(); + this.reqres.removeAllHandlers(); + this.commands.removeAllHandlers(); + return this; + }, + // Connect a hash of events; one for each messaging system + connectEvents: function(hash, context) { + this._connect("vent", hash, context); + return this; + }, + connectCommands: function(hash, context) { + this._connect("commands", hash, context); + return this; + }, + connectRequests: function(hash, context) { + this._connect("reqres", hash, context); + return this; + }, + // Attach the handlers to a given message system `type` + _connect: function(type, hash, context) { + if (!hash) { + return; + } + context = context || this; + var method = type === "vent" ? "on" : "setHandler"; + _.each(hash, function(fn, eventName) { + this[type][method](eventName, _.bind(fn, context)); + }, this); + } + }); + return Channel; + }(Wreqr); + // Wreqr.Radio + // -------------- + // + // An object that lets you communicate with many channels. + Wreqr.radio = function(Wreqr) { + "use strict"; + var Radio = function() { + this._channels = {}; + this.vent = {}; + this.commands = {}; + this.reqres = {}; + this._proxyMethods(); + }; + _.extend(Radio.prototype, { + channel: function(channelName) { + if (!channelName) { + throw new Error("Channel must receive a name"); + } + return this._getChannel(channelName); + }, + _getChannel: function(channelName) { + var channel = this._channels[channelName]; + if (!channel) { + channel = new Wreqr.Channel(channelName); + this._channels[channelName] = channel; + } + return channel; + }, + _proxyMethods: function() { + _.each([ "vent", "commands", "reqres" ], function(system) { + _.each(messageSystems[system], function(method) { + this[system][method] = proxyMethod(this, system, method); + }, this); + }, this); + } + }); + var messageSystems = { + vent: [ "on", "off", "trigger", "once", "stopListening", "listenTo", "listenToOnce" ], + commands: [ "execute", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ], + reqres: [ "request", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ] + }; + var proxyMethod = function(radio, system, method) { + return function(channelName) { + var messageSystem = radio._getChannel(channelName)[system]; + var args = Array.prototype.slice.call(arguments, 1); + return messageSystem[method].apply(messageSystem, args); + }; + }; + return new Radio(); + }(Wreqr); + return Backbone.Wreqr; + })(Backbone, _); - this.storage.clearCommands(name); - }, - - // Internal method to initialize storage either from the type's - // `storageType` or the instance `options.storageType`. - _initializeStorage: function(options){ - var storage; - - var StorageType = options.storageType || this.storageType; - if (_.isFunction(StorageType)){ - storage = new StorageType(); - } else { - storage = StorageType; - } - - this.storage = storage; - } - }); - -})(Wreqr); - - // Wreqr.RequestResponse -// --------------------- -// -// A simple request/response implementation. Register a -// request handler, and return a response from it -Wreqr.RequestResponse = (function(Wreqr){ - "use strict"; - - return Wreqr.Handlers.extend({ - request: function(){ - var name = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - - return this.getHandler(name).apply(this, args); - } - }); - -})(Wreqr); - - // Event Aggregator -// ---------------- -// A pub-sub object that can be used to decouple various parts -// of an application through event-driven architecture. - -Wreqr.EventAggregator = (function(Backbone, _){ - "use strict"; - var EA = function(){}; - - // Copy the `extend` function used by Backbone's classes - EA.extend = Backbone.Model.extend; - - // Copy the basic Backbone.Events on to the event aggregator - _.extend(EA.prototype, Backbone.Events); - - return EA; -})(Backbone, _); + var previousMarionette = root.Marionette; + var previousMn = root.Mn; + var Marionette = Backbone.Marionette = {}; - return Wreqr; -})(Backbone, Backbone.Marionette, _); + Marionette.VERSION = '2.4.1'; -var Marionette = (function(global, Backbone, _){ - "use strict"; + Marionette.noConflict = function() { + root.Marionette = previousMarionette; + root.Mn = previousMn; + return this; + }; - // Define and export the Marionette namespace - var Marionette = {}; Backbone.Marionette = Marionette; - // Get the DOM manipulator for later use - Marionette.$ = Backbone.$; - -// Helpers -// ------- - -// For slicing `arguments` in functions -var slice = Array.prototype.slice; - -function throwError(message, name) { - var error = new Error(message); - error.name = name || 'Error'; - throw error; -} - -// Marionette.extend -// ----------------- - -// Borrow the Backbone `extend` method so we can use it as needed -Marionette.extend = Backbone.Model.extend; - -// Marionette.getOption -// -------------------- - -// Retrieve an object, function or other value from a target -// object or its `options`, with `options` taking precedence. -Marionette.getOption = function(target, optionName){ - if (!target || !optionName){ return; } - var value; - - if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){ - value = target.options[optionName]; - } else { - value = target[optionName]; - } - - return value; -}; + // Get the Deferred creator for later use + Marionette.Deferred = Backbone.$.Deferred; -// Marionette.normalizeMethods -// ---------------------- - -// Pass in a mapping of events => functions or function names -// and return a mapping of events => functions -Marionette.normalizeMethods = function(hash) { - var normalizedHash = {}, method; - _.each(hash, function(fn, name) { - method = fn; - if (!_.isFunction(method)) { - method = this[method]; - } - if (!method) { - return; - } - normalizedHash[name] = method; - }, this); - return normalizedHash; -}; - -// Trigger an event and/or a corresponding method name. Examples: -// -// `this.triggerMethod("foo")` will trigger the "foo" event and -// call the "onFoo" method. -// -// `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and -// call the "onFooBar" method. -Marionette.triggerMethod = (function(){ - - // split the event name on the ":" - var splitter = /(^|:)(\w)/gi; - - // take the event section ("section1:section2:section3") - // and turn it in to uppercase name - function getEventName(match, prefix, eventName) { - return eventName.toUpperCase(); - } - - // actual triggerMethod implementation - var triggerMethod = function(event) { - // get the method name from the event name - var methodName = 'on' + event.replace(splitter, getEventName); - var method = this[methodName]; - - // trigger the event, if a trigger method exists - if(_.isFunction(this.trigger)) { - this.trigger.apply(this, arguments); - } - - // call the onMethodName if it exists - if (_.isFunction(method)) { - // pass all arguments, except the event name - return method.apply(this, _.tail(arguments)); + /* jshint unused: false *//* global console */ + + // Helpers + // ------- + + // Marionette.extend + // ----------------- + + // Borrow the Backbone `extend` method so we can use it as needed + Marionette.extend = Backbone.Model.extend; + + // Marionette.isNodeAttached + // ------------------------- + + // Determine if `el` is a child of the document + Marionette.isNodeAttached = function(el) { + return Backbone.$.contains(document.documentElement, el); + }; + + // Merge `keys` from `options` onto `this` + Marionette.mergeOptions = function(options, keys) { + if (!options) { return; } + _.extend(this, _.pick(options, keys)); + }; + + // Marionette.getOption + // -------------------- + + // Retrieve an object, function or other value from a target + // object or its `options`, with `options` taking precedence. + Marionette.getOption = function(target, optionName) { + if (!target || !optionName) { return; } + if (target.options && (target.options[optionName] !== undefined)) { + return target.options[optionName]; + } else { + return target[optionName]; } }; - - return triggerMethod; -})(); - -// DOMRefresh -// ---------- -// -// Monitor a view's state, and after it has been rendered and shown -// in the DOM, trigger a "dom:refresh" event every time it is -// re-rendered. - -Marionette.MonitorDOMRefresh = (function(documentElement){ - // track when the view has been shown in the DOM, - // using a Marionette.Region (or by other means of triggering "show") - function handleShow(view){ - view._isShown = true; - triggerDOMRefresh(view); - } - - // track when the view has been rendered - function handleRender(view){ - view._isRendered = true; - triggerDOMRefresh(view); - } - - // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method - function triggerDOMRefresh(view){ - if (view._isShown && view._isRendered && isInDOM(view)){ - if (_.isFunction(view.triggerMethod)){ - view.triggerMethod("dom:refresh"); + + // Proxy `Marionette.getOption` + Marionette.proxyGetOption = function(optionName) { + return Marionette.getOption(this, optionName); + }; + + // Similar to `_.result`, this is a simple helper + // If a function is provided we call it with context + // otherwise just return the value. If the value is + // undefined return a default value + Marionette._getValue = function(value, context, params) { + if (_.isFunction(value)) { + value = params ? value.apply(context, params) : value.call(context); + } + return value; + }; + + // Marionette.normalizeMethods + // ---------------------- + + // Pass in a mapping of events => functions or function names + // and return a mapping of events => functions + Marionette.normalizeMethods = function(hash) { + return _.reduce(hash, function(normalizedHash, method, name) { + if (!_.isFunction(method)) { + method = this[method]; } - } - } - - function isInDOM(view) { - return documentElement.contains(view.el); - } - - // Export public API - return function(view){ - view.listenTo(view, "show", function(){ - handleShow(view); - }); - - view.listenTo(view, "render", function(){ - handleRender(view); + if (method) { + normalizedHash[name] = method; + } + return normalizedHash; + }, {}, this); + }; + + // utility method for parsing @ui. syntax strings + // into associated selector + Marionette.normalizeUIString = function(uiString, ui) { + return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) { + return ui[r.slice(4)]; }); }; -})(document.documentElement); - - -// Marionette.bindEntityEvents & unbindEntityEvents -// --------------------------- -// -// These methods are used to bind/unbind a backbone "entity" (collection/model) -// to methods on a target object. -// -// The first parameter, `target`, must have a `listenTo` method from the -// EventBinder object. -// -// The second parameter is the entity (Backbone.Model or Backbone.Collection) -// to bind the events from. -// -// The third parameter is a hash of { "event:name": "eventHandler" } -// configuration. Multiple handlers can be separated by a space. A -// function can be supplied instead of a string handler name. - -(function(Marionette){ - "use strict"; - - // Bind the event to handlers specified as a string of - // handler names on the target object - function bindFromStrings(target, entity, evt, methods){ - var methodNames = methods.split(/\s+/); - - _.each(methodNames,function(methodName) { - - var method = target[methodName]; - if(!method) { - throwError("Method '"+ methodName +"' was configured as an event handler, but does not exist."); + + // allows for the use of the @ui. syntax within + // a given key for triggers and events + // swaps the @ui with the associated selector. + // Returns a new, non-mutated, parsed events hash. + Marionette.normalizeUIKeys = function(hash, ui) { + return _.reduce(hash, function(memo, val, key) { + var normalizedKey = Marionette.normalizeUIString(key, ui); + memo[normalizedKey] = val; + return memo; + }, {}); + }; + + // allows for the use of the @ui. syntax within + // a given value for regions + // swaps the @ui with the associated selector + Marionette.normalizeUIValues = function(hash, ui, properties) { + _.each(hash, function(val, key) { + if (_.isString(val)) { + hash[key] = Marionette.normalizeUIString(val, ui); + } else if (_.isObject(val) && _.isArray(properties)) { + _.extend(val, Marionette.normalizeUIValues(_.pick(val, properties), ui)); + /* Value is an object, and we got an array of embedded property names to normalize. */ + _.each(properties, function(property) { + var propertyVal = val[property]; + if (_.isString(propertyVal)) { + val[property] = Marionette.normalizeUIString(propertyVal, ui); + } + }); } - - target.listenTo(entity, evt, method); }); - } - - // Bind the event to a supplied callback function - function bindToFunction(target, entity, evt, method){ - target.listenTo(entity, evt, method); - } - - // Bind the event to handlers specified as a string of - // handler names on the target object - function unbindFromStrings(target, entity, evt, methods){ - var methodNames = methods.split(/\s+/); - - _.each(methodNames,function(methodName) { - var method = target[methodName]; - target.stopListening(entity, evt, method); - }); - } - - // Bind the event to a supplied callback function - function unbindToFunction(target, entity, evt, method){ - target.stopListening(entity, evt, method); - } - - - // generic looping function - function iterateEvents(target, entity, bindings, functionCallback, stringCallback){ - if (!entity || !bindings) { return; } - - // allow the bindings to be a function - if (_.isFunction(bindings)){ - bindings = bindings.call(target); - } - - // iterate the bindings and bind them - _.each(bindings, function(methods, evt){ - - // allow for a function as the handler, - // or a list of event names as a string - if (_.isFunction(methods)){ - functionCallback(target, entity, evt, methods); - } else { - stringCallback(target, entity, evt, methods); - } - + return hash; + }; + + // Mix in methods from Underscore, for iteration, and other + // collection related features. + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-121 + Marionette.actAsCollection = function(object, listProperty) { + var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', + 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', + 'last', 'without', 'isEmpty', 'pluck']; + + _.each(methods, function(method) { + object[method] = function() { + var list = _.values(_.result(this, listProperty)); + var args = [list].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; }); - } - - // Export Public API - Marionette.bindEntityEvents = function(target, entity, bindings){ - iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); }; - - Marionette.unbindEntityEvents = function(target, entity, bindings){ - iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); + + var deprecate = Marionette.deprecate = function(message, test) { + if (_.isObject(message)) { + message = ( + message.prev + ' is going to be removed in the future. ' + + 'Please use ' + message.next + ' instead.' + + (message.url ? ' See: ' + message.url : '') + ); + } + + if ((test === undefined || !test) && !deprecate._cache[message]) { + deprecate._warn('Deprecation warning: ' + message); + deprecate._cache[message] = true; + } }; - -})(Marionette); - - -// Callbacks -// --------- - -// A simple way of managing a collection of callbacks -// and executing them at a later point in time, using jQuery's -// `Deferred` object. -Marionette.Callbacks = function(){ - this._deferred = Marionette.$.Deferred(); - this._callbacks = []; -}; - -_.extend(Marionette.Callbacks.prototype, { - - // Add a callback to be executed. Callbacks added here are - // guaranteed to execute, even if they are added after the - // `run` method is called. - add: function(callback, contextOverride){ - this._callbacks.push({cb: callback, ctx: contextOverride}); - - this._deferred.done(function(context, options){ - if (contextOverride){ context = contextOverride; } - callback.call(context, options); - }); - }, - - // Run all registered callbacks with the context specified. - // Additional callbacks can be added after this has been run - // and they will still be executed. - run: function(options, context){ - this._deferred.resolve(context, options); - }, - - // Resets the list of callbacks to be run, allowing the same list - // to be run multiple times - whenever the `run` method is called. - reset: function(){ - var callbacks = this._callbacks; - this._deferred = Marionette.$.Deferred(); - this._callbacks = []; - - _.each(callbacks, function(cb){ - this.add(cb.cb, cb.ctx); - }, this); - } -}); - - -// Marionette Controller -// --------------------- -// -// A multi-purpose object to use as a controller for -// modules and routers, and as a mediator for workflow -// and coordination of other objects, views, and more. -Marionette.Controller = function(options){ - this.triggerMethod = Marionette.triggerMethod; - this.options = options || {}; - - if (_.isFunction(this.initialize)){ - this.initialize(this.options); - } -}; - -Marionette.Controller.extend = Marionette.extend; - -// Controller Methods -// -------------- - -// Ensure it can trigger events with Backbone.Events -_.extend(Marionette.Controller.prototype, Backbone.Events, { - close: function(){ - this.stopListening(); - this.triggerMethod("close"); - this.unbind(); - } -}); - -// Region -// ------ -// -// Manage the visual regions of your composite application. See -// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ - -Marionette.Region = function(options){ - this.options = options || {}; - this.el = Marionette.getOption(this, "el"); - - if (!this.el){ - throwError("An 'el' must be specified for a region.", "NoElError"); - } - - if (this.initialize){ - var args = Array.prototype.slice.apply(arguments); - this.initialize.apply(this, args); - } -}; - - -// Region Type methods -// ------------------- - -_.extend(Marionette.Region, { - - // Build an instance of a region by passing in a configuration object - // and a default region type to use if none is specified in the config. + + deprecate._warn = typeof console !== 'undefined' && (console.warn || console.log) || function() {}; + deprecate._cache = {}; + + /* jshint maxstatements: 14, maxcomplexity: 7 */ + + // Trigger Method + // -------------- + + Marionette._triggerMethod = (function() { + // split the event name on the ":" + var splitter = /(^|:)(\w)/gi; + + // take the event section ("section1:section2:section3") + // and turn it in to uppercase name + function getEventName(match, prefix, eventName) { + return eventName.toUpperCase(); + } + + return function(context, event, args) { + var noEventArg = arguments.length < 3; + if (noEventArg) { + args = event; + event = args[0]; + } + + // get the method name from the event name + var methodName = 'on' + event.replace(splitter, getEventName); + var method = context[methodName]; + var result; + + // call the onMethodName if it exists + if (_.isFunction(method)) { + // pass all args, except the event name + result = method.apply(context, noEventArg ? _.rest(args) : args); + } + + // trigger the event, if a trigger method exists + if (_.isFunction(context.trigger)) { + if (noEventArg + args.length > 1) { + context.trigger.apply(context, noEventArg ? args : [event].concat(_.drop(args, 0))); + } else { + context.trigger(event); + } + } + + return result; + }; + })(); + + // Trigger an event and/or a corresponding method name. Examples: // - // The config object should either be a string as a jQuery DOM selector, - // a Region type directly, or an object literal that specifies both - // a selector and regionType: + // `this.triggerMethod("foo")` will trigger the "foo" event and + // call the "onFoo" method. // - // ```js - // { - // selector: "#foo", - // regionType: MyCustomRegion - // } - // ``` + // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and + // call the "onFooBar" method. + Marionette.triggerMethod = function(event) { + return Marionette._triggerMethod(this, arguments); + }; + + // triggerMethodOn invokes triggerMethod on a specific context // - buildRegion: function(regionConfig, defaultRegionType){ - var regionIsString = _.isString(regionConfig); - var regionSelectorIsString = _.isString(regionConfig.selector); - var regionTypeIsUndefined = _.isUndefined(regionConfig.regionType); - var regionIsType = _.isFunction(regionConfig); - - if (!regionIsType && !regionIsString && !regionSelectorIsString) { - throwError("Region must be specified as a Region type, a selector string or an object with selector property"); + // e.g. `Marionette.triggerMethodOn(view, 'show')` + // will trigger a "show" event or invoke onShow the view. + Marionette.triggerMethodOn = function(context) { + var fnc = _.isFunction(context.triggerMethod) ? + context.triggerMethod : + Marionette.triggerMethod; + + return fnc.apply(context, _.rest(arguments)); + }; + + // DOM Refresh + // ----------- + + // Monitor a view's state, and after it has been rendered and shown + // in the DOM, trigger a "dom:refresh" event every time it is + // re-rendered. + + Marionette.MonitorDOMRefresh = function(view) { + + // track when the view has been shown in the DOM, + // using a Marionette.Region (or by other means of triggering "show") + function handleShow() { + view._isShown = true; + triggerDOMRefresh(); } - - var selector, RegionType; - - // get the selector for the region - - if (regionIsString) { - selector = regionConfig; + + // track when the view has been rendered + function handleRender() { + view._isRendered = true; + triggerDOMRefresh(); } - - if (regionConfig.selector) { - selector = regionConfig.selector; - delete regionConfig.selector; + + // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method + function triggerDOMRefresh() { + if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) { + if (_.isFunction(view.triggerMethod)) { + view.triggerMethod('dom:refresh'); + } + } } - - // get the type for the region - - if (regionIsType){ - RegionType = regionConfig; + + view.on({ + show: handleShow, + render: handleRender + }); + }; + + /* jshint maxparams: 5 */ + + // Bind Entity Events & Unbind Entity Events + // ----------------------------------------- + // + // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model) + // to methods on a target object. + // + // The first parameter, `target`, must have the Backbone.Events module mixed in. + // + // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or + // any object that has Backbone.Events mixed in) to bind the events from. + // + // The third parameter is a hash of { "event:name": "eventHandler" } + // configuration. Multiple handlers can be separated by a space. A + // function can be supplied instead of a string handler name. + + (function(Marionette) { + 'use strict'; + + // Bind the event to handlers specified as a string of + // handler names on the target object + function bindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + + var method = target[methodName]; + if (!method) { + throw new Marionette.Error('Method "' + methodName + + '" was configured as an event handler, but does not exist.'); + } + + target.listenTo(entity, evt, method); + }); } - - if (!regionIsType && regionTypeIsUndefined) { - RegionType = defaultRegionType; + + // Bind the event to a supplied callback function + function bindToFunction(target, entity, evt, method) { + target.listenTo(entity, evt, method); } - - if (regionConfig.regionType) { - RegionType = regionConfig.regionType; - delete regionConfig.regionType; + + // Bind the event to handlers specified as a string of + // handler names on the target object + function unbindFromStrings(target, entity, evt, methods) { + var methodNames = methods.split(/\s+/); + + _.each(methodNames, function(methodName) { + var method = target[methodName]; + target.stopListening(entity, evt, method); + }); } - - if (regionIsString || regionIsType) { - regionConfig = {}; + + // Bind the event to a supplied callback function + function unbindToFunction(target, entity, evt, method) { + target.stopListening(entity, evt, method); } - - regionConfig.el = selector; - - // build the region instance - var region = new RegionType(regionConfig); - - // override the `getEl` function if we have a parentEl - // this must be overridden to ensure the selector is found - // on the first use of the region. if we try to assign the - // region's `el` to `parentEl.find(selector)` in the object - // literal to build the region, the element will not be - // guaranteed to be in the DOM already, and will cause problems - if (regionConfig.parentEl){ - region.getEl = function(selector) { - var parentEl = regionConfig.parentEl; - if (_.isFunction(parentEl)){ - parentEl = parentEl(); + + // generic looping function + function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { + if (!entity || !bindings) { return; } + + // type-check bindings + if (!_.isObject(bindings)) { + throw new Marionette.Error({ + message: 'Bindings must be an object or function.', + url: 'marionette.functions.html#marionettebindentityevents' + }); + } + + // allow the bindings to be a function + bindings = Marionette._getValue(bindings, target); + + // iterate the bindings and bind them + _.each(bindings, function(methods, evt) { + + // allow for a function as the handler, + // or a list of event names as a string + if (_.isFunction(methods)) { + functionCallback(target, entity, evt, methods); + } else { + stringCallback(target, entity, evt, methods); } - return parentEl.find(selector); - }; + + }); } + + // Export Public API + Marionette.bindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); + }; + + Marionette.unbindEntityEvents = function(target, entity, bindings) { + iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); + }; + + // Proxy `bindEntityEvents` + Marionette.proxyBindEntityEvents = function(entity, bindings) { + return Marionette.bindEntityEvents(this, entity, bindings); + }; + + // Proxy `unbindEntityEvents` + Marionette.proxyUnbindEntityEvents = function(entity, bindings) { + return Marionette.unbindEntityEvents(this, entity, bindings); + }; + })(Marionette); + - return region; - } - -}); - -// Region Instance Methods -// ----------------------- - -_.extend(Marionette.Region.prototype, Backbone.Events, { - - // Displays a backbone view instance inside of the region. - // Handles calling the `render` method for you. Reads content - // directly from the `el` attribute. Also calls an optional - // `onShow` and `close` method on your view, just after showing - // or just before closing the view, respectively. - show: function(view){ - this.ensureEl(); - - var isViewClosed = view.isClosed || _.isUndefined(view.$el); - var isDifferentView = view !== this.currentView; - - if (isDifferentView) { - this.close(); + // Error + // ----- + + var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number']; + + Marionette.Error = Marionette.extend.call(Error, { + urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/', + + constructor: function(message, options) { + if (_.isObject(message)) { + options = message; + message = options.message; + } else if (!options) { + options = {}; + } + + var error = Error.call(this, message); + _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps)); + + this.captureStackTrace(); + + if (options.url) { + this.url = this.urlRoot + options.url; + } + }, + + captureStackTrace: function() { + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Marionette.Error); + } + }, + + toString: function() { + return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : ''); } - - view.render(); - - if (isDifferentView || isViewClosed) { - this.open(view); + }); + + Marionette.Error.extend = Marionette.extend; + + // Callbacks + // --------- + + // A simple way of managing a collection of callbacks + // and executing them at a later point in time, using jQuery's + // `Deferred` object. + Marionette.Callbacks = function() { + this._deferred = Marionette.Deferred(); + this._callbacks = []; + }; + + _.extend(Marionette.Callbacks.prototype, { + + // Add a callback to be executed. Callbacks added here are + // guaranteed to execute, even if they are added after the + // `run` method is called. + add: function(callback, contextOverride) { + var promise = _.result(this._deferred, 'promise'); + + this._callbacks.push({cb: callback, ctx: contextOverride}); + + promise.then(function(args) { + if (contextOverride) { args.context = contextOverride; } + callback.call(args.context, args.options); + }); + }, + + // Run all registered callbacks with the context specified. + // Additional callbacks can be added after this has been run + // and they will still be executed. + run: function(options, context) { + this._deferred.resolve({ + options: options, + context: context + }); + }, + + // Resets the list of callbacks to be run, allowing the same list + // to be run multiple times - whenever the `run` method is called. + reset: function() { + var callbacks = this._callbacks; + this._deferred = Marionette.Deferred(); + this._callbacks = []; + + _.each(callbacks, function(cb) { + this.add(cb.cb, cb.ctx); + }, this); } - - this.currentView = view; - - Marionette.triggerMethod.call(this, "show", view); - Marionette.triggerMethod.call(view, "show"); - }, - - ensureEl: function(){ - if (!this.$el || this.$el.length === 0){ + }); + + // Controller + // ---------- + + // A multi-purpose object to use as a controller for + // modules and routers, and as a mediator for workflow + // and coordination of other objects, views, and more. + Marionette.Controller = function(options) { + this.options = options || {}; + + if (_.isFunction(this.initialize)) { + this.initialize(this.options); + } + }; + + Marionette.Controller.extend = Marionette.extend; + + // Controller Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Controller.prototype, Backbone.Events, { + destroy: function() { + Marionette._triggerMethod(this, 'before:destroy', arguments); + Marionette._triggerMethod(this, 'destroy', arguments); + + this.stopListening(); + this.off(); + return this; + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // A handy way to merge options onto the instance + mergeOptions: Marionette.mergeOptions, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption + + }); + + // Object + // ------ + + // A Base Class that other Classes should descend from. + // Object borrows many conventions and utilities from Backbone. + Marionette.Object = function(options) { + this.options = _.extend({}, _.result(this, 'options'), options); + + this.initialize.apply(this, arguments); + }; + + Marionette.Object.extend = Marionette.extend; + + // Object Methods + // -------------- + + // Ensure it can trigger events with Backbone.Events + _.extend(Marionette.Object.prototype, Backbone.Events, { + + //this is a noop method intended to be overridden by classes that extend from this base + initialize: function() {}, + + destroy: function() { + this.triggerMethod('before:destroy'); + this.triggerMethod('destroy'); + this.stopListening(); + + return this; + }, + + // Import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod, + + // A handy way to merge options onto the instance + mergeOptions: Marionette.mergeOptions, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `bindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */ + + // Region + // ------ + + // Manage the visual regions of your composite application. See + // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ + + Marionette.Region = Marionette.Object.extend({ + constructor: function(options) { + + // set options temporarily so that we can get `el`. + // options will be overriden by Object.constructor + this.options = options || {}; + this.el = this.getOption('el'); + + // Handle when this.el is passed in as a $ wrapped element. + this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; + + if (!this.el) { + throw new Marionette.Error({ + name: 'NoElError', + message: 'An "el" must be specified for a region.' + }); + } + this.$el = this.getEl(this.el); + Marionette.Object.call(this, options); + }, + + // Displays a backbone view instance inside of the region. + // Handles calling the `render` method for you. Reads content + // directly from the `el` attribute. Also calls an optional + // `onShow` and `onDestroy` method on your view, just after showing + // or just before destroying the view, respectively. + // The `preventDestroy` option can be used to prevent a view from + // the old view being destroyed on show. + // The `forceShow` option can be used to force a view to be + // re-rendered if it's already shown in the region. + show: function(view, options) { + if (!this._ensureElement()) { + return; + } + + this._ensureViewIsIntact(view); + + var showOptions = options || {}; + var isDifferentView = view !== this.currentView; + var preventDestroy = !!showOptions.preventDestroy; + var forceShow = !!showOptions.forceShow; + + // We are only changing the view if there is a current view to change to begin with + var isChangingView = !!this.currentView; + + // Only destroy the current view if we don't want to `preventDestroy` and if + // the view given in the first argument is different than `currentView` + var _shouldDestroyView = isDifferentView && !preventDestroy; + + // Only show the view given in the first argument if it is different than + // the current view or if we want to re-show the view. Note that if + // `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true. + var _shouldShowView = isDifferentView || forceShow; + + if (isChangingView) { + this.triggerMethod('before:swapOut', this.currentView, this, options); + } + + if (this.currentView) { + delete this.currentView._parent; + } + + if (_shouldDestroyView) { + this.empty(); + + // A `destroy` event is attached to the clean up manually removed views. + // We need to detach this event when a new view is going to be shown as it + // is no longer relevant. + } else if (isChangingView && _shouldShowView) { + this.currentView.off('destroy', this.empty, this); + } + + if (_shouldShowView) { + + // We need to listen for if a view is destroyed + // in a way other than through the region. + // If this happens we need to remove the reference + // to the currentView since once a view has been destroyed + // we can not reuse it. + view.once('destroy', this.empty, this); + view.render(); + + view._parent = this; + + if (isChangingView) { + this.triggerMethod('before:swap', view, this, options); + } + + this.triggerMethod('before:show', view, this, options); + Marionette.triggerMethodOn(view, 'before:show', view, this, options); + + if (isChangingView) { + this.triggerMethod('swapOut', this.currentView, this, options); + } + + // An array of views that we're about to display + var attachedRegion = Marionette.isNodeAttached(this.el); + + // The views that we're about to attach to the document + // It's important that we prevent _getNestedViews from being executed unnecessarily + // as it's a potentially-slow method + var displayedViews = []; + + var triggerBeforeAttach = showOptions.triggerBeforeAttach || this.triggerBeforeAttach; + var triggerAttach = showOptions.triggerAttach || this.triggerAttach; + + if (attachedRegion && triggerBeforeAttach) { + displayedViews = this._displayedViews(view); + this._triggerAttach(displayedViews, 'before:'); + } + + this.attachHtml(view); + this.currentView = view; + + if (attachedRegion && triggerAttach) { + displayedViews = this._displayedViews(view); + this._triggerAttach(displayedViews); + } + + if (isChangingView) { + this.triggerMethod('swap', view, this, options); + } + + this.triggerMethod('show', view, this, options); + Marionette.triggerMethodOn(view, 'show', view, this, options); + + return this; + } + + return this; + }, + + triggerBeforeAttach: true, + triggerAttach: true, + + _triggerAttach: function(views, prefix) { + var eventName = (prefix || '') + 'attach'; + _.each(views, function(view) { + Marionette.triggerMethodOn(view, eventName, view, this); + }, this); + }, + + _displayedViews: function(view) { + return _.union([view], _.result(view, '_getNestedViews') || []); + }, + + _ensureElement: function() { + if (!_.isObject(this.el)) { + this.$el = this.getEl(this.el); + this.el = this.$el[0]; + } + + if (!this.$el || this.$el.length === 0) { + if (this.getOption('allowMissingEl')) { + return false; + } else { + throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM'); + } + } + return true; + }, + + _ensureViewIsIntact: function(view) { + if (!view) { + throw new Marionette.Error({ + name: 'ViewNotValid', + message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.' + }); + } + + if (view.isDestroyed) { + throw new Marionette.Error({ + name: 'ViewDestroyedError', + message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.' + }); + } + }, + + // Override this method to change how the region finds the DOM + // element that it manages. Return a jQuery selector object scoped + // to a provided parent el or the document if none exists. + getEl: function(el) { + return Backbone.$(el, Marionette._getValue(this.options.parentEl, this)); + }, + + // Override this method to change how the new view is + // appended to the `$el` that the region is managing + attachHtml: function(view) { + this.$el.contents().detach(); + + this.el.appendChild(view.el); + }, + + // Destroy the current view, if there is one. If there is no + // current view, it does nothing and returns immediately. + empty: function(options) { + var view = this.currentView; + + var preventDestroy = Marionette._getValue(options, 'preventDestroy', this); + // If there is no view in the region + // we should not remove anything + if (!view) { return; } + + view.off('destroy', this.empty, this); + this.triggerMethod('before:empty', view); + if (!preventDestroy) { + this._destroyView(); + } + this.triggerMethod('empty', view); + + // Remove region pointer to the currentView + delete this.currentView; + + if (preventDestroy) { + this.$el.contents().detach(); + } + + return this; + }, + + // call 'destroy' or 'remove', depending on which is found + // on the view (if showing a raw Backbone view or a Marionette View) + _destroyView: function() { + var view = this.currentView; + + if (view.destroy && !view.isDestroyed) { + view.destroy(); + } else if (view.remove) { + view.remove(); + + // appending isDestroyed to raw Backbone View allows regions + // to throw a ViewDestroyedError for this view + view.isDestroyed = true; + } + }, + + // Attach an existing view to the region. This + // will not call `render` or `onShow` for the new view, + // and will not replace the current HTML for the `el` + // of the region. + attachView: function(view) { + this.currentView = view; + return this; + }, + + // Checks whether a view is currently present within + // the region. Returns `true` if there is and `false` if + // no view is present. + hasView: function() { + return !!this.currentView; + }, + + // Reset the region by destroying any existing view and + // clearing out the cached `$el`. The next time a view + // is shown via this region, the region will re-query the + // DOM for the region's `el`. + reset: function() { + this.empty(); + + if (this.$el) { + this.el = this.$el.selector; + } + + delete this.$el; + return this; } + }, - - // Override this method to change how the region finds the - // DOM element that it manages. Return a jQuery selector object. - getEl: function(selector){ - return Marionette.$(selector); - }, - - // Override this method to change how the new view is - // appended to the `$el` that the region is managing - open: function(view){ - this.$el.empty().append(view.el); - }, - - // Close the current view, if there is one. If there is no - // current view, it does nothing and returns immediately. - close: function(){ - var view = this.currentView; - if (!view || view.isClosed){ return; } - - // call 'close' or 'remove', depending on which is found - if (view.close) { view.close(); } - else if (view.remove) { view.remove(); } - - Marionette.triggerMethod.call(this, "close", view); - - delete this.currentView; - }, - - // Attach an existing view to the region. This - // will not call `render` or `onShow` for the new view, - // and will not replace the current HTML for the `el` - // of the region. - attachView: function(view){ - this.currentView = view; - }, - - // Reset the region by closing any existing view and - // clearing out the cached `$el`. The next time a view - // is shown via this region, the region will re-query the - // DOM for the region's `el`. - reset: function(){ - this.close(); - delete this.$el; - } -}); - -// Copy the `extend` function used by Backbone's classes -Marionette.Region.extend = Marionette.extend; - -// Marionette.RegionManager -// ------------------------ -// -// Manage one or more related `Marionette.Region` objects. -Marionette.RegionManager = (function(Marionette){ - - var RegionManager = Marionette.Controller.extend({ - constructor: function(options){ + + // Static Methods + { + + // Build an instance of a region by passing in a configuration object + // and a default region class to use if none is specified in the config. + // + // The config object should either be a string as a jQuery DOM selector, + // a Region class directly, or an object literal that specifies a selector, + // a custom regionClass, and any options to be supplied to the region: + // + // ```js + // { + // selector: "#foo", + // regionClass: MyCustomRegion, + // allowMissingEl: false + // } + // ``` + // + buildRegion: function(regionConfig, DefaultRegionClass) { + if (_.isString(regionConfig)) { + return this._buildRegionFromSelector(regionConfig, DefaultRegionClass); + } + + if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) { + return this._buildRegionFromObject(regionConfig, DefaultRegionClass); + } + + if (_.isFunction(regionConfig)) { + return this._buildRegionFromRegionClass(regionConfig); + } + + throw new Marionette.Error({ + message: 'Improper region configuration type.', + url: 'marionette.region.html#region-configuration-types' + }); + }, + + // Build the region from a string selector like '#foo-region' + _buildRegionFromSelector: function(selector, DefaultRegionClass) { + return new DefaultRegionClass({el: selector}); + }, + + // Build the region from a configuration object + // ```js + // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false } + // ``` + _buildRegionFromObject: function(regionConfig, DefaultRegionClass) { + var RegionClass = regionConfig.regionClass || DefaultRegionClass; + var options = _.omit(regionConfig, 'selector', 'regionClass'); + + if (regionConfig.selector && !options.el) { + options.el = regionConfig.selector; + } + + return new RegionClass(options); + }, + + // Build the region directly from a given `RegionClass` + _buildRegionFromRegionClass: function(RegionClass) { + return new RegionClass(); + } + }); + + // Region Manager + // -------------- + + // Manage one or more related `Marionette.Region` objects. + Marionette.RegionManager = Marionette.Controller.extend({ + constructor: function(options) { this._regions = {}; - Marionette.Controller.prototype.constructor.call(this, options); + this.length = 0; + + Marionette.Controller.call(this, options); + + this.addRegions(this.getOption('regions')); }, - - // Add multiple regions using an object literal, where + + // Add multiple regions using an object literal or a + // function that returns an object literal, where // each key becomes the region name, and each value is // the region definition. - addRegions: function(regionDefinitions, defaults){ - var regions = {}; - - _.each(regionDefinitions, function(definition, name){ - if (_.isString(definition)){ - definition = { selector: definition }; + addRegions: function(regionDefinitions, defaults) { + regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments); + + return _.reduce(regionDefinitions, function(regions, definition, name) { + if (_.isString(definition)) { + definition = {selector: definition}; } - - if (definition.selector){ + if (definition.selector) { definition = _.defaults({}, definition, defaults); } - - var region = this.addRegion(name, definition); - regions[name] = region; - }, this); - - return regions; + + regions[name] = this.addRegion(name, definition); + return regions; + }, {}, this); }, - + // Add an individual region to the region manager, // and return the region instance - addRegion: function(name, definition){ + addRegion: function(name, definition) { var region; - - var isObject = _.isObject(definition); - var isString = _.isString(definition); - var hasSelector = !!definition.selector; - - if (isString || (isObject && hasSelector)){ - region = Marionette.Region.buildRegion(definition, Marionette.Region); - } else if (_.isFunction(definition)){ - region = Marionette.Region.buildRegion(definition, Marionette.Region); - } else { + + if (definition instanceof Marionette.Region) { region = definition; + } else { + region = Marionette.Region.buildRegion(definition, Marionette.Region); } - + + this.triggerMethod('before:add:region', name, region); + + region._parent = this; this._store(name, region); - this.triggerMethod("region:add", name, region); + + this.triggerMethod('add:region', name, region); return region; }, - + // Get a region by name - get: function(name){ + get: function(name) { return this._regions[name]; }, - + + // Gets all the regions contained within + // the `regionManager` instance. + getRegions: function() { + return _.clone(this._regions); + }, + // Remove a region by name - removeRegion: function(name){ + removeRegion: function(name) { var region = this._regions[name]; this._remove(name, region); + + return region; }, - - // Close all regions in the region manager, and + + // Empty all regions in the region manager, and // remove them - removeRegions: function(){ - _.each(this._regions, function(region, name){ + removeRegions: function() { + var regions = this.getRegions(); + _.each(this._regions, function(region, name) { this._remove(name, region); }, this); + + return regions; }, - - // Close all regions in the region manager, but + + // Empty all regions in the region manager, but // leave them attached - closeRegions: function(){ - _.each(this._regions, function(region, name){ - region.close(); - }, this); + emptyRegions: function() { + var regions = this.getRegions(); + _.invoke(regions, 'empty'); + return regions; }, - - // Close all regions and shut down the region + + // Destroy all regions and shut down the region // manager entirely - close: function(){ + destroy: function() { this.removeRegions(); - Marionette.Controller.prototype.close.apply(this, arguments); + return Marionette.Controller.prototype.destroy.apply(this, arguments); }, - + // internal method to store regions - _store: function(name, region){ + _store: function(name, region) { + if (!this._regions[name]) { + this.length++; + } + this._regions[name] = region; - this._setLength(); }, - + // internal method to remove a region - _remove: function(name, region){ - region.close(); + _remove: function(name, region) { + this.triggerMethod('before:remove:region', name, region); + region.empty(); + region.stopListening(); + + delete region._parent; delete this._regions[name]; - this._setLength(); - this.triggerMethod("region:remove", name, region); - }, - - // set the number of regions current held - _setLength: function(){ - this.length = _.size(this._regions); + this.length--; + this.triggerMethod('remove:region', name, region); } - }); + + Marionette.actAsCollection(Marionette.RegionManager.prototype, '_regions'); + - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - RegionManager.prototype[method] = function() { - var regions = _.values(this._regions); - var args = [regions].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - return RegionManager; -})(Marionette); - - -// Template Cache -// -------------- - -// Manage templates stored in `<script>` blocks, -// caching them for faster access. -Marionette.TemplateCache = function(templateId){ - this.templateId = templateId; -}; - -// TemplateCache object-level methods. Manage the template -// caches from these method calls instead of creating -// your own TemplateCache instances -_.extend(Marionette.TemplateCache, { - templateCaches: {}, - - // Get the specified template by id. Either - // retrieves the cached version, or loads it - // from the DOM. - get: function(templateId){ - var cachedTemplate = this.templateCaches[templateId]; - - if (!cachedTemplate){ - cachedTemplate = new Marionette.TemplateCache(templateId); - this.templateCaches[templateId] = cachedTemplate; - } - - return cachedTemplate.load(); - }, - - // Clear templates from the cache. If no arguments - // are specified, clears all templates: - // `clear()` - // - // If arguments are specified, clears each of the - // specified templates from the cache: - // `clear("#t1", "#t2", "...")` - clear: function(){ - var i; - var args = slice.call(arguments); - var length = args.length; - - if (length > 0){ - for(i=0; i<length; i++){ - delete this.templateCaches[args[i]]; + // Template Cache + // -------------- + + // Manage templates stored in `<script>` blocks, + // caching them for faster access. + Marionette.TemplateCache = function(templateId) { + this.templateId = templateId; + }; + + // TemplateCache object-level methods. Manage the template + // caches from these method calls instead of creating + // your own TemplateCache instances + _.extend(Marionette.TemplateCache, { + templateCaches: {}, + + // Get the specified template by id. Either + // retrieves the cached version, or loads it + // from the DOM. + get: function(templateId, options) { + var cachedTemplate = this.templateCaches[templateId]; + + if (!cachedTemplate) { + cachedTemplate = new Marionette.TemplateCache(templateId); + this.templateCaches[templateId] = cachedTemplate; + } + + return cachedTemplate.load(options); + }, + + // Clear templates from the cache. If no arguments + // are specified, clears all templates: + // `clear()` + // + // If arguments are specified, clears each of the + // specified templates from the cache: + // `clear("#t1", "#t2", "...")` + clear: function() { + var i; + var args = _.toArray(arguments); + var length = args.length; + + if (length > 0) { + for (i = 0; i < length; i++) { + delete this.templateCaches[args[i]]; + } + } else { + this.templateCaches = {}; } - } else { - this.templateCaches = {}; } - } -}); - -// TemplateCache instance methods, allowing each -// template cache object to manage its own state -// and know whether or not it has been loaded -_.extend(Marionette.TemplateCache.prototype, { - - // Internal method to load the template - load: function(){ - // Guard clause to prevent loading this template more than once - if (this.compiledTemplate){ + }); + + // TemplateCache instance methods, allowing each + // template cache object to manage its own state + // and know whether or not it has been loaded + _.extend(Marionette.TemplateCache.prototype, { + + // Internal method to load the template + load: function(options) { + // Guard clause to prevent loading this template more than once + if (this.compiledTemplate) { + return this.compiledTemplate; + } + + // Load the template and compile it + var template = this.loadTemplate(this.templateId, options); + this.compiledTemplate = this.compileTemplate(template, options); + return this.compiledTemplate; + }, + + // Load a template from the DOM, by default. Override + // this method to provide your own template retrieval + // For asynchronous loading with AMD/RequireJS, consider + // using a template-loader plugin as described here: + // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs + loadTemplate: function(templateId, options) { + var template = Backbone.$(templateId).html(); + + if (!template || template.length === 0) { + throw new Marionette.Error({ + name: 'NoTemplateError', + message: 'Could not find template: "' + templateId + '"' + }); + } + + return template; + }, + + // Pre-compile the template before caching it. Override + // this method if you do not need to pre-compile a template + // (JST / RequireJS for example) or if you want to change + // the template engine used (Handebars, etc). + compileTemplate: function(rawTemplate, options) { + return _.template(rawTemplate, options); } - - // Load the template and compile it - var template = this.loadTemplate(this.templateId); - this.compiledTemplate = this.compileTemplate(template); - - return this.compiledTemplate; - }, - - // Load a template from the DOM, by default. Override - // this method to provide your own template retrieval - // For asynchronous loading with AMD/RequireJS, consider - // using a template-loader plugin as described here: - // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs - loadTemplate: function(templateId){ - var template = Marionette.$(templateId).html(); - - if (!template || template.length === 0){ - throwError("Could not find template: '" + templateId + "'", "NoTemplateError"); - } - - return template; - }, - - // Pre-compile the template before caching it. Override - // this method if you do not need to pre-compile a template - // (JST / RequireJS for example) or if you want to change - // the template engine used (Handebars, etc). - compileTemplate: function(rawTemplate){ - return _.template(rawTemplate); - } -}); - - -// Renderer -// -------- - -// Render a template with data by passing in the template -// selector and the data to render. -Marionette.Renderer = { - - // Render a template with data. The `template` parameter is - // passed to the `TemplateCache` object to retrieve the - // template function. Override this method to provide your own - // custom rendering and template handling for all of Marionette. - render: function(template, data){ - - if (!template) { - throwError("Cannot render the template since it's false, null or undefined.", "TemplateNotFoundError"); - } - - var templateFunc; - if (typeof template === "function"){ - templateFunc = template; - } else { - templateFunc = Marionette.TemplateCache.get(template); - } - - return templateFunc(data); - } -}; - - - -// Marionette.View -// --------------- - -// The core view type that other Marionette views extend from. -Marionette.View = Backbone.View.extend({ - - constructor: function(options){ - _.bindAll(this, "render"); - - // this exposes view options to the view initializer - // this is a backfill since backbone removed the assignment - // of this.options - // at some point however this may be removed - this.options = _.extend({}, _.result(this, 'options'), _.isFunction(options) ? options.call(this) : options); - - // parses out the @ui DSL for events - this.events = this.normalizeUIKeys(_.result(this, 'events')); - Backbone.View.prototype.constructor.apply(this, arguments); - - Marionette.MonitorDOMRefresh(this); - this.listenTo(this, "show", this.onShowCalled); - }, - - // import the "triggerMethod" to trigger events with corresponding - // methods if the method exists - triggerMethod: Marionette.triggerMethod, - - // Imports the "normalizeMethods" to transform hashes of - // events=>function references/names to a hash of events=>function references - normalizeMethods: Marionette.normalizeMethods, - - // Get the template for this view - // instance. You can set a `template` attribute in the view - // definition or pass a `template: "whatever"` parameter in - // to the constructor options. - getTemplate: function(){ - return Marionette.getOption(this, "template"); - }, - - // Mix in template helper methods. Looks for a - // `templateHelpers` attribute, which can either be an - // object literal, or a function that returns an object - // literal. All methods and attributes from this object - // are copies to the object passed in. - mixinTemplateHelpers: function(target){ - target = target || {}; - var templateHelpers = Marionette.getOption(this, "templateHelpers"); - if (_.isFunction(templateHelpers)){ - templateHelpers = templateHelpers.call(this); - } - return _.extend(target, templateHelpers); - }, - - // allows for the use of the @ui. syntax within - // a given key for triggers and events - // swaps the @ui with the associated selector - normalizeUIKeys: function(hash) { - var _this = this; - if (typeof(hash) === "undefined") { - return; + }); + + // Renderer + // -------- + + // Render a template with data by passing in the template + // selector and the data to render. + Marionette.Renderer = { + + // Render a template with data. The `template` parameter is + // passed to the `TemplateCache` object to retrieve the + // template function. Override this method to provide your own + // custom rendering and template handling for all of Marionette. + render: function(template, data) { + if (!template) { + throw new Marionette.Error({ + name: 'TemplateNotFoundError', + message: 'Cannot render the template since its false, null or undefined.' + }); + } + + var templateFunc = _.isFunction(template) ? template : Marionette.TemplateCache.get(template); + + return templateFunc(data); } + }; + - _.each(_.keys(hash), function(v) { - var pattern = /@ui.[a-zA-Z_$0-9]*/g; - if (v.match(pattern)) { - hash[v.replace(pattern, function(r) { - return _.result(_this, "ui")[r.slice(4)]; - })] = hash[v]; - delete hash[v]; + /* jshint maxlen: 114, nonew: false */ + // View + // ---- + + // The core view class that other Marionette views extend from. + Marionette.View = Backbone.View.extend({ + isDestroyed: false, + + constructor: function(options) { + _.bindAll(this, 'render'); + + options = Marionette._getValue(options, this); + + // this exposes view options to the view initializer + // this is a backfill since backbone removed the assignment + // of this.options + // at some point however this may be removed + this.options = _.extend({}, _.result(this, 'options'), options); + + this._behaviors = Marionette.Behaviors(this); + + Backbone.View.call(this, this.options); + + Marionette.MonitorDOMRefresh(this); + }, + + // Get the template for this view + // instance. You can set a `template` attribute in the view + // definition or pass a `template: "whatever"` parameter in + // to the constructor options. + getTemplate: function() { + return this.getOption('template'); + }, + + // Serialize a model by returning its attributes. Clones + // the attributes to allow modification. + serializeModel: function(model) { + return model.toJSON.apply(model, _.rest(arguments)); + }, + + // Mix in template helper methods. Looks for a + // `templateHelpers` attribute, which can either be an + // object literal, or a function that returns an object + // literal. All methods and attributes from this object + // are copies to the object passed in. + mixinTemplateHelpers: function(target) { + target = target || {}; + var templateHelpers = this.getOption('templateHelpers'); + templateHelpers = Marionette._getValue(templateHelpers, this); + return _.extend(target, templateHelpers); + }, + + // normalize the keys of passed hash with the views `ui` selectors. + // `{"@ui.foo": "bar"}` + normalizeUIKeys: function(hash) { + var uiBindings = _.result(this, '_uiBindings'); + return Marionette.normalizeUIKeys(hash, uiBindings || _.result(this, 'ui')); + }, + + // normalize the values of passed hash with the views `ui` selectors. + // `{foo: "@ui.bar"}` + normalizeUIValues: function(hash, properties) { + var ui = _.result(this, 'ui'); + var uiBindings = _.result(this, '_uiBindings'); + return Marionette.normalizeUIValues(hash, uiBindings || ui, properties); + }, + + // Configure `triggers` to forward DOM events to view + // events. `triggers: {"click .foo": "do:foo"}` + configureTriggers: function() { + if (!this.triggers) { return; } + + // Allow `triggers` to be configured as a function + var triggers = this.normalizeUIKeys(_.result(this, 'triggers')); + + // Configure the triggers, prevent default + // action and stop propagation of DOM events + return _.reduce(triggers, function(events, value, key) { + events[key] = this._buildViewTrigger(value); + return events; + }, {}, this); + }, + + // Overriding Backbone.View's delegateEvents to handle + // the `triggers`, `modelEvents`, and `collectionEvents` configuration + delegateEvents: function(events) { + this._delegateDOMEvents(events); + this.bindEntityEvents(this.model, this.getOption('modelEvents')); + this.bindEntityEvents(this.collection, this.getOption('collectionEvents')); + + _.each(this._behaviors, function(behavior) { + behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents')); + behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents')); + }, this); + + return this; + }, + + // internal method to delegate DOM events and triggers + _delegateDOMEvents: function(eventsArg) { + var events = Marionette._getValue(eventsArg || this.events, this); + + // normalize ui keys + events = this.normalizeUIKeys(events); + if (_.isUndefined(eventsArg)) {this.events = events;} + + var combinedEvents = {}; + + // look up if this view has behavior events + var behaviorEvents = _.result(this, 'behaviorEvents') || {}; + var triggers = this.configureTriggers(); + var behaviorTriggers = _.result(this, 'behaviorTriggers') || {}; + + // behavior events will be overriden by view events and or triggers + _.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers); + + Backbone.View.prototype.delegateEvents.call(this, combinedEvents); + }, + + // Overriding Backbone.View's undelegateEvents to handle unbinding + // the `triggers`, `modelEvents`, and `collectionEvents` config + undelegateEvents: function() { + Backbone.View.prototype.undelegateEvents.apply(this, arguments); + + this.unbindEntityEvents(this.model, this.getOption('modelEvents')); + this.unbindEntityEvents(this.collection, this.getOption('collectionEvents')); + + _.each(this._behaviors, function(behavior) { + behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents')); + behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents')); + }, this); + + return this; + }, + + // Internal helper method to verify whether the view hasn't been destroyed + _ensureViewIsIntact: function() { + if (this.isDestroyed) { + throw new Marionette.Error({ + name: 'ViewDestroyedError', + message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.' + }); } - }); - - return hash; - }, - - // Configure `triggers` to forward DOM events to view - // events. `triggers: {"click .foo": "do:foo"}` - configureTriggers: function(){ - if (!this.triggers) { return; } - - var triggerEvents = {}; - - // Allow `triggers` to be configured as a function - var triggers = this.normalizeUIKeys(_.result(this, "triggers")); - - // Configure the triggers, prevent default - // action and stop propagation of DOM events - _.each(triggers, function(value, key){ - - var hasOptions = _.isObject(value); - var eventName = hasOptions ? value.event : value; - - // build the event handler function for the DOM event - triggerEvents[key] = function(e){ - - // stop the event in its tracks + }, + + // Default `destroy` implementation, for removing a view from the + // DOM and unbinding it. Regions will call this method + // for you. You can specify an `onDestroy` method in your view to + // add custom code that is called after the view is destroyed. + destroy: function() { + if (this.isDestroyed) { return this; } + + var args = _.toArray(arguments); + + this.triggerMethod.apply(this, ['before:destroy'].concat(args)); + + // mark as destroyed before doing the actual destroy, to + // prevent infinite loops within "destroy" event handlers + // that are trying to destroy other views + this.isDestroyed = true; + this.triggerMethod.apply(this, ['destroy'].concat(args)); + + // unbind UI elements + this.unbindUIElements(); + + this.isRendered = false; + + // remove the view from the DOM + this.remove(); + + // Call destroy on each behavior after + // destroying the view. + // This unbinds event listeners + // that behaviors have registered for. + _.invoke(this._behaviors, 'destroy', args); + + return this; + }, + + bindUIElements: function() { + this._bindUIElements(); + _.invoke(this._behaviors, this._bindUIElements); + }, + + // This method binds the elements specified in the "ui" hash inside the view's code with + // the associated jQuery selectors. + _bindUIElements: function() { + if (!this.ui) { return; } + + // store the ui hash in _uiBindings so they can be reset later + // and so re-rendering the view will be able to find the bindings + if (!this._uiBindings) { + this._uiBindings = this.ui; + } + + // get the bindings result, as a function or otherwise + var bindings = _.result(this, '_uiBindings'); + + // empty the ui so we don't have anything to start with + this.ui = {}; + + // bind each of the selectors + _.each(bindings, function(selector, key) { + this.ui[key] = this.$(selector); + }, this); + }, + + // This method unbinds the elements specified in the "ui" hash + unbindUIElements: function() { + this._unbindUIElements(); + _.invoke(this._behaviors, this._unbindUIElements); + }, + + _unbindUIElements: function() { + if (!this.ui || !this._uiBindings) { return; } + + // delete all of the existing ui bindings + _.each(this.ui, function($el, name) { + delete this.ui[name]; + }, this); + + // reset the ui element to the original bindings configuration + this.ui = this._uiBindings; + delete this._uiBindings; + }, + + // Internal method to create an event handler for a given `triggerDef` like + // 'click:foo' + _buildViewTrigger: function(triggerDef) { + var hasOptions = _.isObject(triggerDef); + + var options = _.defaults({}, (hasOptions ? triggerDef : {}), { + preventDefault: true, + stopPropagation: true + }); + + var eventName = hasOptions ? options.event : triggerDef; + + return function(e) { if (e) { - var prevent = e.preventDefault; - var stop = e.stopPropagation; - - var shouldPrevent = hasOptions ? value.preventDefault : prevent; - var shouldStop = hasOptions ? value.stopPropagation : stop; - - if (shouldPrevent && prevent) { prevent.apply(e); } - if (shouldStop && stop) { stop.apply(e); } + if (e.preventDefault && options.preventDefault) { + e.preventDefault(); + } + + if (e.stopPropagation && options.stopPropagation) { + e.stopPropagation(); + } } - - // build the args for the event + var args = { view: this, model: this.model, collection: this.collection }; - - // trigger the event + this.triggerMethod(eventName, args); }; - - }, this); - - return triggerEvents; - }, - - // Overriding Backbone.View's delegateEvents to handle - // the `triggers`, `modelEvents`, and `collectionEvents` configuration - delegateEvents: function(events){ - this._delegateDOMEvents(events); - Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); - Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); - }, - - // internal method to delegate DOM events and triggers - _delegateDOMEvents: function(events){ - events = events || this.events; - if (_.isFunction(events)){ events = events.call(this); } - - var combinedEvents = {}; - var triggers = this.configureTriggers(); - _.extend(combinedEvents, events, triggers); - - Backbone.View.prototype.delegateEvents.call(this, combinedEvents); - }, - - // Overriding Backbone.View's undelegateEvents to handle unbinding - // the `triggers`, `modelEvents`, and `collectionEvents` config - undelegateEvents: function(){ - var args = Array.prototype.slice.call(arguments); - Backbone.View.prototype.undelegateEvents.apply(this, args); - - Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); - Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); - }, - - // Internal method, handles the `show` event. - onShowCalled: function(){}, - - // Default `close` implementation, for removing a view from the - // DOM and unbinding it. Regions will call this method - // for you. You can specify an `onClose` method in your view to - // add custom code that is called after the view is closed. - close: function(){ - if (this.isClosed) { return; } - - // allow the close to be stopped by returning `false` - // from the `onBeforeClose` method - var shouldClose = this.triggerMethod("before:close"); - if (shouldClose === false){ - return; - } - - // mark as closed before doing the actual close, to - // prevent infinite loops within "close" event handlers - // that are trying to close other views - this.isClosed = true; - this.triggerMethod("close"); - - // unbind UI elements - this.unbindUIElements(); - - // remove the view from the DOM - this.remove(); - }, - - // This method binds the elements specified in the "ui" hash inside the view's code with - // the associated jQuery selectors. - bindUIElements: function(){ - if (!this.ui) { return; } - - // store the ui hash in _uiBindings so they can be reset later - // and so re-rendering the view will be able to find the bindings - if (!this._uiBindings){ - this._uiBindings = this.ui; - } - - // get the bindings result, as a function or otherwise - var bindings = _.result(this, "_uiBindings"); - - // empty the ui so we don't have anything to start with - this.ui = {}; - - // bind each of the selectors - _.each(_.keys(bindings), function(key) { - var selector = bindings[key]; - this.ui[key] = this.$(selector); - }, this); - }, - - // This method unbinds the elements specified in the "ui" hash - unbindUIElements: function(){ - if (!this.ui || !this._uiBindings){ return; } - - // delete all of the existing ui bindings - _.each(this.ui, function($el, name){ - delete this.ui[name]; - }, this); - - // reset the ui element to the original bindings configuration - this.ui = this._uiBindings; - delete this._uiBindings; - } -}); - -// Item View -// --------- - -// A single item view implementation that contains code for rendering -// with underscore.js templates, serializing the view's model or collection, -// and calling several methods on extended views, such as `onRender`. -Marionette.ItemView = Marionette.View.extend({ - - // Setting up the inheritance chain which allows changes to - // Marionette.View.prototype.constructor which allows overriding - constructor: function(){ - Marionette.View.prototype.constructor.apply(this, arguments); - }, - - // Serialize the model or collection for the view. If a model is - // found, `.toJSON()` is called. If a collection is found, `.toJSON()` - // is also called, but is used to populate an `items` array in the - // resulting data. If both are found, defaults to the model. - // You can override the `serializeData` method in your own view - // definition, to provide custom serialization for your view's data. - serializeData: function(){ - var data = {}; - - if (this.model) { - data = this.model.toJSON(); - } - else if (this.collection) { - data = { items: this.collection.toJSON() }; - } - - return data; - }, - - // Render the view, defaulting to underscore.js templates. - // You can override this in your view definition to provide - // a very specific rendering for your view. In general, though, - // you should override the `Marionette.Renderer` object to - // change how Marionette renders views. - render: function(){ - this.isClosed = false; - - this.triggerMethod("before:render", this); - this.triggerMethod("item:before:render", this); - - var data = this.serializeData(); - data = this.mixinTemplateHelpers(data); - - var template = this.getTemplate(); - var html = Marionette.Renderer.render(template, data); - - this.$el.html(html); - this.bindUIElements(); - - this.triggerMethod("render", this); - this.triggerMethod("item:rendered", this); - - return this; - }, - - // Override the default close event to add a few - // more events that are triggered. - close: function(){ - if (this.isClosed){ return; } - - this.triggerMethod('item:before:close'); - - Marionette.View.prototype.close.apply(this, arguments); - - this.triggerMethod('item:closed'); - } -}); - -// Collection View -// --------------- - -// A view that iterates over a Backbone.Collection -// and renders an individual ItemView for each model. -Marionette.CollectionView = Marionette.View.extend({ - // used as the prefix for item view events - // that are forwarded through the collectionview - itemViewEventPrefix: "itemview", - - // constructor - constructor: function(options){ - this._initChildViewStorage(); - - Marionette.View.prototype.constructor.apply(this, arguments); - - this._initialEvents(); - this.initRenderBuffer(); - }, - - // Instead of inserting elements one by one into the page, - // it's much more performant to insert elements into a document - // fragment and then insert that document fragment into the page - initRenderBuffer: function() { - this.elBuffer = document.createDocumentFragment(); - this._bufferedChildren = []; - }, - - startBuffering: function() { - this.initRenderBuffer(); - this.isBuffering = true; - }, - - endBuffering: function() { - this.isBuffering = false; - this.appendBuffer(this, this.elBuffer); - this._triggerShowBufferedChildren(); - this.initRenderBuffer(); - }, - - _triggerShowBufferedChildren: function () { - if (this._isShown) { - _.each(this._bufferedChildren, function (child) { - Marionette.triggerMethod.call(child, "show"); + }, + + setElement: function() { + var ret = Backbone.View.prototype.setElement.apply(this, arguments); + + // proxy behavior $el to the view's $el. + // This is needed because a view's $el proxy + // is not set until after setElement is called. + _.invoke(this._behaviors, 'proxyViewProperties', this); + + return ret; + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: function() { + var ret = Marionette._triggerMethod(this, arguments); + + this._triggerEventOnBehaviors(arguments); + this._triggerEventOnParentLayout(arguments[0], _.rest(arguments)); + + return ret; + }, + + _triggerEventOnBehaviors: function(args) { + var triggerMethod = Marionette._triggerMethod; + var behaviors = this._behaviors; + // Use good ol' for as this is a very hot function + for (var i = 0, length = behaviors && behaviors.length; i < length; i++) { + triggerMethod(behaviors[i], args); + } + }, + + _triggerEventOnParentLayout: function(eventName, args) { + var layoutView = this._parentLayoutView(); + if (!layoutView) { + return; + } + + // invoke triggerMethod on parent view + var eventPrefix = Marionette.getOption(layoutView, 'childViewEventPrefix'); + var prefixedEventName = eventPrefix + ':' + eventName; + + Marionette._triggerMethod(layoutView, [prefixedEventName, this].concat(args)); + + // call the parent view's childEvents handler + var childEvents = Marionette.getOption(layoutView, 'childEvents'); + var normalizedChildEvents = layoutView.normalizeMethods(childEvents); + + if (!!normalizedChildEvents && _.isFunction(normalizedChildEvents[eventName])) { + normalizedChildEvents[eventName].apply(layoutView, [this].concat(args)); + } + }, + + // This method returns any views that are immediate + // children of this view + _getImmediateChildren: function() { + return []; + }, + + // Returns an array of every nested view within this view + _getNestedViews: function() { + var children = this._getImmediateChildren(); + + if (!children.length) { return children; } + + return _.reduce(children, function(memo, view) { + if (!view._getNestedViews) { return memo; } + return memo.concat(view._getNestedViews()); + }, children); + }, + + // Internal utility for building an ancestor + // view tree list. + _getAncestors: function() { + var ancestors = []; + var parent = this._parent; + + while (parent) { + ancestors.push(parent); + parent = parent._parent; + } + + return ancestors; + }, + + // Returns the containing parent view. + _parentLayoutView: function() { + var ancestors = this._getAncestors(); + return _.find(ancestors, function(parent) { + return parent instanceof Marionette.LayoutView; }); - this._bufferedChildren = []; - } - }, - - // Configured the initial events that the collection view - // binds to. - _initialEvents: function(){ - if (this.collection){ - this.listenTo(this.collection, "add", this.addChildView); - this.listenTo(this.collection, "remove", this.removeItemView); - this.listenTo(this.collection, "reset", this.render); - } - }, - - // Handle a child item added to the collection - addChildView: function(item, collection, options){ - this.closeEmptyView(); - var ItemView = this.getItemView(item); - var index = this.collection.indexOf(item); - this.addItemView(item, ItemView, index); - }, - - // Override from `Marionette.View` to guarantee the `onShow` method - // of child views is called. - onShowCalled: function(){ - this.children.each(function(child){ - Marionette.triggerMethod.call(child, "show"); - }); - }, - - // Internal method to trigger the before render callbacks - // and events - triggerBeforeRender: function(){ - this.triggerMethod("before:render", this); - this.triggerMethod("collection:before:render", this); - }, - - // Internal method to trigger the rendered callbacks and - // events - triggerRendered: function(){ - this.triggerMethod("render", this); - this.triggerMethod("collection:rendered", this); - }, - - // Render the collection of items. Override this method to - // provide your own implementation of a render function for - // the collection view. - render: function(){ - this.isClosed = false; - this.triggerBeforeRender(); - this._renderChildren(); - this.triggerRendered(); - return this; - }, - - // Internal method. Separated so that CompositeView can have - // more control over events being triggered, around the rendering - // process - _renderChildren: function(){ - this.startBuffering(); - - this.closeEmptyView(); - this.closeChildren(); - - if (!this.isEmpty(this.collection)) { - this.showCollection(); - } else { - this.showEmptyView(); - } - - this.endBuffering(); - }, - - // Internal method to loop through each item in the - // collection view and show it - showCollection: function(){ - var ItemView; - this.collection.each(function(item, index){ - ItemView = this.getItemView(item); - this.addItemView(item, ItemView, index); - }, this); - }, - - // Internal method to show an empty view in place of - // a collection of item views, when the collection is - // empty - showEmptyView: function(){ - var EmptyView = this.getEmptyView(); - - if (EmptyView && !this._showingEmptyView){ - this._showingEmptyView = true; - var model = new Backbone.Model(); - this.addItemView(model, EmptyView, 0); - } - }, - - // Internal method to close an existing emptyView instance - // if one exists. Called when a collection view has been - // rendered empty, and then an item is added to the collection. - closeEmptyView: function(){ - if (this._showingEmptyView){ - this.closeChildren(); - delete this._showingEmptyView; - } - }, - - // Retrieve the empty view type - getEmptyView: function(){ - return Marionette.getOption(this, "emptyView"); - }, - - // Retrieve the itemView type, either from `this.options.itemView` - // or from the `itemView` in the object definition. The "options" - // takes precedence. - getItemView: function(item){ - var itemView = Marionette.getOption(this, "itemView"); - - if (!itemView){ - throwError("An `itemView` must be specified", "NoItemViewError"); - } - - return itemView; - }, - - // Render the child item's view and add it to the - // HTML for the collection view. - addItemView: function(item, ItemView, index){ - // get the itemViewOptions if any were specified - var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); - if (_.isFunction(itemViewOptions)){ - itemViewOptions = itemViewOptions.call(this, item, index); - } - - // build the view - var view = this.buildItemView(item, ItemView, itemViewOptions); - - // set up the child view event forwarding - this.addChildViewEventForwarding(view); - - // this view is about to be added - this.triggerMethod("before:item:added", view); - - // Store the child view itself so we can properly - // remove and/or close it later - this.children.add(view); - - // Render it and show it - this.renderItemView(view, index); - - // call the "show" method if the collection view - // has already been shown - if (this._isShown && !this.isBuffering){ - Marionette.triggerMethod.call(view, "show"); - } - - // this view was added - this.triggerMethod("after:item:added", view); - - return view; - }, - - // Set up the child view event forwarding. Uses an "itemview:" - // prefix in front of all forwarded events. - addChildViewEventForwarding: function(view){ - var prefix = Marionette.getOption(this, "itemViewEventPrefix"); - - // Forward all child item view events through the parent, - // prepending "itemview:" to the event name - this.listenTo(view, "all", function(){ - var args = slice.call(arguments); - var rootEvent = args[0]; - var itemEvents = this.normalizeMethods(this.getItemEvents()); - - args[0] = prefix + ":" + rootEvent; - args.splice(1, 0, view); - - // call collectionView itemEvent if defined - if (typeof itemEvents !== "undefined" && _.isFunction(itemEvents[rootEvent])) { - itemEvents[rootEvent].apply(this, args); + }, + + // Imports the "normalizeMethods" to transform hashes of + // events=>function references/names to a hash of events=>function references + normalizeMethods: Marionette.normalizeMethods, + + // A handy way to merge passed-in options onto the instance + mergeOptions: Marionette.mergeOptions, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + // Proxy `bindEntityEvents` to enable binding view's events from another entity. + bindEntityEvents: Marionette.proxyBindEntityEvents, + + // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + // Item View + // --------- + + // A single item view implementation that contains code for rendering + // with underscore.js templates, serializing the view's model or collection, + // and calling several methods on extended views, such as `onRender`. + Marionette.ItemView = Marionette.View.extend({ + + // Setting up the inheritance chain which allows changes to + // Marionette.View.prototype.constructor which allows overriding + constructor: function() { + Marionette.View.apply(this, arguments); + }, + + // Serialize the model or collection for the view. If a model is + // found, the view's `serializeModel` is called. If a collection is found, + // each model in the collection is serialized by calling + // the view's `serializeCollection` and put into an `items` array in + // the resulting data. If both are found, defaults to the model. + // You can override the `serializeData` method in your own view definition, + // to provide custom serialization for your view's data. + serializeData: function() { + if (!this.model && !this.collection) { + return {}; } - - Marionette.triggerMethod.apply(this, args); - }, this); - }, - - // returns the value of itemEvents depending on if a function - getItemEvents: function() { - if (_.isFunction(this.itemEvents)) { - return this.itemEvents.call(this); - } - - return this.itemEvents; - }, - - // render the item view - renderItemView: function(view, index) { - view.render(); - this.appendHtml(this, view, index); - }, - - // Build an `itemView` for every model in the collection. - buildItemView: function(item, ItemViewType, itemViewOptions){ - var options = _.extend({model: item}, itemViewOptions); - return new ItemViewType(options); - }, - - // get the child view by item it holds, and remove it - removeItemView: function(item){ - var view = this.children.findByModel(item); - this.removeChildView(view); - this.checkEmpty(); - }, - - // Remove the child view and close it - removeChildView: function(view){ - - // shut down the child view properly, - // including events that the collection has from it - if (view){ - this.stopListening(view); - - // call 'close' or 'remove', depending on which is found - if (view.close) { view.close(); } - else if (view.remove) { view.remove(); } - - this.children.remove(view); - } - - this.triggerMethod("item:removed", view); - }, - - // helper to check if the collection is empty - isEmpty: function(collection){ - // check if we're empty now - return !this.collection || this.collection.length === 0; - }, - - // If empty, show the empty view - checkEmpty: function (){ - if (this.isEmpty(this.collection)){ - this.showEmptyView(); - } - }, - - // You might need to override this if you've overridden appendHtml - appendBuffer: function(collectionView, buffer) { - collectionView.$el.append(buffer); - }, - - // Append the HTML to the collection's `el`. - // Override this method to do something other - // than `.append`. - appendHtml: function(collectionView, itemView, index){ - if (collectionView.isBuffering) { - // buffering happens on reset events and initial renders - // in order to reduce the number of inserts into the - // document, which are expensive. - collectionView.elBuffer.appendChild(itemView.el); - collectionView._bufferedChildren.push(itemView); - } - else { - // If we've already rendered the main collection, just - // append the new items directly into the element. - collectionView.$el.append(itemView.el); - } - }, - - // Internal method to set up the `children` object for - // storing all of the child views - _initChildViewStorage: function(){ - this.children = new Backbone.ChildViewContainer(); - }, - - // Handle cleanup and other closing needs for - // the collection of views. - close: function(){ - if (this.isClosed){ return; } - - this.triggerMethod("collection:before:close"); - this.closeChildren(); - this.triggerMethod("collection:closed"); - - Marionette.View.prototype.close.apply(this, arguments); - }, - - // Close the child views that this collection view - // is holding on to, if any - closeChildren: function(){ - this.children.each(function(child){ - this.removeChildView(child); - }, this); - this.checkEmpty(); - } -}); - - -// Composite View -// -------------- - -// Used for rendering a branch-leaf, hierarchical structure. -// Extends directly from CollectionView and also renders an -// an item view as `modelView`, for the top leaf -Marionette.CompositeView = Marionette.CollectionView.extend({ - - // Setting up the inheritance chain which allows changes to - // Marionette.CollectionView.prototype.constructor which allows overriding - constructor: function(){ - Marionette.CollectionView.prototype.constructor.apply(this, arguments); - }, - - // Configured the initial events that the composite view - // binds to. Override this method to prevent the initial - // events, or to add your own initial events. - _initialEvents: function(){ - - // Bind only after composite view is rendered to avoid adding child views - // to nonexistent itemViewContainer - this.once('render', function () { - if (this.collection){ - this.listenTo(this.collection, "add", this.addChildView); - this.listenTo(this.collection, "remove", this.removeItemView); - this.listenTo(this.collection, "reset", this._renderChildren); + + var args = [this.model || this.collection]; + if (arguments.length) { + args.push.apply(args, arguments); } - }); - - }, - - // Retrieve the `itemView` to be used when rendering each of - // the items in the collection. The default is to return - // `this.itemView` or Marionette.CompositeView if no `itemView` - // has been defined - getItemView: function(item){ - var itemView = Marionette.getOption(this, "itemView") || this.constructor; - - if (!itemView){ - throwError("An `itemView` must be specified", "NoItemViewError"); - } - - return itemView; - }, - - // Serialize the collection for the view. - // You can override the `serializeData` method in your own view - // definition, to provide custom serialization for your view's data. - serializeData: function(){ - var data = {}; - - if (this.model){ - data = this.model.toJSON(); - } - - return data; - }, - - // Renders the model once, and the collection once. Calling - // this again will tell the model's view to re-render itself - // but the collection will not re-render. - render: function(){ - this.isRendered = true; - this.isClosed = false; - this.resetItemViewContainer(); - - this.triggerBeforeRender(); - var html = this.renderModel(); - this.$el.html(html); - // the ui bindings is done here and not at the end of render since they - // will not be available until after the model is rendered, but should be - // available before the collection is rendered. - this.bindUIElements(); - this.triggerMethod("composite:model:rendered"); - - this._renderChildren(); - - this.triggerMethod("composite:rendered"); - this.triggerRendered(); - return this; - }, - - _renderChildren: function(){ - if (this.isRendered){ - this.triggerMethod("composite:collection:before:render"); - Marionette.CollectionView.prototype._renderChildren.call(this); - this.triggerMethod("composite:collection:rendered"); - } - }, - - // Render an individual model, if we have one, as - // part of a composite view (branch / leaf). For example: - // a treeview. - renderModel: function(){ - var data = {}; - data = this.serializeData(); - data = this.mixinTemplateHelpers(data); - - var template = this.getTemplate(); - return Marionette.Renderer.render(template, data); - }, - - - // You might need to override this if you've overridden appendHtml - appendBuffer: function(compositeView, buffer) { - var $container = this.getItemViewContainer(compositeView); - $container.append(buffer); - }, - - // Appends the `el` of itemView instances to the specified - // `itemViewContainer` (a jQuery selector). Override this method to - // provide custom logic of how the child item view instances have their - // HTML appended to the composite view instance. - appendHtml: function(compositeView, itemView, index){ - if (compositeView.isBuffering) { - compositeView.elBuffer.appendChild(itemView.el); - compositeView._bufferedChildren.push(itemView); - } - else { - // If we've already rendered the main collection, just - // append the new items directly into the element. - var $container = this.getItemViewContainer(compositeView); - $container.append(itemView.el); + + if (this.model) { + return this.serializeModel.apply(this, args); + } else { + return { + items: this.serializeCollection.apply(this, args) + }; + } + }, + + // Serialize a collection by serializing each of its models. + serializeCollection: function(collection) { + return collection.toJSON.apply(collection, _.rest(arguments)); + }, + + // Render the view, defaulting to underscore.js templates. + // You can override this in your view definition to provide + // a very specific rendering for your view. In general, though, + // you should override the `Marionette.Renderer` object to + // change how Marionette renders views. + render: function() { + this._ensureViewIsIntact(); + + this.triggerMethod('before:render', this); + + this._renderTemplate(); + this.isRendered = true; + this.bindUIElements(); + + this.triggerMethod('render', this); + + return this; + }, + + // Internal method to render the template with the serialized data + // and template helpers via the `Marionette.Renderer` object. + // Throws an `UndefinedTemplateError` error if the template is + // any falsely value but literal `false`. + _renderTemplate: function() { + var template = this.getTemplate(); + + // Allow template-less item views + if (template === false) { + return; + } + + if (!template) { + throw new Marionette.Error({ + name: 'UndefinedTemplateError', + message: 'Cannot render the template since it is null or undefined.' + }); + } + + // Add in entity data and template helpers + var data = this.mixinTemplateHelpers(this.serializeData()); + + // Render and add to el + var html = Marionette.Renderer.render(template, data, this); + this.attachElContent(html); + + return this; + }, + + // Attaches the content of a given view. + // This method can be overridden to optimize rendering, + // or to render in a non standard way. + // + // For example, using `innerHTML` instead of `$el.html` + // + // ```js + // attachElContent: function(html) { + // this.el.innerHTML = html; + // return this; + // } + // ``` + attachElContent: function(html) { + this.$el.html(html); + + return this; } - }, - - - // Internal method to ensure an `$itemViewContainer` exists, for the - // `appendHtml` method to use. - getItemViewContainer: function(containerView){ - if ("$itemViewContainer" in containerView){ - return containerView.$itemViewContainer; + }); + + /* jshint maxstatements: 14 */ + + // Collection View + // --------------- + + // A view that iterates over a Backbone.Collection + // and renders an individual child view for each model. + Marionette.CollectionView = Marionette.View.extend({ + + // used as the prefix for child view events + // that are forwarded through the collectionview + childViewEventPrefix: 'childview', + + // flag for maintaining the sorted order of the collection + sort: true, + + // constructor + // option to pass `{sort: false}` to prevent the `CollectionView` from + // maintaining the sorted order of the collection. + // This will fallback onto appending childView's to the end. + // + // option to pass `{comparator: compFunction()}` to allow the `CollectionView` + // to use a custom sort order for the collection. + constructor: function(options) { + + this.once('render', this._initialEvents); + this._initChildViewStorage(); + + Marionette.View.apply(this, arguments); + + this.on('show', this._onShowCalled); + + this.initRenderBuffer(); + }, + + // Instead of inserting elements one by one into the page, + // it's much more performant to insert elements into a document + // fragment and then insert that document fragment into the page + initRenderBuffer: function() { + this._bufferedChildren = []; + }, + + startBuffering: function() { + this.initRenderBuffer(); + this.isBuffering = true; + }, + + endBuffering: function() { + this.isBuffering = false; + this._triggerBeforeShowBufferedChildren(); + + this.attachBuffer(this); + + this._triggerShowBufferedChildren(); + this.initRenderBuffer(); + }, + + _triggerBeforeShowBufferedChildren: function() { + if (this._isShown) { + _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'before:show')); + } + }, + + _triggerShowBufferedChildren: function() { + if (this._isShown) { + _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'show')); + + this._bufferedChildren = []; + } + }, + + // Internal method for _.each loops to call `Marionette.triggerMethodOn` on + // a child view + _triggerMethodOnChild: function(event, childView) { + Marionette.triggerMethodOn(childView, event); + }, + + // Configured the initial events that the collection view + // binds to. + _initialEvents: function() { + if (this.collection) { + this.listenTo(this.collection, 'add', this._onCollectionAdd); + this.listenTo(this.collection, 'remove', this._onCollectionRemove); + this.listenTo(this.collection, 'reset', this.render); + + if (this.getOption('sort')) { + this.listenTo(this.collection, 'sort', this._sortViews); + } + } + }, + + // Handle a child added to the collection + _onCollectionAdd: function(child, collection, opts) { + var index; + if (opts.at !== undefined) { + index = opts.at; + } else { + index = _.indexOf(this._filteredSortedModels(), child); + } + + if (this._shouldAddChild(child, index)) { + this.destroyEmptyView(); + var ChildView = this.getChildView(child); + this.addChild(child, ChildView, index); + } + }, + + // get the child view by model it holds, and remove it + _onCollectionRemove: function(model) { + var view = this.children.findByModel(model); + this.removeChildView(view); + this.checkEmpty(); + }, + + _onShowCalled: function() { + this.children.each(_.partial(this._triggerMethodOnChild, 'show')); + }, + + // Render children views. Override this method to + // provide your own implementation of a render function for + // the collection view. + render: function() { + this._ensureViewIsIntact(); + this.triggerMethod('before:render', this); + this._renderChildren(); + this.isRendered = true; + this.triggerMethod('render', this); + return this; + }, + + // Reorder DOM after sorting. When your element's rendering + // do not use their index, you can pass reorderOnSort: true + // to only reorder the DOM after a sort instead of rendering + // all the collectionView + reorder: function() { + var children = this.children; + var models = this._filteredSortedModels(); + var modelsChanged = _.find(models, function(model) { + return !children.findByModel(model); + }); + + // If the models we're displaying have changed due to filtering + // We need to add and/or remove child views + // So render as normal + if (modelsChanged) { + this.render(); + } else { + // get the DOM nodes in the same order as the models + var els = _.map(models, function(model) { + return children.findByModel(model).el; + }); + + // since append moves elements that are already in the DOM, + // appending the elements will effectively reorder them + this.triggerMethod('before:reorder'); + this._appendReorderedChildren(els); + this.triggerMethod('reorder'); + } + }, + + // Render view after sorting. Override this method to + // change how the view renders after a `sort` on the collection. + // An example of this would be to only `renderChildren` in a `CompositeView` + // rather than the full view. + resortView: function() { + if (Marionette.getOption(this, 'reorderOnSort')) { + this.reorder(); + } else { + this.render(); + } + }, + + // Internal method. This checks for any changes in the order of the collection. + // If the index of any view doesn't match, it will render. + _sortViews: function() { + var models = this._filteredSortedModels(); + + // check for any changes in sort order of views + var orderChanged = _.find(models, function(item, index) { + var view = this.children.findByModel(item); + return !view || view._index !== index; + }, this); + + if (orderChanged) { + this.resortView(); + } + }, + + // Internal reference to what index a `emptyView` is. + _emptyViewIndex: -1, + + // Internal method. Separated so that CompositeView can append to the childViewContainer + // if necessary + _appendReorderedChildren: function(children) { + this.$el.append(children); + }, + + // Internal method. Separated so that CompositeView can have + // more control over events being triggered, around the rendering + // process + _renderChildren: function() { + this.destroyEmptyView(); + this.destroyChildren(); + + if (this.isEmpty(this.collection)) { + this.showEmptyView(); + } else { + this.triggerMethod('before:render:collection', this); + this.startBuffering(); + this.showCollection(); + this.endBuffering(); + this.triggerMethod('render:collection', this); + + // If we have shown children and none have passed the filter, show the empty view + if (this.children.isEmpty()) { + this.showEmptyView(); + } + } + }, + + // Internal method to loop through collection and show each child view. + showCollection: function() { + var ChildView; + + var models = this._filteredSortedModels(); + + _.each(models, function(child, index) { + ChildView = this.getChildView(child); + this.addChild(child, ChildView, index); + }, this); + }, + + // Allow the collection to be sorted by a custom view comparator + _filteredSortedModels: function() { + var models; + var viewComparator = this.getViewComparator(); + + if (viewComparator) { + if (_.isString(viewComparator) || viewComparator.length === 1) { + models = this.collection.sortBy(viewComparator, this); + } else { + models = _.clone(this.collection.models).sort(_.bind(viewComparator, this)); + } + } else { + models = this.collection.models; + } + + // Filter after sorting in case the filter uses the index + if (this.getOption('filter')) { + models = _.filter(models, function(model, index) { + return this._shouldAddChild(model, index); + }, this); + } + + return models; + }, + + // Internal method to show an empty view in place of + // a collection of child views, when the collection is empty + showEmptyView: function() { + var EmptyView = this.getEmptyView(); + + if (EmptyView && !this._showingEmptyView) { + this.triggerMethod('before:render:empty'); + + this._showingEmptyView = true; + var model = new Backbone.Model(); + this.addEmptyView(model, EmptyView); + + this.triggerMethod('render:empty'); + } + }, + + // Internal method to destroy an existing emptyView instance + // if one exists. Called when a collection view has been + // rendered empty, and then a child is added to the collection. + destroyEmptyView: function() { + if (this._showingEmptyView) { + this.triggerMethod('before:remove:empty'); + + this.destroyChildren(); + delete this._showingEmptyView; + + this.triggerMethod('remove:empty'); + } + }, + + // Retrieve the empty view class + getEmptyView: function() { + return this.getOption('emptyView'); + }, + + // Render and show the emptyView. Similar to addChild method + // but "add:child" events are not fired, and the event from + // emptyView are not forwarded + addEmptyView: function(child, EmptyView) { + + // get the emptyViewOptions, falling back to childViewOptions + var emptyViewOptions = this.getOption('emptyViewOptions') || + this.getOption('childViewOptions'); + + if (_.isFunction(emptyViewOptions)) { + emptyViewOptions = emptyViewOptions.call(this, child, this._emptyViewIndex); + } + + // build the empty view + var view = this.buildChildView(child, EmptyView, emptyViewOptions); + + view._parent = this; + + // Proxy emptyView events + this.proxyChildEvents(view); + + // trigger the 'before:show' event on `view` if the collection view + // has already been shown + if (this._isShown) { + Marionette.triggerMethodOn(view, 'before:show'); + } + + // Store the `emptyView` like a `childView` so we can properly + // remove and/or close it later + this.children.add(view); + + // Render it and show it + this.renderChildView(view, this._emptyViewIndex); + + // call the 'show' method if the collection view + // has already been shown + if (this._isShown) { + Marionette.triggerMethodOn(view, 'show'); + } + }, + + // Retrieve the `childView` class, either from `this.options.childView` + // or from the `childView` in the object definition. The "options" + // takes precedence. + // This method receives the model that will be passed to the instance + // created from this `childView`. Overriding methods may use the child + // to determine what `childView` class to return. + getChildView: function(child) { + var childView = this.getOption('childView'); + + if (!childView) { + throw new Marionette.Error({ + name: 'NoChildViewError', + message: 'A "childView" must be specified' + }); + } + + return childView; + }, + + // Render the child's view and add it to the + // HTML for the collection view at a given index. + // This will also update the indices of later views in the collection + // in order to keep the children in sync with the collection. + addChild: function(child, ChildView, index) { + var childViewOptions = this.getOption('childViewOptions'); + childViewOptions = Marionette._getValue(childViewOptions, this, [child, index]); + + var view = this.buildChildView(child, ChildView, childViewOptions); + + // increment indices of views after this one + this._updateIndices(view, true, index); + + this._addChildView(view, index); + + view._parent = this; + + return view; + }, + + // Internal method. This decrements or increments the indices of views after the + // added/removed view to keep in sync with the collection. + _updateIndices: function(view, increment, index) { + if (!this.getOption('sort')) { + return; + } + + if (increment) { + // assign the index to the view + view._index = index; + } + + // update the indexes of views after this one + this.children.each(function(laterView) { + if (laterView._index >= view._index) { + laterView._index += increment ? 1 : -1; + } + }); + }, + + // Internal Method. Add the view to children and render it at + // the given index. + _addChildView: function(view, index) { + // set up the child view event forwarding + this.proxyChildEvents(view); + + this.triggerMethod('before:add:child', view); + + // trigger the 'before:show' event on `view` if the collection view + // has already been shown + if (this._isShown && !this.isBuffering) { + Marionette.triggerMethodOn(view, 'before:show'); + } + + // Store the child view itself so we can properly + // remove and/or destroy it later + this.children.add(view); + this.renderChildView(view, index); + + if (this._isShown && !this.isBuffering) { + Marionette.triggerMethodOn(view, 'show'); + } + + this.triggerMethod('add:child', view); + }, + + // render the child view + renderChildView: function(view, index) { + view.render(); + this.attachHtml(this, view, index); + return view; + }, + + // Build a `childView` for a model in the collection. + buildChildView: function(child, ChildViewClass, childViewOptions) { + var options = _.extend({model: child}, childViewOptions); + return new ChildViewClass(options); + }, + + // Remove the child view and destroy it. + // This function also updates the indices of + // later views in the collection in order to keep + // the children in sync with the collection. + removeChildView: function(view) { + + if (view) { + this.triggerMethod('before:remove:child', view); + + // call 'destroy' or 'remove', depending on which is found + if (view.destroy) { + view.destroy(); + } else if (view.remove) { + view.remove(); + } + + delete view._parent; + this.stopListening(view); + this.children.remove(view); + this.triggerMethod('remove:child', view); + + // decrement the index of views after this one + this._updateIndices(view, false); + } + + return view; + }, + + // check if the collection is empty + isEmpty: function() { + return !this.collection || this.collection.length === 0; + }, + + // If empty, show the empty view + checkEmpty: function() { + if (this.isEmpty(this.collection)) { + this.showEmptyView(); + } + }, + + // You might need to override this if you've overridden attachHtml + attachBuffer: function(collectionView) { + collectionView.$el.append(this._createBuffer(collectionView)); + }, + + // Create a fragment buffer from the currently buffered children + _createBuffer: function(collectionView) { + var elBuffer = document.createDocumentFragment(); + _.each(collectionView._bufferedChildren, function(b) { + elBuffer.appendChild(b.el); + }); + return elBuffer; + }, + + // Append the HTML to the collection's `el`. + // Override this method to do something other + // than `.append`. + attachHtml: function(collectionView, childView, index) { + if (collectionView.isBuffering) { + // buffering happens on reset events and initial renders + // in order to reduce the number of inserts into the + // document, which are expensive. + collectionView._bufferedChildren.splice(index, 0, childView); + } else { + // If we've already rendered the main collection, append + // the new child into the correct order if we need to. Otherwise + // append to the end. + if (!collectionView._insertBefore(childView, index)) { + collectionView._insertAfter(childView); + } + } + }, + + // Internal method. Check whether we need to insert the view into + // the correct position. + _insertBefore: function(childView, index) { + var currentView; + var findPosition = this.getOption('sort') && (index < this.children.length - 1); + if (findPosition) { + // Find the view after this one + currentView = this.children.find(function(view) { + return view._index === index + 1; + }); + } + + if (currentView) { + currentView.$el.before(childView.el); + return true; + } + + return false; + }, + + // Internal method. Append a view to the end of the $el + _insertAfter: function(childView) { + this.$el.append(childView.el); + }, + + // Internal method to set up the `children` object for + // storing all of the child views + _initChildViewStorage: function() { + this.children = new Backbone.ChildViewContainer(); + }, + + // Handle cleanup and other destroying needs for the collection of views + destroy: function() { + if (this.isDestroyed) { return this; } + + this.triggerMethod('before:destroy:collection'); + this.destroyChildren(); + this.triggerMethod('destroy:collection'); + + return Marionette.View.prototype.destroy.apply(this, arguments); + }, + + // Destroy the child views that this collection view + // is holding on to, if any + destroyChildren: function() { + var childViews = this.children.map(_.identity); + this.children.each(this.removeChildView, this); + this.checkEmpty(); + return childViews; + }, + + // Return true if the given child should be shown + // Return false otherwise + // The filter will be passed (child, index, collection) + // Where + // 'child' is the given model + // 'index' is the index of that model in the collection + // 'collection' is the collection referenced by this CollectionView + _shouldAddChild: function(child, index) { + var filter = this.getOption('filter'); + return !_.isFunction(filter) || filter.call(this, child, index, this.collection); + }, + + // Set up the child view event forwarding. Uses a "childview:" + // prefix in front of all forwarded events. + proxyChildEvents: function(view) { + var prefix = this.getOption('childViewEventPrefix'); + + // Forward all child view events through the parent, + // prepending "childview:" to the event name + this.listenTo(view, 'all', function() { + var args = _.toArray(arguments); + var rootEvent = args[0]; + var childEvents = this.normalizeMethods(_.result(this, 'childEvents')); + + args[0] = prefix + ':' + rootEvent; + args.splice(1, 0, view); + + // call collectionView childEvent if defined + if (typeof childEvents !== 'undefined' && _.isFunction(childEvents[rootEvent])) { + childEvents[rootEvent].apply(this, args.slice(1)); + } + + this.triggerMethod.apply(this, args); + }); + }, + + _getImmediateChildren: function() { + return _.values(this.children._views); + }, + + getViewComparator: function() { + return this.getOption('viewComparator'); } - - var container; - var itemViewContainer = Marionette.getOption(containerView, "itemViewContainer"); - if (itemViewContainer){ - - var selector = _.isFunction(itemViewContainer) ? itemViewContainer.call(this) : itemViewContainer; - container = containerView.$(selector); - if (container.length <= 0) { - throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError"); + }); + + /* jshint maxstatements: 17, maxlen: 117 */ + + // Composite View + // -------------- + + // Used for rendering a branch-leaf, hierarchical structure. + // Extends directly from CollectionView and also renders an + // a child view as `modelView`, for the top leaf + Marionette.CompositeView = Marionette.CollectionView.extend({ + + // Setting up the inheritance chain which allows changes to + // Marionette.CollectionView.prototype.constructor which allows overriding + // option to pass '{sort: false}' to prevent the CompositeView from + // maintaining the sorted order of the collection. + // This will fallback onto appending childView's to the end. + constructor: function() { + Marionette.CollectionView.apply(this, arguments); + }, + + // Configured the initial events that the composite view + // binds to. Override this method to prevent the initial + // events, or to add your own initial events. + _initialEvents: function() { + + // Bind only after composite view is rendered to avoid adding child views + // to nonexistent childViewContainer + + if (this.collection) { + this.listenTo(this.collection, 'add', this._onCollectionAdd); + this.listenTo(this.collection, 'remove', this._onCollectionRemove); + this.listenTo(this.collection, 'reset', this._renderChildren); + + if (this.getOption('sort')) { + this.listenTo(this.collection, 'sort', this._sortViews); + } + } + }, + + // Retrieve the `childView` to be used when rendering each of + // the items in the collection. The default is to return + // `this.childView` or Marionette.CompositeView if no `childView` + // has been defined + getChildView: function(child) { + var childView = this.getOption('childView') || this.constructor; + + return childView; + }, + + // Serialize the model for the view. + // You can override the `serializeData` method in your own view + // definition, to provide custom serialization for your view's data. + serializeData: function() { + var data = {}; + + if (this.model) { + data = _.partial(this.serializeModel, this.model).apply(this, arguments); + } + + return data; + }, + + // Renders the model and the collection. + render: function() { + this._ensureViewIsIntact(); + this._isRendering = true; + this.resetChildViewContainer(); + + this.triggerMethod('before:render', this); + + this._renderTemplate(); + this._renderChildren(); + + this._isRendering = false; + this.isRendered = true; + this.triggerMethod('render', this); + return this; + }, + + _renderChildren: function() { + if (this.isRendered || this._isRendering) { + Marionette.CollectionView.prototype._renderChildren.call(this); + } + }, + + // Render the root template that the children + // views are appended to + _renderTemplate: function() { + var data = {}; + data = this.serializeData(); + data = this.mixinTemplateHelpers(data); + + this.triggerMethod('before:render:template'); + + var template = this.getTemplate(); + var html = Marionette.Renderer.render(template, data, this); + this.attachElContent(html); + + // the ui bindings is done here and not at the end of render since they + // will not be available until after the model is rendered, but should be + // available before the collection is rendered. + this.bindUIElements(); + this.triggerMethod('render:template'); + }, + + // Attaches the content of the root. + // This method can be overridden to optimize rendering, + // or to render in a non standard way. + // + // For example, using `innerHTML` instead of `$el.html` + // + // ```js + // attachElContent: function(html) { + // this.el.innerHTML = html; + // return this; + // } + // ``` + attachElContent: function(html) { + this.$el.html(html); + + return this; + }, + + // You might need to override this if you've overridden attachHtml + attachBuffer: function(compositeView) { + var $container = this.getChildViewContainer(compositeView); + $container.append(this._createBuffer(compositeView)); + }, + + // Internal method. Append a view to the end of the $el. + // Overidden from CollectionView to ensure view is appended to + // childViewContainer + _insertAfter: function(childView) { + var $container = this.getChildViewContainer(this, childView); + $container.append(childView.el); + }, + + // Internal method. Append reordered childView'. + // Overidden from CollectionView to ensure reordered views + // are appended to childViewContainer + _appendReorderedChildren: function(children) { + var $container = this.getChildViewContainer(this); + $container.append(children); + }, + + // Internal method to ensure an `$childViewContainer` exists, for the + // `attachHtml` method to use. + getChildViewContainer: function(containerView, childView) { + if ('$childViewContainer' in containerView) { + return containerView.$childViewContainer; + } + + var container; + var childViewContainer = Marionette.getOption(containerView, 'childViewContainer'); + if (childViewContainer) { + + var selector = Marionette._getValue(childViewContainer, containerView); + + if (selector.charAt(0) === '@' && containerView.ui) { + container = containerView.ui[selector.substr(4)]; + } else { + container = containerView.$(selector); + } + + if (container.length <= 0) { + throw new Marionette.Error({ + name: 'ChildViewContainerMissingError', + message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer + }); + } + + } else { + container = containerView.$el; + } + + containerView.$childViewContainer = container; + return container; + }, + + // Internal method to reset the `$childViewContainer` on render + resetChildViewContainer: function() { + if (this.$childViewContainer) { + delete this.$childViewContainer; } - - } else { - container = containerView.$el; } - - containerView.$itemViewContainer = container; - return container; - }, - - // Internal method to reset the `$itemViewContainer` on render - resetItemViewContainer: function(){ - if (this.$itemViewContainer){ - delete this.$itemViewContainer; + }); + + // Layout View + // ----------- + + // Used for managing application layoutViews, nested layoutViews and + // multiple regions within an application or sub-application. + // + // A specialized view class that renders an area of HTML and then + // attaches `Region` instances to the specified `regions`. + // Used for composite view management and sub-application areas. + Marionette.LayoutView = Marionette.ItemView.extend({ + regionClass: Marionette.Region, + + options: { + destroyImmediate: false + }, + + // used as the prefix for child view events + // that are forwarded through the layoutview + childViewEventPrefix: 'childview', + + // Ensure the regions are available when the `initialize` method + // is called. + constructor: function(options) { + options = options || {}; + + this._firstRender = true; + this._initializeRegions(options); + + Marionette.ItemView.call(this, options); + }, + + // LayoutView's render will use the existing region objects the + // first time it is called. Subsequent calls will destroy the + // views that the regions are showing and then reset the `el` + // for the regions to the newly rendered DOM elements. + render: function() { + this._ensureViewIsIntact(); + + if (this._firstRender) { + // if this is the first render, don't do anything to + // reset the regions + this._firstRender = false; + } else { + // If this is not the first render call, then we need to + // re-initialize the `el` for each region + this._reInitializeRegions(); + } + + return Marionette.ItemView.prototype.render.apply(this, arguments); + }, + + // Handle destroying regions, and then destroy the view itself. + destroy: function() { + if (this.isDestroyed) { return this; } + // #2134: remove parent element before destroying the child views, so + // removing the child views doesn't retrigger repaints + if (this.getOption('destroyImmediate') === true) { + this.$el.remove(); + } + this.regionManager.destroy(); + return Marionette.ItemView.prototype.destroy.apply(this, arguments); + }, + + showChildView: function(regionName, view) { + return this.getRegion(regionName).show(view); + }, + + getChildView: function(regionName) { + return this.getRegion(regionName).currentView; + }, + + // Add a single region, by name, to the layoutView + addRegion: function(name, definition) { + var regions = {}; + regions[name] = definition; + return this._buildRegions(regions)[name]; + }, + + // Add multiple regions as a {name: definition, name2: def2} object literal + addRegions: function(regions) { + this.regions = _.extend({}, this.regions, regions); + return this._buildRegions(regions); + }, + + // Remove a single region from the LayoutView, by name + removeRegion: function(name) { + delete this.regions[name]; + return this.regionManager.removeRegion(name); + }, + + // Provides alternative access to regions + // Accepts the region name + // getRegion('main') + getRegion: function(region) { + return this.regionManager.get(region); + }, + + // Get all regions + getRegions: function() { + return this.regionManager.getRegions(); + }, + + // internal method to build regions + _buildRegions: function(regions) { + var defaults = { + regionClass: this.getOption('regionClass'), + parentEl: _.partial(_.result, this, 'el') + }; + + return this.regionManager.addRegions(regions, defaults); + }, + + // Internal method to initialize the regions that have been defined in a + // `regions` attribute on this layoutView. + _initializeRegions: function(options) { + var regions; + this._initRegionManager(); + + regions = Marionette._getValue(this.regions, this, [options]) || {}; + + // Enable users to define `regions` as instance options. + var regionOptions = this.getOption.call(options, 'regions'); + + // enable region options to be a function + regionOptions = Marionette._getValue(regionOptions, this, [options]); + + _.extend(regions, regionOptions); + + // Normalize region selectors hash to allow + // a user to use the @ui. syntax. + regions = this.normalizeUIValues(regions, ['selector', 'el']); + + this.addRegions(regions); + }, + + // Internal method to re-initialize all of the regions by updating the `el` that + // they point to + _reInitializeRegions: function() { + this.regionManager.invoke('reset'); + }, + + // Enable easy overriding of the default `RegionManager` + // for customized region interactions and business specific + // view logic for better control over single regions. + getRegionManager: function() { + return new Marionette.RegionManager(); + }, + + // Internal method to initialize the region manager + // and all regions in it + _initRegionManager: function() { + this.regionManager = this.getRegionManager(); + this.regionManager._parent = this; + + this.listenTo(this.regionManager, 'before:add:region', function(name) { + this.triggerMethod('before:add:region', name); + }); + + this.listenTo(this.regionManager, 'add:region', function(name, region) { + this[name] = region; + this.triggerMethod('add:region', name, region); + }); + + this.listenTo(this.regionManager, 'before:remove:region', function(name) { + this.triggerMethod('before:remove:region', name); + }); + + this.listenTo(this.regionManager, 'remove:region', function(name, region) { + delete this[name]; + this.triggerMethod('remove:region', name, region); + }); + }, + + _getImmediateChildren: function() { + return _.chain(this.regionManager.getRegions()) + .pluck('currentView') + .compact() + .value(); } - } -}); - - -// Layout -// ------ - -// Used for managing application layouts, nested layouts and -// multiple regions within an application or sub-application. -// -// A specialized view type that renders an area of HTML and then -// attaches `Region` instances to the specified `regions`. -// Used for composite view management and sub-application areas. -Marionette.Layout = Marionette.ItemView.extend({ - regionType: Marionette.Region, - - // Ensure the regions are available when the `initialize` method - // is called. - constructor: function (options) { - options = options || {}; - - this._firstRender = true; - this._initializeRegions(options); - - Marionette.ItemView.prototype.constructor.call(this, options); - }, - - // Layout's render will use the existing region objects the - // first time it is called. Subsequent calls will close the - // views that the regions are showing and then reset the `el` - // for the regions to the newly rendered DOM elements. - render: function(){ + }); + - if (this.isClosed){ - // a previously closed layout means we need to - // completely re-initialize the regions - this._initializeRegions(); + // Behavior + // -------- + + // A Behavior is an isolated set of DOM / + // user interactions that can be mixed into any View. + // Behaviors allow you to blackbox View specific interactions + // into portable logical chunks, keeping your views simple and your code DRY. + + Marionette.Behavior = Marionette.Object.extend({ + constructor: function(options, view) { + // Setup reference to the view. + // this comes in handle when a behavior + // wants to directly talk up the chain + // to the view. + this.view = view; + this.defaults = _.result(this, 'defaults') || {}; + this.options = _.extend({}, this.defaults, options); + // Construct an internal UI hash using + // the views UI hash and then the behaviors UI hash. + // This allows the user to use UI hash elements + // defined in the parent view as well as those + // defined in the given behavior. + this.ui = _.extend({}, _.result(view, 'ui'), _.result(this, 'ui')); + + Marionette.Object.apply(this, arguments); + }, + + // proxy behavior $ method to the view + // this is useful for doing jquery DOM lookups + // scoped to behaviors view. + $: function() { + return this.view.$.apply(this.view, arguments); + }, + + // Stops the behavior from listening to events. + // Overrides Object#destroy to prevent additional events from being triggered. + destroy: function() { + this.stopListening(); + + return this; + }, + + proxyViewProperties: function(view) { + this.$el = view.$el; + this.el = view.el; } - if (this._firstRender) { - // if this is the first render, don't do anything to - // reset the regions - this._firstRender = false; - } else if (!this.isClosed){ - // If this is not the first render call, then we need to - // re-initializing the `el` for each region - this._reInitializeRegions(); + }); + + /* jshint maxlen: 143 */ + // Behaviors + // --------- + + // Behaviors is a utility class that takes care of + // gluing your behavior instances to their given View. + // The most important part of this class is that you + // **MUST** override the class level behaviorsLookup + // method for things to work properly. + + Marionette.Behaviors = (function(Marionette, _) { + // Borrow event splitter from Backbone + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + function Behaviors(view, behaviors) { + + if (!_.isObject(view.behaviors)) { + return {}; + } + + // Behaviors defined on a view can be a flat object literal + // or it can be a function that returns an object. + behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors')); + + // Wraps several of the view's methods + // calling the methods first on each behavior + // and then eventually calling the method on the view. + Behaviors.wrap(view, behaviors, _.keys(methods)); + return behaviors; } - - return Marionette.ItemView.prototype.render.apply(this, arguments); - }, - - // Handle closing regions, and then close the view itself. - close: function () { - if (this.isClosed){ return; } - this.regionManager.close(); - Marionette.ItemView.prototype.close.apply(this, arguments); - }, - - // Add a single region, by name, to the layout - addRegion: function(name, definition){ - var regions = {}; - regions[name] = definition; - return this._buildRegions(regions)[name]; - }, - - // Add multiple regions as a {name: definition, name2: def2} object literal - addRegions: function(regions){ - this.regions = _.extend({}, this.regions, regions); - return this._buildRegions(regions); - }, - - // Remove a single region from the Layout, by name - removeRegion: function(name){ - delete this.regions[name]; - return this.regionManager.removeRegion(name); - }, - - // internal method to build regions - _buildRegions: function(regions){ - var that = this; - - var defaults = { - regionType: Marionette.getOption(this, "regionType"), - parentEl: function(){ return that.$el; } + + var methods = { + behaviorTriggers: function(behaviorTriggers, behaviors) { + var triggerBuilder = new BehaviorTriggersBuilder(this, behaviors); + return triggerBuilder.buildBehaviorTriggers(); + }, + + behaviorEvents: function(behaviorEvents, behaviors) { + var _behaviorsEvents = {}; + + _.each(behaviors, function(b, i) { + var _events = {}; + var behaviorEvents = _.clone(_.result(b, 'events')) || {}; + + // Normalize behavior events hash to allow + // a user to use the @ui. syntax. + behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, getBehaviorsUI(b)); + + var j = 0; + _.each(behaviorEvents, function(behaviour, key) { + var match = key.match(delegateEventSplitter); + + // Set event name to be namespaced using the view cid, + // the behavior index, and the behavior event index + // to generate a non colliding event namespace + // http://api.jquery.com/event.namespace/ + var eventName = match[1] + '.' + [this.cid, i, j++, ' '].join(''); + var selector = match[2]; + + var eventKey = eventName + selector; + var handler = _.isFunction(behaviour) ? behaviour : b[behaviour]; + + _events[eventKey] = _.bind(handler, b); + }, this); + + _behaviorsEvents = _.extend(_behaviorsEvents, _events); + }, this); + + return _behaviorsEvents; + } }; - - return this.regionManager.addRegions(regions, defaults); - }, - - // Internal method to initialize the regions that have been defined in a - // `regions` attribute on this layout. - _initializeRegions: function (options) { - var regions; - this._initRegionManager(); - - if (_.isFunction(this.regions)) { - regions = this.regions(options); - } else { - regions = this.regions || {}; - } - - this.addRegions(regions); - }, - - // Internal method to re-initialize all of the regions by updating the `el` that - // they point to - _reInitializeRegions: function(){ - this.regionManager.closeRegions(); - this.regionManager.each(function(region){ - region.reset(); - }); - }, - - // Internal method to initialize the region manager - // and all regions in it - _initRegionManager: function(){ - this.regionManager = new Marionette.RegionManager(); - - this.listenTo(this.regionManager, "region:add", function(name, region){ - this[name] = region; - this.trigger("region:add", name, region); - }); - - this.listenTo(this.regionManager, "region:remove", function(name, region){ - delete this[name]; - this.trigger("region:remove", name, region); + + _.extend(Behaviors, { + + // Placeholder method to be extended by the user. + // The method should define the object that stores the behaviors. + // i.e. + // + // ```js + // Marionette.Behaviors.behaviorsLookup: function() { + // return App.Behaviors + // } + // ``` + behaviorsLookup: function() { + throw new Marionette.Error({ + message: 'You must define where your behaviors are stored.', + url: 'marionette.behaviors.html#behaviorslookup' + }); + }, + + // Takes care of getting the behavior class + // given options and a key. + // If a user passes in options.behaviorClass + // default to using that. Otherwise delegate + // the lookup to the users `behaviorsLookup` implementation. + getBehaviorClass: function(options, key) { + if (options.behaviorClass) { + return options.behaviorClass; + } + + // Get behavior class can be either a flat object or a method + return Marionette._getValue(Behaviors.behaviorsLookup, this, [options, key])[key]; + }, + + // Iterate over the behaviors object, for each behavior + // instantiate it and get its grouped behaviors. + parseBehaviors: function(view, behaviors) { + return _.chain(behaviors).map(function(options, key) { + var BehaviorClass = Behaviors.getBehaviorClass(options, key); + + var behavior = new BehaviorClass(options, view); + var nestedBehaviors = Behaviors.parseBehaviors(view, _.result(behavior, 'behaviors')); + + return [behavior].concat(nestedBehaviors); + }).flatten().value(); + }, + + // Wrap view internal methods so that they delegate to behaviors. For example, + // `onDestroy` should trigger destroy on all of the behaviors and then destroy itself. + // i.e. + // + // `view.delegateEvents = _.partial(methods.delegateEvents, view.delegateEvents, behaviors);` + wrap: function(view, behaviors, methodNames) { + _.each(methodNames, function(methodName) { + view[methodName] = _.partial(methods[methodName], view[methodName], behaviors); + }); + } }); - } -}); - - -// AppRouter -// --------- - -// Reduce the boilerplate code of handling route events -// and then calling a single method on another object. -// Have your routers configured to call the method on -// your object, directly. -// -// Configure an AppRouter with `appRoutes`. -// -// App routers can only take one `controller` object. -// It is recommended that you divide your controller -// objects in to smaller pieces of related functionality -// and have multiple routers / controllers, instead of -// just one giant router and controller. -// -// You can also add standard routes to an AppRouter. - -Marionette.AppRouter = Backbone.Router.extend({ - - constructor: function(options){ - Backbone.Router.prototype.constructor.apply(this, arguments); - - this.options = options || {}; - - var appRoutes = Marionette.getOption(this, "appRoutes"); - var controller = this._getController(); - this.processAppRoutes(controller, appRoutes); - }, - - // Similar to route method on a Backbone Router but - // method is called on the controller - appRoute: function(route, methodName) { - var controller = this._getController(); - this._addAppRoute(controller, route, methodName); - }, - - // Internal method to process the `appRoutes` for the - // router, and turn them in to routes that trigger the - // specified method on the specified `controller`. - processAppRoutes: function(controller, appRoutes) { - if (!appRoutes){ return; } - - var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes - - _.each(routeNames, function(route) { - this._addAppRoute(controller, route, appRoutes[route]); - }, this); - }, - - _getController: function(){ - return Marionette.getOption(this, "controller"); - }, - - _addAppRoute: function(controller, route, methodName){ - var method = controller[methodName]; - - if (!method) { - throwError("Method '" + methodName + "' was not found on the controller"); + + // Class to build handlers for `triggers` on behaviors + // for views + function BehaviorTriggersBuilder(view, behaviors) { + this._view = view; + this._behaviors = behaviors; + this._triggers = {}; } - - this.route(route, methodName, _.bind(method, controller)); - } -}); - - -// Application -// ----------- - -// Contain and manage the composite application as a whole. -// Stores and starts up `Region` objects, includes an -// event aggregator as `app.vent` -Marionette.Application = function(options){ - this._initRegionManager(); - this._initCallbacks = new Marionette.Callbacks(); - this.vent = new Backbone.Wreqr.EventAggregator(); - this.commands = new Backbone.Wreqr.Commands(); - this.reqres = new Backbone.Wreqr.RequestResponse(); - this.submodules = {}; - - _.extend(this, options); - - this.triggerMethod = Marionette.triggerMethod; -}; - -_.extend(Marionette.Application.prototype, Backbone.Events, { - // Command execution, facilitated by Backbone.Wreqr.Commands - execute: function(){ - this.commands.execute.apply(this.commands, arguments); - }, - - // Request/response, facilitated by Backbone.Wreqr.RequestResponse - request: function(){ - return this.reqres.request.apply(this.reqres, arguments); - }, - - // Add an initializer that is either run at when the `start` - // method is called, or run immediately if added after `start` - // has already been called. - addInitializer: function(initializer){ - this._initCallbacks.add(initializer); - }, - - // kick off all of the application's processes. - // initializes all of the regions that have been added - // to the app, and runs all of the initializer functions - start: function(options){ - this.triggerMethod("initialize:before", options); - this._initCallbacks.run(options, this); - this.triggerMethod("initialize:after", options); - - this.triggerMethod("start", options); - }, - - // Add regions to your app. - // Accepts a hash of named strings or Region objects - // addRegions({something: "#someRegion"}) - // addRegions({something: Region.extend({el: "#someRegion"}) }); - addRegions: function(regions){ - return this._regionManager.addRegions(regions); - }, - - // Close all regions in the app, without removing them - closeRegions: function(){ - this._regionManager.closeRegions(); - }, - - // Removes a region from your app, by name - // Accepts the regions name - // removeRegion('myRegion') - removeRegion: function(region) { - this._regionManager.removeRegion(region); - }, - - // Provides alternative access to regions - // Accepts the region name - // getRegion('main') - getRegion: function(region) { - return this._regionManager.get(region); - }, - - // Create a module, attached to the application - module: function(moduleNames, moduleDefinition){ - - // Overwrite the module class if the user specifies one - var ModuleClass = Marionette.Module.getClass(moduleDefinition); - - // slice the args, and add this application object as the - // first argument of the array - var args = slice.call(arguments); - args.unshift(this); - - // see the Marionette.Module object for more information - return ModuleClass.create.apply(ModuleClass, args); - }, - - // Internal method to set up the region manager - _initRegionManager: function(){ - this._regionManager = new Marionette.RegionManager(); - - this.listenTo(this._regionManager, "region:add", function(name, region){ - this[name] = region; - }); - - this.listenTo(this._regionManager, "region:remove", function(name, region){ - delete this[name]; - }); - } -}); - -// Copy the `extend` function used by Backbone's classes -Marionette.Application.extend = Marionette.extend; - -// Module -// ------ - -// A simple module system, used to create privacy and encapsulation in -// Marionette applications -Marionette.Module = function(moduleName, app, options){ - this.moduleName = moduleName; - this.options = _.extend({}, this.options, options); - this.initialize = options.initialize || this.initialize; - - // store sub-modules - this.submodules = {}; - - this._setupInitializersAndFinalizers(); - - // store the configuration for this module - this.app = app; - this.startWithParent = true; - - this.triggerMethod = Marionette.triggerMethod; - - if (_.isFunction(this.initialize)){ - this.initialize(this.options, moduleName, app); - } -}; - -Marionette.Module.extend = Marionette.extend; - -// Extend the Module prototype with events / listenTo, so that the module -// can be used as an event aggregator or pub/sub. -_.extend(Marionette.Module.prototype, Backbone.Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic when extending Marionette.Module. - initialize: function(){}, - - // Initializer for a specific module. Initializers are run when the - // module's `start` method is called. - addInitializer: function(callback){ - this._initializerCallbacks.add(callback); - }, - - // Finalizers are run when a module is stopped. They are used to teardown - // and finalize any variables, references, events and other code that the - // module had set up. - addFinalizer: function(callback){ - this._finalizerCallbacks.add(callback); - }, - - // Start the module, and run all of its initializers - start: function(options){ - // Prevent re-starting a module that is already started - if (this._isInitialized){ return; } - - // start the sub-modules (depth-first hierarchy) - _.each(this.submodules, function(mod){ - // check to see if we should start the sub-module with this parent - if (mod.startWithParent){ - mod.start(options); + + _.extend(BehaviorTriggersBuilder.prototype, { + // Main method to build the triggers hash with event keys and handlers + buildBehaviorTriggers: function() { + _.each(this._behaviors, this._buildTriggerHandlersForBehavior, this); + return this._triggers; + }, + + // Internal method to build all trigger handlers for a given behavior + _buildTriggerHandlersForBehavior: function(behavior, i) { + var triggersHash = _.clone(_.result(behavior, 'triggers')) || {}; + + triggersHash = Marionette.normalizeUIKeys(triggersHash, getBehaviorsUI(behavior)); + + _.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i)); + }, + + // Internal method to create and assign the trigger handler for a given + // behavior + _setHandlerForBehavior: function(behavior, i, eventName, trigger) { + // Unique identifier for the `this._triggers` hash + var triggerKey = trigger.replace(/^\S+/, function(triggerName) { + return triggerName + '.' + 'behaviortriggers' + i; + }); + + this._triggers[triggerKey] = this._view._buildViewTrigger(eventName); } }); - - // run the callbacks to "start" the current module - this.triggerMethod("before:start", options); - - this._initializerCallbacks.run(options, this); - this._isInitialized = true; - - this.triggerMethod("start", options); - }, - - // Stop this module by running its finalizers and then stop all of - // the sub-modules for this module - stop: function(){ - // if we are not initialized, don't bother finalizing - if (!this._isInitialized){ return; } - this._isInitialized = false; - - Marionette.triggerMethod.call(this, "before:stop"); - - // stop the sub-modules; depth-first, to make sure the - // sub-modules are stopped / finalized before parents - _.each(this.submodules, function(mod){ mod.stop(); }); - - // run the finalizers - this._finalizerCallbacks.run(undefined,this); - - // reset the initializers and finalizers - this._initializerCallbacks.reset(); - this._finalizerCallbacks.reset(); - - Marionette.triggerMethod.call(this, "stop"); - }, - - // Configure the module with a definition function and any custom args - // that are to be passed in to the definition function - addDefinition: function(moduleDefinition, customArgs){ - this._runModuleDefinition(moduleDefinition, customArgs); - }, - - // Internal method: run the module definition function with the correct - // arguments - _runModuleDefinition: function(definition, customArgs){ - if (!definition){ return; } - - // build the correct list of arguments for the module definition - var args = _.flatten([ - this, - this.app, - Backbone, - Marionette, - Marionette.$, _, - customArgs - ]); - - definition.apply(this, args); - }, - - // Internal method: set up new copies of initializers and finalizers. - // Calling this method will wipe out all existing initializers and - // finalizers. - _setupInitializersAndFinalizers: function(){ - this._initializerCallbacks = new Marionette.Callbacks(); - this._finalizerCallbacks = new Marionette.Callbacks(); - } -}); - -// Type methods to create modules -_.extend(Marionette.Module, { - - // Create a module, hanging off the app parameter as the parent object. - create: function(app, moduleNames, moduleDefinition){ - var module = app; - - // get the custom args passed in after the module definition and - // get rid of the module name and definition function - var customArgs = slice.call(arguments); - customArgs.splice(0, 3); - - // split the module names and get the length - moduleNames = moduleNames.split("."); - var length = moduleNames.length; - - // store the module definition for the last module in the chain - var moduleDefinitions = []; - moduleDefinitions[length-1] = moduleDefinition; - - // Loop through all the parts of the module definition - _.each(moduleNames, function(moduleName, i){ - var parentModule = module; - module = this._getModule(parentModule, moduleName, app, moduleDefinition); - this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); - }, this); - - // Return the last module in the definition chain - return module; - }, - - _getModule: function(parentModule, moduleName, app, def, args){ - var options = _.extend({}, def); - var ModuleClass = this.getClass(def); - - // Get an existing module of this name if we have one - var module = parentModule[moduleName]; - - if (!module){ - // Create a new module if we don't have one - module = new ModuleClass(moduleName, app, options); - parentModule[moduleName] = module; - // store the module on the parent - parentModule.submodules[moduleName] = module; - } - - return module; - }, - - getClass: function(moduleDefinition) { - var ModuleClass = Marionette.Module; - - if (!moduleDefinition) { - return ModuleClass; - } - - if (moduleDefinition.prototype instanceof ModuleClass) { - return moduleDefinition; + + function getBehaviorsUI(behavior) { + return behavior._uiBindings || behavior.ui; } + + return Behaviors; + + })(Marionette, _); + - return moduleDefinition.moduleClass || ModuleClass; - }, - - _addModuleDefinition: function(parentModule, module, def, args){ - var fn; - var startWithParent; - - if (_.isFunction(def)){ - // if a function is supplied for the module definition - fn = def; - startWithParent = true; - - } else if (_.isObject(def)){ - // if an object is supplied - fn = def.define; - startWithParent = !_.isUndefined(def.startWithParent) ? def.startWithParent : true; - - } else { - // if nothing is supplied - startWithParent = true; + // App Router + // ---------- + + // Reduce the boilerplate code of handling route events + // and then calling a single method on another object. + // Have your routers configured to call the method on + // your object, directly. + // + // Configure an AppRouter with `appRoutes`. + // + // App routers can only take one `controller` object. + // It is recommended that you divide your controller + // objects in to smaller pieces of related functionality + // and have multiple routers / controllers, instead of + // just one giant router and controller. + // + // You can also add standard routes to an AppRouter. + + Marionette.AppRouter = Backbone.Router.extend({ + + constructor: function(options) { + this.options = options || {}; + + Backbone.Router.apply(this, arguments); + + var appRoutes = this.getOption('appRoutes'); + var controller = this._getController(); + this.processAppRoutes(controller, appRoutes); + this.on('route', this._processOnRoute, this); + }, + + // Similar to route method on a Backbone Router but + // method is called on the controller + appRoute: function(route, methodName) { + var controller = this._getController(); + this._addAppRoute(controller, route, methodName); + }, + + // process the route event and trigger the onRoute + // method call, if it exists + _processOnRoute: function(routeName, routeArgs) { + // make sure an onRoute before trying to call it + if (_.isFunction(this.onRoute)) { + // find the path that matches the current route + var routePath = _.invert(this.getOption('appRoutes'))[routeName]; + this.onRoute(routeName, routePath, routeArgs); + } + }, + + // Internal method to process the `appRoutes` for the + // router, and turn them in to routes that trigger the + // specified method on the specified `controller`. + processAppRoutes: function(controller, appRoutes) { + if (!appRoutes) { return; } + + var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes + + _.each(routeNames, function(route) { + this._addAppRoute(controller, route, appRoutes[route]); + }, this); + }, + + _getController: function() { + return this.getOption('controller'); + }, + + _addAppRoute: function(controller, route, methodName) { + var method = controller[methodName]; + + if (!method) { + throw new Marionette.Error('Method "' + methodName + '" was not found on the controller'); + } + + this.route(route, methodName, _.bind(method, controller)); + }, + + mergeOptions: Marionette.mergeOptions, + + // Proxy `getOption` to enable getting options from this or this.options by name. + getOption: Marionette.proxyGetOption, + + triggerMethod: Marionette.triggerMethod, + + bindEntityEvents: Marionette.proxyBindEntityEvents, + + unbindEntityEvents: Marionette.proxyUnbindEntityEvents + }); + + // Application + // ----------- + + // Contain and manage the composite application as a whole. + // Stores and starts up `Region` objects, includes an + // event aggregator as `app.vent` + Marionette.Application = Marionette.Object.extend({ + constructor: function(options) { + this._initializeRegions(options); + this._initCallbacks = new Marionette.Callbacks(); + this.submodules = {}; + _.extend(this, options); + this._initChannel(); + Marionette.Object.call(this, options); + }, + + // Command execution, facilitated by Backbone.Wreqr.Commands + execute: function() { + this.commands.execute.apply(this.commands, arguments); + }, + + // Request/response, facilitated by Backbone.Wreqr.RequestResponse + request: function() { + return this.reqres.request.apply(this.reqres, arguments); + }, + + // Add an initializer that is either run at when the `start` + // method is called, or run immediately if added after `start` + // has already been called. + addInitializer: function(initializer) { + this._initCallbacks.add(initializer); + }, + + // kick off all of the application's processes. + // initializes all of the regions that have been added + // to the app, and runs all of the initializer functions + start: function(options) { + this.triggerMethod('before:start', options); + this._initCallbacks.run(options, this); + this.triggerMethod('start', options); + }, + + // Add regions to your app. + // Accepts a hash of named strings or Region objects + // addRegions({something: "#someRegion"}) + // addRegions({something: Region.extend({el: "#someRegion"}) }); + addRegions: function(regions) { + return this._regionManager.addRegions(regions); + }, + + // Empty all regions in the app, without removing them + emptyRegions: function() { + return this._regionManager.emptyRegions(); + }, + + // Removes a region from your app, by name + // Accepts the regions name + // removeRegion('myRegion') + removeRegion: function(region) { + return this._regionManager.removeRegion(region); + }, + + // Provides alternative access to regions + // Accepts the region name + // getRegion('main') + getRegion: function(region) { + return this._regionManager.get(region); + }, + + // Get all the regions from the region manager + getRegions: function() { + return this._regionManager.getRegions(); + }, + + // Create a module, attached to the application + module: function(moduleNames, moduleDefinition) { + + // Overwrite the module class if the user specifies one + var ModuleClass = Marionette.Module.getClass(moduleDefinition); + + var args = _.toArray(arguments); + args.unshift(this); + + // see the Marionette.Module object for more information + return ModuleClass.create.apply(ModuleClass, args); + }, + + // Enable easy overriding of the default `RegionManager` + // for customized region interactions and business-specific + // view logic for better control over single regions. + getRegionManager: function() { + return new Marionette.RegionManager(); + }, + + // Internal method to initialize the regions that have been defined in a + // `regions` attribute on the application instance + _initializeRegions: function(options) { + var regions = _.isFunction(this.regions) ? this.regions(options) : this.regions || {}; + + this._initRegionManager(); + + // Enable users to define `regions` in instance options. + var optionRegions = Marionette.getOption(options, 'regions'); + + // Enable region options to be a function + if (_.isFunction(optionRegions)) { + optionRegions = optionRegions.call(this, options); + } + + // Overwrite current regions with those passed in options + _.extend(regions, optionRegions); + + this.addRegions(regions); + + return this; + }, + + // Internal method to set up the region manager + _initRegionManager: function() { + this._regionManager = this.getRegionManager(); + this._regionManager._parent = this; + + this.listenTo(this._regionManager, 'before:add:region', function() { + Marionette._triggerMethod(this, 'before:add:region', arguments); + }); + + this.listenTo(this._regionManager, 'add:region', function(name, region) { + this[name] = region; + Marionette._triggerMethod(this, 'add:region', arguments); + }); + + this.listenTo(this._regionManager, 'before:remove:region', function() { + Marionette._triggerMethod(this, 'before:remove:region', arguments); + }); + + this.listenTo(this._regionManager, 'remove:region', function(name) { + delete this[name]; + Marionette._triggerMethod(this, 'remove:region', arguments); + }); + }, + + // Internal method to setup the Wreqr.radio channel + _initChannel: function() { + this.channelName = _.result(this, 'channelName') || 'global'; + this.channel = _.result(this, 'channel') || Backbone.Wreqr.radio.channel(this.channelName); + this.vent = _.result(this, 'vent') || this.channel.vent; + this.commands = _.result(this, 'commands') || this.channel.commands; + this.reqres = _.result(this, 'reqres') || this.channel.reqres; } - - // add module definition if needed - if (fn){ - module.addDefinition(fn, args); + }); + + /* jshint maxparams: 9 */ + + // Module + // ------ + + // A simple module system, used to create privacy and encapsulation in + // Marionette applications + Marionette.Module = function(moduleName, app, options) { + this.moduleName = moduleName; + this.options = _.extend({}, this.options, options); + // Allow for a user to overide the initialize + // for a given module instance. + this.initialize = options.initialize || this.initialize; + + // Set up an internal store for sub-modules. + this.submodules = {}; + + this._setupInitializersAndFinalizers(); + + // Set an internal reference to the app + // within a module. + this.app = app; + + if (_.isFunction(this.initialize)) { + this.initialize(moduleName, app, this.options); } - - // `and` the two together, ensuring a single `false` will prevent it - // from starting with the parent - module.startWithParent = module.startWithParent && startWithParent; - - // setup auto-start if needed - if (module.startWithParent && !module.startWithParentIsConfigured){ - - // only configure this once + }; + + Marionette.Module.extend = Marionette.extend; + + // Extend the Module prototype with events / listenTo, so that the module + // can be used as an event aggregator or pub/sub. + _.extend(Marionette.Module.prototype, Backbone.Events, { + + // By default modules start with their parents. + startWithParent: true, + + // Initialize is an empty function by default. Override it with your own + // initialization logic when extending Marionette.Module. + initialize: function() {}, + + // Initializer for a specific module. Initializers are run when the + // module's `start` method is called. + addInitializer: function(callback) { + this._initializerCallbacks.add(callback); + }, + + // Finalizers are run when a module is stopped. They are used to teardown + // and finalize any variables, references, events and other code that the + // module had set up. + addFinalizer: function(callback) { + this._finalizerCallbacks.add(callback); + }, + + // Start the module, and run all of its initializers + start: function(options) { + // Prevent re-starting a module that is already started + if (this._isInitialized) { return; } + + // start the sub-modules (depth-first hierarchy) + _.each(this.submodules, function(mod) { + // check to see if we should start the sub-module with this parent + if (mod.startWithParent) { + mod.start(options); + } + }); + + // run the callbacks to "start" the current module + this.triggerMethod('before:start', options); + + this._initializerCallbacks.run(options, this); + this._isInitialized = true; + + this.triggerMethod('start', options); + }, + + // Stop this module by running its finalizers and then stop all of + // the sub-modules for this module + stop: function() { + // if we are not initialized, don't bother finalizing + if (!this._isInitialized) { return; } + this._isInitialized = false; + + this.triggerMethod('before:stop'); + + // stop the sub-modules; depth-first, to make sure the + // sub-modules are stopped / finalized before parents + _.invoke(this.submodules, 'stop'); + + // run the finalizers + this._finalizerCallbacks.run(undefined, this); + + // reset the initializers and finalizers + this._initializerCallbacks.reset(); + this._finalizerCallbacks.reset(); + + this.triggerMethod('stop'); + }, + + // Configure the module with a definition function and any custom args + // that are to be passed in to the definition function + addDefinition: function(moduleDefinition, customArgs) { + this._runModuleDefinition(moduleDefinition, customArgs); + }, + + // Internal method: run the module definition function with the correct + // arguments + _runModuleDefinition: function(definition, customArgs) { + // If there is no definition short circut the method. + if (!definition) { return; } + + // build the correct list of arguments for the module definition + var args = _.flatten([ + this, + this.app, + Backbone, + Marionette, + Backbone.$, _, + customArgs + ]); + + definition.apply(this, args); + }, + + // Internal method: set up new copies of initializers and finalizers. + // Calling this method will wipe out all existing initializers and + // finalizers. + _setupInitializersAndFinalizers: function() { + this._initializerCallbacks = new Marionette.Callbacks(); + this._finalizerCallbacks = new Marionette.Callbacks(); + }, + + // import the `triggerMethod` to trigger events with corresponding + // methods if the method exists + triggerMethod: Marionette.triggerMethod + }); + + // Class methods to create modules + _.extend(Marionette.Module, { + + // Create a module, hanging off the app parameter as the parent object. + create: function(app, moduleNames, moduleDefinition) { + var module = app; + + // get the custom args passed in after the module definition and + // get rid of the module name and definition function + var customArgs = _.drop(arguments, 3); + + // Split the module names and get the number of submodules. + // i.e. an example module name of `Doge.Wow.Amaze` would + // then have the potential for 3 module definitions. + moduleNames = moduleNames.split('.'); + var length = moduleNames.length; + + // store the module definition for the last module in the chain + var moduleDefinitions = []; + moduleDefinitions[length - 1] = moduleDefinition; + + // Loop through all the parts of the module definition + _.each(moduleNames, function(moduleName, i) { + var parentModule = module; + module = this._getModule(parentModule, moduleName, app, moduleDefinition); + this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); + }, this); + + // Return the last module in the definition chain + return module; + }, + + _getModule: function(parentModule, moduleName, app, def, args) { + var options = _.extend({}, def); + var ModuleClass = this.getClass(def); + + // Get an existing module of this name if we have one + var module = parentModule[moduleName]; + + if (!module) { + // Create a new module if we don't have one + module = new ModuleClass(moduleName, app, options); + parentModule[moduleName] = module; + // store the module on the parent + parentModule.submodules[moduleName] = module; + } + + return module; + }, + + // ## Module Classes + // + // Module classes can be used as an alternative to the define pattern. + // The extend function of a Module is identical to the extend functions + // on other Backbone and Marionette classes. + // This allows module lifecyle events like `onStart` and `onStop` to be called directly. + getClass: function(moduleDefinition) { + var ModuleClass = Marionette.Module; + + if (!moduleDefinition) { + return ModuleClass; + } + + // If all of the module's functionality is defined inside its class, + // then the class can be passed in directly. `MyApp.module("Foo", FooModule)`. + if (moduleDefinition.prototype instanceof ModuleClass) { + return moduleDefinition; + } + + return moduleDefinition.moduleClass || ModuleClass; + }, + + // Add the module definition and add a startWithParent initializer function. + // This is complicated because module definitions are heavily overloaded + // and support an anonymous function, module class, or options object + _addModuleDefinition: function(parentModule, module, def, args) { + var fn = this._getDefine(def); + var startWithParent = this._getStartWithParent(def, module); + + if (fn) { + module.addDefinition(fn, args); + } + + this._addStartWithParent(parentModule, module, startWithParent); + }, + + _getStartWithParent: function(def, module) { + var swp; + + if (_.isFunction(def) && (def.prototype instanceof Marionette.Module)) { + swp = module.constructor.prototype.startWithParent; + return _.isUndefined(swp) ? true : swp; + } + + if (_.isObject(def)) { + swp = def.startWithParent; + return _.isUndefined(swp) ? true : swp; + } + + return true; + }, + + _getDefine: function(def) { + if (_.isFunction(def) && !(def.prototype instanceof Marionette.Module)) { + return def; + } + + if (_.isObject(def)) { + return def.define; + } + + return null; + }, + + _addStartWithParent: function(parentModule, module, startWithParent) { + module.startWithParent = module.startWithParent && startWithParent; + + if (!module.startWithParent || !!module.startWithParentIsConfigured) { + return; + } + module.startWithParentIsConfigured = true; - - // add the module initializer config - parentModule.addInitializer(function(options){ - if (module.startWithParent){ + + parentModule.addInitializer(function(options) { + if (module.startWithParent) { module.start(options); } }); - } - - } -}); - - + }); + return Marionette; -})(this, Backbone, _); +})); |