blob: 7151ae18d45c05727cc0303a92aa07ac018467f0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* brooklyn extensions for supporting views */
define([
"jquery", "underscore", "backbone", "brooklyn-utils",
"text!tpl/lib/basic-modal.html",
"text!tpl/lib/config-key-type-value-input-pair.html"
], function (
$, _, Backbone, Util,
ModalHtml, ConfigKeyInputHtml
) {
var module = {};
module.refresh = true;
/** Toggles automatic refreshes of instances of View. */
module.toggleRefresh = function () {
this.refresh = !this.refresh;
return this.refresh;
};
// TODO this customising of the View prototype could be expanded to include
// other methods from viewutils. see discussion at
// https://github.com/brooklyncentral/brooklyn/pull/939
// add close method to all views for clean-up
// (NB we have to update the prototype _here_ before any views are instantiated;
// see "close" called below in "showView")
Backbone.View.prototype.close = function () {
// call user defined close method if exists
this.viewIsClosed = true;
if (_.isFunction(this.beforeClose)) {
this.beforeClose();
}
_.each(this._periodicFunctions, function (i) {
clearInterval(i);
});
this.remove();
this.unbind();
};
Backbone.View.prototype.viewIsClosed = false;
/**
* Registers a callback (cf setInterval) that is unregistered cleanly when the view
* closes. The callback is run in the context of the owning view, so callbacks can
* refer to 'this' safely.
*/
Backbone.View.prototype.callPeriodically = function (uid, callback, interval) {
if (!this._periodicFunctions) {
this._periodicFunctions = {};
}
var old = this._periodicFunctions[uid];
if (old) clearInterval(old);
// Wrap callback in function that checks whether updates are enabled
var periodic = function () {
if (module.refresh) {
callback.apply(this);
}
};
// Bind this to the view
periodic = _.bind(periodic, this);
this._periodicFunctions[uid] = setInterval(periodic, interval);
};
/**
* A form that listens to modifications to its inputs, maintaining a model that is
* submitted when a button with class 'submit' is clicked.
*
* Expects a body view or a template function to render.
*/
module.Form = Backbone.View.extend({
events: {
"change": "onChange",
"submit": "onSubmit"
},
initialize: function() {
if (!this.options.body && !this.options.template) {
throw new Error("body view or template function required by GenericForm");
} else if (!this.options.onSubmit) {
throw new Error("onSubmit function required by GenericForm");
}
this.onSubmitCallback = this.options.onSubmit;
this.model = new (this.options.model || Backbone.Model);
_.bindAll(this, "onSubmit", "onChange");
this.render();
},
beforeClose: function() {
if (this.options.body) {
this.options.body.close();
}
},
render: function() {
if (this.options.body) {
this.options.body.render();
this.$el.html(this.options.body.$el);
} else {
this.$el.html(this.options.template());
}
// Initialise the model with existing values
Util.bindModelFromForm(this.model, this.$el);
return this;
},
onChange: function(e) {
var target = $(e.target);
var name = target.attr("name");
this.model.set(name, Util.inputValue(target), { silent: true });
},
onSubmit: function(e) {
e.preventDefault();
// Could validate model
this.onSubmitCallback(this.model.clone());
return false;
}
});
/**
* A view to render another view in a modal. Give another view to render as
* the `body' parameter that has an onSubmit function that will be called
* when the modal's `Save' button is clicked, and/or an onCancel callback
* that will be called when the modal is closed without saving.
*
* The onSubmit callback should return either:
* <ul>
* <li><b>nothing</b>: the callback is treated as successful
* <li><b>true</b> or <b>false</b>: the callback is treated as appropriate
* <li>a <b>promise</b> with `done' and `fail' callbacks (for example a jqXHR object):
* The callback is treated as successful when the promise is done without error.
* <li><b>anything else</b>: the callback is treated as successful
* </ul>
* When the onSubmit callback is successful the modal is closed.
*
* The return value of the onCancel callback is ignored.
*
* The modal will still be open and visible when the onSubmit callback is called.
* The modal will have been closed when the onCancel callback is called.
*/
module.Modal = Backbone.View.extend({
id: _.uniqueId("modal"),
className: "modal",
template: _.template(ModalHtml),
events: {
"hide": "onClose",
"click .modal-submit": "onSubmit"
},
initialize: function() {
if (!this.options.body) {
throw new Error("Modal view requires body to render");
}
_.bindAll(this, "onSubmit", "onCancel", "show");
if (this.options.autoOpen) {
this.show();
}
},
beforeClose: function() {
if (this.options.body) {
this.options.body.close();
}
},
render: function() {
var optionalTitle = this.options.body.title;
var title = _.isFunction(optionalTitle)
? optionalTitle()
: _.isString(optionalTitle)
? optionalTitle : this.options.title;
this.$el.html(this.template({
title: title,
submitButtonText: this.options.submitButtonText || "Apply",
cancelButtonText: this.options.cancelButtonText || "Cancel"
}));
this.options.body.render();
this.$(".modal-body").html(this.options.body.$el);
return this;
},
show: function() {
this.render().$el.modal();
return this;
},
onSubmit: function(event) {
if (_.isFunction(this.options.body.onSubmit)) {
var submission = this.options.body.onSubmit.apply(this.options.body, [event]);
var self = this;
var submissionSuccess = function() {
// Closes view via event.
self.closingSuccessfully = true;
self.$el.modal("hide");
};
var submissionFailure = function () {
// Better response.
console.log("modal submission failed!", arguments);
};
// Assuming no value is fine
if (!submission) {
submission = true;
}
if (_.isBoolean(submission) && submission) {
submissionSuccess();
} else if (_.isBoolean(submission)) {
submissionFailure();
} else if (_.isFunction(submission.done) && _.isFunction(submission.fail)) {
submission.done(submissionSuccess).fail(submissionFailure);
} else {
// assuming success and closing modal
submissionSuccess()
}
}
return false;
},
onCancel: function () {
if (_.isFunction(this.options.body.onCancel)) {
this.options.body.onCancel.apply(this.options.body);
}
},
onClose: function () {
if (!this.closingSuccessfully) {
this.onCancel();
}
this.close();
}
});
/**
* Shows a modal with yes/no buttons as a user confirmation prompt.
* @param {string} question The message to show in the body of the modal
* @param {string} [title] An optional title to show. Uses generic default if not given.
* @returns {jquery.Deferred} The promise from a jquery.Deferred object. The
* promise is resolved if the modal was submitted normally and rejected
* otherwise.
*/
module.requestConfirmation = function (question, title) {
var deferred = $.Deferred();
var Confirmation = Backbone.View.extend({
title: title || "Confirm action",
render: function () {
this.$el.html(question || "");
},
onSubmit: function () {
deferred.resolve();
},
onCancel: function () {
deferred.reject();
}
});
new module.Modal({
body: new Confirmation(),
autoOpen: true,
submitButtonText: "Yes",
cancelButtonText: "No"
});
return deferred.promise();
};
/** Creates, displays and returns a modal with the given view used as its body */
module.showModalWith = function (bodyView) {
return new module.Modal({body: bodyView}).show();
};
/**
* Presents inputs for config key names/values with buttons to add/remove entries
* and a function to extract a map of name->value.
*/
module.ConfigKeyInputPairList = Backbone.View.extend({
template: _.template(ConfigKeyInputHtml),
// Could listen to input change events and add 'error' class to any type inputs
// that duplicate values.
events: {
"click .config-key-row-remove": "rowRemove",
"keypress .last": "rowAdd"
},
render: function () {
if (this.options.configKeys) {
var templated = _.map(this.options.configKeys, function (value, key) {
return this.templateRow(key, value);
}, this);
this.$el.html(templated.join(""));
}
this.$el.append(this.templateRow());
this.markLast();
return this;
},
rowAdd: function (event) {
this.$el.append(this.templateRow());
this.markLast();
},
rowRemove: function (event) {
$(event.currentTarget).parent().remove();
if (this.$el.children().length == 0) {
this.rowAdd();
}
this.markLast();
},
markLast: function () {
this.$(".last").removeClass("last");
// Marking inputs rather than parent div to avoid weird behaviour when
// remove row button is triggered with the keyboard.
this.$(".config-key-type").last().addClass("last");
this.$(".config-key-value").last().addClass("last");
},
templateRow: function (type, value) {
return this.template({type: type || "", value: value || ""});
},
getConfigKeys: function () {
var cks = {};
this.$(".config-key-type").each(function (index, input) {
input = $(input);
var type = input.val() && input.val().trim();
var value = input.next().val() && input.next().val().trim();
if (type && value) {
cks[type] = value;
}
});
return cks;
}
});
return module;
});