/*!
backbone-bootstrap-modals 0.1.3
http://github.com/leafygreen/backbone-bootstrap-modals
Licensed under the MIT license.
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
/*!
backbone-bootstrap-modals 0.1.3
http://github.com/leafygreen/backbone-bootstrap-modals
Licensed under the MIT license.
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
AMD (+ global for extensions)
define(["underscore", "backbone"], function (_, Backbone) {
return (root.BackboneBootstrapModals = factory(_, Backbone));
});
} else if (typeof exports === "object") {
CommonJS
module.exports = factory(require("underscore"), require("backbone"));
} else {
Browser
root.BackboneBootstrapModals = factory(root._, root.Backbone);
}}(this, function (_, Backbone) {
"use strict";
Create Project Namespace
var BackboneBootstrapModals = {};
A simple view representing the modal title and dismiss icon.
BackboneBootstrapModals.BaseHeaderView = Backbone.View.extend({
className: 'modal-header',
initialize: function (opts) {
var options = opts || {};
this.label = options.label || '';
this.labelId = options.labelId || 'myModalLabel';
this.labelTagName = options.labelTagName || 'h4';
this.showClose = (options.showClose !== undefined) ? options.showClose : true;
},
render: function() {
var $header = $('<'+this.labelTagName+'>', {
'id': this.labelId,
'class': 'modal-title'
}).text(this.label);
var $close;
if (this.showClose) {
$close = $('<button>', {
'type': 'button',
'class': 'close',
'data-dismiss': 'modal',
'aria-hidden': 'true'
}).html('×');
}
this.$el.html([$close, $header]);
return this;
}
});
A simple view representing a minimal modal body
BackboneBootstrapModals.BaseBodyView = Backbone.View.extend({
className: 'modal-body',
initialize: function (opts) {
var options = opts || {};
this.text = options.text;
this.textTagName = options.textTagName || 'p';
},
render: function() {
var html;
if (this.text) {
if (_.isArray(this.text)) {
html = _.map(this.text, _.bind(this.createTag, this));
} else {
html = this.createTag(this.text);
}
this.$el.html(html);
}
return this;
},
createTag: function(text) {
var $tag = $('<'+this.textTagName+'>').text(text);
return $tag;
}
});
A simple view representing a set of modal action buttons
BackboneBootstrapModals.BaseFooterView = Backbone.View.extend({
className: 'modal-footer',
initialize: function (opts) {
var options = opts || {};
this.buttons = options.buttons || [];
},
render: function() {
function createButton(button) {
var $btn = $('<button>', _.extend({
'id': button.id,
'class': button.className
}, button.attributes)).text(button.value);
return $btn;
}
this.$el.html(this.buttons.map(createButton));
return this;
}
});
The base class all other modals extend.
BackboneBootstrapModals.BaseModal = Backbone.View.extend({
className: 'modal',
attributes: {
'tabindex': '-1',
'role': 'dialog',
'aria-labelledby': 'myModalLabel'
},
Handlers for Bootstrap Modal Events: http://getbootstrap.com/javascript/#modals
bootstrapModalEvents: {
'show.bs.modal': 'onShowBsModal',
'shown.bs.modal': 'onShownBsModal',
'hide.bs.modal': 'onHideBsModal',
'hidden.bs.modal': 'onHiddenBsModal'
},
The default views if not overridden or specified in options
headerView: BackboneBootstrapModals.BaseHeaderView,
bodyView: BackboneBootstrapModals.BaseBodyView,
footerView: BackboneBootstrapModals.BaseFooterView,
The default modal options if not overridden or specified in options
modalOptions: {
backdrop: true,
keyboard: true
},
properties to copy from options
modalProperties: [
'modalOptions',
'headerView',
'headerViewOptions',
'bodyView',
'bodyViewOptions',
'footerView',
'footerViewOptions'
],
constructor: function(opts) {
var options = opts || {};
this.shown = false;
_.extend(this, _.pick(options, this.modalProperties));
Backbone.View.prototype.constructor.apply(this, arguments);
},
render: function() {
Remove any existing views before appending subviews to the layout
this.removeSubviews();
this.initializeSubviews();
this.$el.html([
'<div class="modal-dialog">',
'<div class="modal-content">',
'</div>',
'</div>'
].join(''));
var $modalContent = this.$el.find('.modal-content');
if (this.headerView) {
$modalContent.append(this.headerViewInstance.render().$el);
}
if (this.bodyView) {
$modalContent.append(this.bodyViewInstance.render().$el);
}
if (this.footerView) {
$modalContent.append(this.footerViewInstance.render().$el);
}
Allow onRender callback for custom hooks
if (this.onRender) { this.onRender.call(this); }
if (!this.shown && this.modalOptions.show !== false) {
this.$el.modal(this.modalOptions);
}
return this;
},
Initialize views for header, body, and footer sections.
initializeSubviews: function() {
this.headerViewInstance = this.buildSubview(
this.getHeaderView(),
_.result(this, 'headerViewOptions'),
'modal-header');
this.bodyViewInstance = this.buildSubview(
this.getBodyView(),
_.result(this, 'bodyViewOptions'),
'modal-body');
this.footerViewInstance = this.buildSubview(
this.getFooterView(),
_.result(this, 'footerViewOptions'),
'modal-footer');
},
Accessors that can be overridden to allow dynamic subview definitions
getHeaderView: function() {
return this.headerView;
},
getBodyView: function() {
return this.bodyView;
},
getFooterView: function() {
return this.footerView;
},
Construct view instance with specified options and additionally propagate model/collection/className attributes
buildSubview: function(viewClass, viewOptions, defaultClassName) {
if (!viewClass) {
throw new Error("view not specified");
}
var options = _.extend({
model: this.model,
collection: this.collection
}, viewOptions);
Ensure the proper className if not specified through the viewClass or viewOptions
if (!(viewClass.prototype.className || options.className)) {
options.className = defaultClassName;
}
return new viewClass(options);
},
remove: function () {
this.removeSubviews();
Backbone.View.prototype.remove.apply(this, arguments);
Allow onClose callback for custom hooks
if (this.onClose) { this.onClose.call(this); }
return this;
},
removeSubviews: function() {
if (this.headerViewInstance) { this.removeSubview(this.headerViewInstance); }
if (this.bodyViewInstance) { this.removeSubview(this.bodyViewInstance); }
if (this.footerViewInstance) { this.removeSubview(this.footerViewInstance); }
},
Attempt to use Marionette’s close first, falling back to Backbone’s remove
removeSubview: function(viewInstance) {
if (Backbone.Marionette && viewInstance.close) {
viewInstance.close.apply(viewInstance);
} else if (viewInstance.remove) {
viewInstance.remove.apply(viewInstance);
}
},
Override default implementation to always include bootstrapModalEvents without clobbering the default events hash
delegateEvents: function(events) {
var combinedEvents = events || _.result(this, 'events') || {};
_.each(this.getAdditionalEventsToDelegate(), function(eventHash) {
_.extend(combinedEvents, eventHash);
});
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
},
Helper method for use in overridden delegateEvents call. This can be overridden in extended classes to provide additional events, e.g. ConfirmationModal.confirmationEvents
getAdditionalEventsToDelegate: function() {
return [this.bootstrapModalEvents];
},
show: function() {
this.$el.modal('show');
},
hide: function() {
this.$el.modal('hide');
},
This event fires immediately when the show instance method is called.
onShowBsModal: function() {
if (this.onShow) { this.onShow.call(this); }
},
This event is fired when the modal has been made visible to the user (will wait for CSS transitions to complete).
onShownBsModal: function() {
this.shown = true;
if (this.onShown) { this.onShown.call(this); }
},
This event is fired immediately when the hide instance method has been called.
onHideBsModal: function() {
if (this.onHide) { this.onHide.call(this); }
},
This event is fired when the modal has finished being hidden from the user (will wait for CSS transitions to complete).
onHiddenBsModal: function() {
this.shown = false;
this.remove();
if (this.onHidden) { this.onHidden.call(this); }
},
});
A simple modal for simple confirmation dialogs
BackboneBootstrapModals.ConfirmationModal = BackboneBootstrapModals.BaseModal.extend({
confirmationEvents: {
'click #confirmation-confirm-btn': 'onClickConfirm',
'keypress .modal-body': 'onKeyPress'
},
Default set of BaseModal options for use as ConfirmationModal
headerViewOptions: function() {
return {
label: _.result(this, 'label'),
labelId: _.result(this, 'labelId'),
labelTagName: _.result(this, 'labelTagName'),
showClose: _.result(this, 'showClose')
};
},
bodyViewOptions: function() {
return {
text: _.result(this, 'text'),
textTagName: _.result(this, 'textTagName')
};
},
footerViewOptions: function() {
var buttons = [],
showCancel = _.result(this, 'showCancel');
if (showCancel === undefined || showCancel === true) {
buttons.push({
id: 'confirmation-cancel-btn',
className: 'btn '+ (_.result(this, 'cancelClassName') || 'btn-default'),
value: (_.result(this, 'cancelText') || 'Cancel'),
attributes: { 'data-dismiss': 'modal', 'aria-hidden': 'true' }
});
}
buttons.push({
id: 'confirmation-confirm-btn',
className: 'btn '+ (_.result(this, 'confirmClassName') || 'btn-primary'),
value: (_.result(this, 'confirmText') || 'Confirm')
});
return {
buttons: buttons
};
},
properties to copy from options
confirmationProperties: [
'label',
'labelId',
'labelTagName',
'showClose',
'text',
'textTagName',
'confirmText',
'confirmClassName',
'cancelText',
'cancelClassName',
'showCancel',
'onConfirm'
],
initialize: function(opts) {
var options = opts || {};
_.extend(this, _.pick(options, this.confirmationProperties));
},
Override BaseModal hook to add additional default delegated events
getAdditionalEventsToDelegate: function() {
var eventHashes = BackboneBootstrapModals.BaseModal.prototype.getAdditionalEventsToDelegate.call(this);
return eventHashes.concat(this.confirmationEvents);
},
onKeyPress: function(e) {
if the user presses the enter key
if (e.which === 13) {
e.preventDefault();
this.$('#confirmation-confirm-btn').click();
}
},
onClickConfirm: function(e) {
e.preventDefault();
e.currentTarget.disabled = true;
this.confirmProgress(e);
},
confirmProgress: function(e){
Execute the specified callback if it exists, then hide the modal. The modal will not be hidden if the callback returns false.
if (this.onConfirm) {
if (this.onConfirm.call(this, e) !== false) {
this.hide();
}
} else {
this.hide();
}
}
});
A simple modal for dialogs with multi-step wizard behavior
BackboneBootstrapModals.WizardModal = BackboneBootstrapModals.BaseModal.extend({
wizardEvents: {
'click #confirmation-previous-btn': 'onClickPrevious',
'click #confirmation-next-btn': 'onClickNext',
'keypress .modal-body': 'onKeyPress'
},
getBodyView: function(options) {
return this.currentStep.view;
},
headerViewOptions: function() {
return {
label: this.currentStep.label
};
},
bodyViewOptions: function() {
return this.currentStep.viewOptions;
},
footerViewOptions: function() {
var buttons = this.getButtonsForCurrentStep();
return {
buttons: buttons
};
},
initialize: function(opts) {
this.initializeSteps(opts);
this.setCurrentStep(0); // always start with the first element
},
initializeSteps: function(opts) {
Override any defined stepGraph with the one in options if present
if (opts.stepGraph) {
this.stepGraph = opts.stepGraph;
}
Validate the steps structure
if (!this.stepGraph || !_.isArray(this.stepGraph) || !this.stepGraph.length) {
throw new Error("steps array must be specified and non-empty");
}
},
setCurrentStep: function(index) {
this.currentStep = this.stepGraph[index];
},
getButtonsForCurrentStep: function() {
var previousStepIndex = this.currentStep.previousIndex,
previousText = this.currentStep.previousText || 'Previous',
previousClassName = this.currentStep.previousClassName || 'btn-default',
nextStepIndex = this.currentStep.nextIndex,
nextText = this.currentStep.nextText || 'Next',
nextClassName = this.currentStep.nextClassName || 'btn-primary',
buttons = [];
if (previousStepIndex !== undefined) {
buttons.push({ id: 'confirmation-previous-btn', className: 'btn '+previousClassName, value: previousText });
}
buttons.push({ id: 'confirmation-next-btn', className: 'btn '+nextClassName, value: nextText });
return buttons;
},
Override BaseModal hook to add additional default delegated events
getAdditionalEventsToDelegate: function() {
var eventHashes = BackboneBootstrapModals.BaseModal.prototype.getAdditionalEventsToDelegate.call(this);
return eventHashes.concat(this.wizardEvents);
},
renderPreviousStep: function() {
this.setCurrentStep(this.currentStep.previousIndex);
this.render();
},
renderNextStep: function() {
var nextStepIndex = this.currentStep.nextIndex;
if (nextStepIndex !== undefined) {
this.setCurrentStep(nextStepIndex);
this.render();
} else {
If no more steps, hide the dialog
this.hide();
}
},
onClickPrevious: function(e) {
e.preventDefault();
e.currentTarget.disabled = true;
this.renderPreviousStep();
},
onKeyPress: function(e) {
if the user presses the enter key
if (e.which === 13) {
e.preventDefault();
this.$('#confirmation-next-btn').click();
}
},
onClickNext: function(e) {
e.preventDefault();
e.currentTarget.disabled = true;
this.goForward(e);
},
goForward: function(e) {
Execute the specified callback if it exists, then proceed. The modal will not proceed if the callback returns false.
if (this.currentStep.onNext) {
if (this.currentStep.onNext.call(this, e) === false) {
return;
}
}
this.renderNextStep();
}
});
return BackboneBootstrapModals;
}));