Files
pgadmin4/web/pgadmin/static/js/backform.js
Ashesh Vashi aa150030eb Introduced a mechanism to load required javascripts at runtime
(lazy loading) using the require.js. This allows us to load the
javascript required for any node, only when it was loaded in the browser
tree. Also, introduced the mechanism to show/edit/create of any node in
a tab panel (wcDocker.Panel).
2015-06-30 11:21:57 +05:30

585 lines
21 KiB
JavaScript

/*
Backform
http://github.com/amiliaapp/backform
Copyright (c) 2014 Amilia Inc.
Written by Martin Drapeau
Licensed under the MIT @license
*/
(function(root, factory) {
// Set up Backform appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'backbone'], function(_, $, Backbone) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backform.
return factory(root, _, $, Backbone);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore');
factory(root, _, (root.jQuery || root.$ || root.Zepto || root.ender), root.Backbone);
// Finally, as a browser global.
} else {
factory(root, root._, (root.jQuery || root.Zepto || root.ender || root.$), root.Backbone);
}
} (this, function(root, _, $, Backbone) {
// Backform namespace and global options
Backform = root.Backform = {
// HTML markup global class names. More can be added by individual controls
// using _.extend. Look at RadioControl as an example.
formClassName: "backform form-horizontal",
groupClassName: "form-group",
controlLabelClassName: "control-label col-sm-4",
controlsClassName: "col-sm-8",
controlClassName: "form-control",
helpClassName: "help-block",
errorClassName: "has-error",
helpMessageClassName: "help-block",
// Bootstrap 2.3 adapter
bootstrap2: function() {
_.extend(Backform, {
groupClassName: "control-group",
controlLabelClassName: "control-label",
controlsClassName: "controls",
controlClassName: "input-xlarge",
helpClassName: "text-error",
errorClassName: "error",
helpMessageClassName: "help-message small"
});
_.each(Backform, function(value, name) {
if (_.isFunction(Backform[name]) &&
_.isFunction(Backform[name].prototype["bootstrap2"]))
Backform[name].prototype["bootstrap2"]();
});
},
// https://github.com/wyuenho/backgrid/blob/master/lib/backgrid.js
resolveNameToClass: function(name, suffix) {
if (_.isString(name)) {
var key = _.map(name.split('-'), function(e) {
return e.slice(0, 1).toUpperCase() + e.slice(1);
}).join('') + suffix;
var klass = Backform[key];
if (_.isUndefined(klass)) {
throw new ReferenceError("Class '" + key + "' not found");
}
return klass;
}
return name;
}
};
// Backform Form view
// A collection of field models.
var Form = Backform.Form = Backbone.View.extend({
fields: undefined,
errorModel: undefined,
tagName: "form",
className: function() {
return Backform.formClassName;
},
initialize: function(options) {
if (!(options.fields instanceof Backbone.Collection))
options.fields = new Fields(options.fields || this.fields);
this.fields = options.fields;
this.model.errorModel = options.errorModel || this.model.errorModel || new Backbone.Model();
this.controls = [];
},
cleanup: function() {
_.each(this.controls, function(c) {
c.remove();
});
this.controls.length = 0;
},
remove: function() {
/* First do the clean up */
this.cleanup();
Backbone.View.prototype.remove.apply(this, arguments);
},
render: function() {
this.cleanup();
this.$el.empty();
var form = this,
$form = this.$el,
model = this.model,
controls = this.controls;
this.fields.each(function(field) {
var control = new (field.get("control"))({
field: field,
model: model
});
$form.append(control.render().$el);
controls.push(control);
});
return this;
}
});
// Converting data to/from Model/DOM.
// Stolen directly from Backgrid's CellFormatter.
// Source: http://backgridjs.com/ref/formatter.html
/**
Just a convenient class for interested parties to subclass.
The default Cell classes don't require the formatter to be a subclass of
Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods
are defined.
@abstract
@class Backform.ControlFormatter
@constructor
*/
var ControlFormatter = Backform.ControlFormatter = function() {};
_.extend(ControlFormatter.prototype, {
/**
Takes a raw value from a model and returns an optionally formatted string
for display. The default implementation simply returns the supplied value
as is without any type conversion.
@member Backform.ControlFormatter
@param {*} rawData
@param {Backbone.Model} model Used for more complicated formatting
@return {*}
*/
fromRaw: function (rawData, model) {
return rawData;
},
/**
Takes a formatted string, usually from user input, and returns a
appropriately typed value for persistence in the model.
If the user input is invalid or unable to be converted to a raw value
suitable for persistence in the model, toRaw must return `undefined`.
@member Backform.ControlFormatter
@param {string} formattedData
@param {Backbone.Model} model Used for more complicated formatting
@return {*|undefined}
*/
toRaw: function (formattedData, model) {
return formattedData;
}
});
// Store value in DOM as stringified JSON.
var JSONFormatter = Backform.JSONFormatter = function() {};
_.extend(JSONFormatter.prototype, {
fromRaw: function(rawData, model) {
return JSON.stringify(rawData);
},
toRaw: function(formattedData, model) {
return JSON.parse(formattedData);
}
});
// Field model and collection
// A field maps a model attriute to a control for rendering and capturing user input
var Field = Backform.Field = Backbone.Model.extend({
defaults: {
name: "", // Name of the model attribute; accepts "." nested path (e.g. x.y.z)
placeholder: "",
disabled: false,
required: false,
value: undefined, // Optional. Default value when model is empty.
control: undefined, // Control name or class
formatter: undefined
},
initialize: function() {
var control = Backform.resolveNameToClass(this.get("control"), "Control");
this.set({control: control}, {silent: true});
}
});
var Fields = Backform.Fields = Backbone.Collection.extend({
model: Field
});
// Base Control class
var Control = Backform.Control = Backbone.View.extend({
defaults: {}, // Additional field defaults
className: function() {
return Backform.groupClassName;
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <span class="<%=Backform.controlClassName%> uneditable-input"><%=value%></span>',
'</div>'
].join("\n")),
initialize: function(options) {
this.field = options.field; // Back-reference to the field
var formatter = Backform.resolveNameToClass(this.field.get("formatter") || this.formatter, "Formatter");
if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) {
formatter = new formatter();
}
this.formatter = formatter;
var attrArr = this.field.get('name').split('.');
var name = attrArr.shift();
this.listenTo(this.model, "change:" + name, this.render);
if (this.model.errorModel instanceof Backbone.Model)
this.listenTo(this.model.errorModel, "change:" + name, this.updateInvalid);
},
formatter: ControlFormatter,
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find(".uneditable-input").text(), this.model);
},
onChange: function(e) {
var model = this.model,
$el = $(e.target),
attrArr = this.field.get("name").split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
value = this.getValueFromDOM(),
changes = {};
if (this.model.errorModel instanceof Backbone.Model) {
if (_.isEmpty(path)) {
this.model.errorModel.unset(name);
} else {
var nestedError = this.model.errorModel.get(name);
if (nestedError) {
this.keyPathSetter(nestedError, path, null);
this.model.errorModel.set(name, nestedError);
}
}
}
changes[name] = _.isEmpty(path) ? value : _.clone(model.get(name)) || {};
if (!_.isEmpty(path)) this.keyPathSetter(changes[name], path, value);
this.stopListening(this.model, "change:" + name, this.render);
model.set(changes);
this.listenTo(this.model, "change:" + name, this.render);
},
render: function() {
var field = _.defaults(this.field.toJSON(), this.defaults),
attributes = this.model.toJSON(),
attrArr = field.name.split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
rawValue = this.keyPathAccessor(attributes[name], path),
data = _.extend(field, {
rawValue: rawValue,
value: this.formatter.fromRaw(rawValue, this.model),
attributes: attributes,
formatter: this.formatter
});
this.$el.html(this.template(data)).addClass(field.name);
this.updateInvalid();
return this;
},
clearInvalid: function() {
this.$el.removeClass(Backform.errorClassName)
.find("." + Backform.helpClassName + ".error").remove();
return this;
},
updateInvalid: function() {
var self = this;
var errorModel = this.model.errorModel;
if (!(errorModel instanceof Backbone.Model)) return this;
this.clearInvalid();
this.$el.find(':input').not('button').each(function(ix, el) {
var attrArr = $(el).attr('name').split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
error = self.keyPathAccessor(errorModel.toJSON(), $(el).attr('name'));
if (_.isEmpty(error)) return;
if (_.isEmpty(error)) return;
self.$el.addClass(Backform.errorClassName);
self.$el.find("." + Backform.controlsClassName)
.append('<span class="' + Backform.helpClassName + ' error">' + (_.isArray(error) ? error.join(", ") : error) + '</span>');
});
return this;
},
keyPathAccessor: function(obj, path) {
var res = obj;
path = path.split('.');
for (var i = 0; i < path.length; i++) {
if (_.isNull(res)) return null;
if (_.isEmpty(path[i])) continue;
if (!_.isUndefined(res[path[i]])) res = res[path[i]];
}
return _.isObject(res) && !_.isArray(res) ? null : res;
},
keyPathSetter: function(obj, path, value) {
path = path.split('.');
while (path.length > 1) {
if (!obj[path[0]]) obj[path[0]] = {};
obj = obj[path.shift()];
}
return obj[path.shift()] = value;
}
});
// Built-in controls
var UneditableInputControl = Backform.UneditableInputControl = Control;
var HelpControl = Backform.HelpControl = Control.extend({
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>">&nbsp;</label>',
'<div class="<%=Backform.controlsClassName%>">',
' <span class="<%=Backform.helpMessageClassName%> help-block"><%=label%></span>',
'</div>'
].join("\n"))
});
var SpacerControl = Backform.SpacerControl = Control.extend({
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>">&nbsp;</label>',
'<div class="<%=Backform.controlsClassName%>"></div>'
].join("\n"))
});
var TextareaControl = Backform.TextareaControl = Control.extend({
defaults: {
label: "",
maxlength: 4000,
extraClasses: [],
helpMessage: null
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <textarea class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>" name="<%=name%>" maxlength="<%=maxlength%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%>><%-value%></textarea>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>'
].join("\n")),
events: {
"change textarea": "onChange",
"focus textarea": "clearInvalid"
},
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("textarea").val(), this.model);
}
});
var SelectControl = Backform.SelectControl = Control.extend({
defaults: {
label: "",
options: [], // List of options as [{label:<label>, value:<value>}, ...]
extraClasses: []
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <select class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>" name="<%=name%>" value="<%-value%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> >',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <option value="<%-formatter.fromRaw(option.value)%>" <%=option.value === rawValue ? "selected=\'selected\'" : ""%> <%=option.disabled ? "disabled=\'disabled\'" : ""%>><%-option.label%></option>',
' <% } %>',
' </select>',
'</div>'
].join("\n")),
events: {
"change select": "onChange",
"focus select": "clearInvalid"
},
formatter: JSONFormatter,
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("select").val(), this.model);
}
});
// Note: Value here is null or an array. Since jQuery val() returns either.
var MultiSelectControl = Backform.MultiSelectControl = SelectControl.extend({
defaults: {
label: "",
options: [], // List of options as [{label:<label>, value:<value>}, ...]
extraClasses: [],
height: '78px'
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <select multiple="multiple" class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>" name="<%=name%>" value="<%-JSON.stringify(value)%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> style="height:<%=height%>">',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <option value="<%-option.value%>" <%=value != null && _.indexOf(value, option.value) != -1 ? "selected=\'selected\'" : ""%> <%=option.disabled ? "disabled=\'disabled\'" : ""%>><%-option.label%></option>',
' <% } %>',
' </select>',
'</div>'
].join("\n")),
events: {
"change select": "onChange",
"dblclick select": "onDoubleClick",
"focus select": "clearInvalid"
},
formatter: {
fromRaw: function(rawData, model) {
return rawData;
},
toRaw: function(formattedData, model) {
return typeof formattedData == "object" ? formattedData : JSON.parse(formattedData);
}
},
onDoubleClick: function(e) {
this.model.trigger('doubleclick', e);
}
});
var InputControl = Backform.InputControl = Control.extend({
defaults: {
type: "text",
label: "",
maxlength: 255,
extraClasses: [],
helpMessage: null
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <input type="<%=type%>" class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>" name="<%=name%>" maxlength="<%=maxlength%>" value="<%-value%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> />',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>'
].join("\n")),
events: {
"change input": "onChange",
"focus input": "clearInvalid"
},
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("input").val(), this.model);
}
});
var BooleanControl = Backform.BooleanControl = InputControl.extend({
defaults: {
type: "checkbox",
label: "",
controlLabel: '&nbsp;',
extraClasses: []
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=controlLabel%></label>',
'<div class="<%=Backform.controlsClassName%>">',
' <div class="checkbox">',
' <label>',
' <input type="<%=type%>" class="<%=extraClasses.join(\' \')%>" name="<%=name%>" <%=value ? "checked=\'checked\'" : ""%> <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> /> <%=label%>',
' </label>',
' </div>',
'</div>'
].join("\n")),
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("input").is(":checked"), this.model);
}
});
var CheckboxControl = Backform.CheckboxControl = BooleanControl;
var RadioControl = Backform.RadioControl = InputControl.extend({
defaults: {
type: "radio",
label: "",
options: [],
extraClasses: [],
helpMessage: null
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%> <%=Backform.radioControlsClassName%>">',
' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <label class="<%=Backform.radioLabelClassName%>">',
' <input type="<%=type%>" class="<%=extraClasses.join(\' \')%>" name="<%=name%>" value="<%-formatter.fromRaw(option.value)%>" <%=rawValue == option.value ? "checked=\'checked\'" : ""%> <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> /> <%-option.label%>',
' </label>',
' <% } %>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>'
].join("\n")),
formatter: JSONFormatter,
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("input:checked").val(), this.model);
},
bootstrap2: function() {
Backform.radioControlsClassName = "controls";
Backform.radioLabelClassName = "radio inline";
}
});
_.extend(Backform, {
radioControlsClassName: "checkbox",
radioLabelClassName: "checkbox-inline"
});
// Requires the Bootstrap Datepicker to work.
var DatepickerControl = Backform.DatepickerControl = InputControl.extend({
defaults: {
type: "text",
label: "",
options: {},
extraClasses: [],
maxlength: 255,
helpMessage: null
},
events: {
"blur input": "onChange",
"change input": "onChange",
"changeDate input": "onChange",
"focus input": "clearInvalid"
},
render: function() {
InputControl.prototype.render.apply(this, arguments);
this.$el.find("input").datepicker(this.field.get("options"));
return this;
}
});
var ButtonControl = Backform.ButtonControl = Control.extend({
defaults: {
type: "submit",
label: "Submit",
status: undefined, // error or success
message: undefined,
extraClasses: []
},
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>">&nbsp;</label>',
'<div class="<%=Backform.controlsClassName%>">',
' <button type="<%=type%>" name="<%=name%>" class="btn <%=extraClasses.join(\' \')%>" <%=disabled ? "disabled" : ""%> ><%=label%></button>',
' <% var cls = ""; if (status == "error") cls = Backform.buttonStatusErrorClassName; if (status == "success") cls = Backform.buttonStatusSuccessClassname; %>',
' <span class="status <%=cls%>"><%=message%></span>',
'</div>'
].join("\n")),
initialize: function() {
Control.prototype.initialize.apply(this, arguments);
this.listenTo(this.field, "change:status", this.render);
this.listenTo(this.field, "change:message", this.render);
},
bootstrap2: function() {
Backform.buttonStatusErrorClassName = "text-error";
Backform.buttonStatusSuccessClassname = "text-success";
}
});
_.extend(Backform, {
buttonStatusErrorClassName: "text-danger",
buttonStatusSuccessClassname: "text-success"
});
return Backform;
}));