/*
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', 'underscore.string', 'jquery', 'backbone', 'backform',
'backgrid', 'codemirror', 'pgadmin.backgrid', 'codemirror.sql',
'select2'
],
function(_, S, $, Backbone, Backform, Backgrid, CodeMirror) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backform.
return factory(root, _, S, $, Backbone, Backform, Backgrid, CodeMirror);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore') || root._,
$ = root.jQuery || root.$ || root.Zepto || root.ender,
Backbone = require('backbone') || root.Backbone,
Backform = require('backform') || root.Backform,
Backgrid = require('backgrid') || root.Backgrid;
CodeMirror = require('codemirror') || root.CodeMirror;
pgAdminBackgrid = require('pgadmin.backgrid');
S = require('underscore.string');
factory(root, _, S, $, Backbone, Backform, Backgrid, CodeMirror);
// Finally, as a browser global.
} else {
factory(root, root._, root.s, (root.jQuery || root.Zepto || root.ender || root.$), root.Backbone, root.Backform, root.Backgrid, root.CodeMirror);
}
}(this, function(root, _, S, $, Backbone, Backform, Backgrid, CodeMirror) {
var pgAdmin = (window.pgAdmin = window.pgAdmin || {});
pgAdmin.editableCell = function() {
if (this.attributes && !_.isUndefined(this.attributes.disabled) &&
!_.isNull(this.attributes.disabled)) {
if(_.isFunction(this.attributes.disabled)) {
return !(this.attributes.disabled.apply(this, arguments));
}
if (_.isBoolean(this.attributes.disabled)) {
return !this.attributes.disabled;
}
}
};
// HTML markup global class names. More can be added by individual controls
// using _.extend. Look at RadioControl as an example.
_.extend(Backform, {
controlLabelClassName: "control-label col-sm-4",
controlsClassName: "pgadmin-controls col-sm-8",
groupClassName: "pgadmin-control-group form-group col-xs-12",
setGroupClassName: "set-group col-xs-12",
tabClassName: "backform-tab col-xs-12",
setGroupContentClassName: "fieldset-content col-xs-12"
});
var controlMapper = Backform.controlMapper = {
'int': ['uneditable-input', 'integer', 'integer'],
'text': ['uneditable-input', 'input', 'string'],
'numeric': ['uneditable-input', 'numeric', 'numeric'],
'date': 'datepicker',
'boolean': 'boolean',
'options': ['readonly-option', 'select', Backgrid.Extension.PGSelectCell],
'multiline': ['textarea', 'textarea', 'string'],
'collection': ['sub-node-collection', 'sub-node-collection', 'string'],
'uniqueColCollection': ['unique-col-collection', 'unique-col-collection', 'string'],
'switch' : 'switch',
'select2': 'select2'
};
var getMappedControl = Backform.getMappedControl = function(type, mode) {
if (type in Backform.controlMapper) {
var m = Backform.controlMapper[type];
if (!_.isArray(m)) {
return m;
}
var idx = 1, len = _.size(m);
switch (mode) {
case 'properties':
idx = 0;
break;
case 'edit':
case 'create':
case 'control':
idx = 1;
break;
case 'cell':
idx = 2;
break;
default:
idx = 0;
break;
}
return m[idx > len ? 0 : idx];
}
return type;
}
var BackformControlInit = Backform.Control.prototype.initialize,
BackformControlRemove = Backform.Control.prototype.remove;
// Override the Backform.Control to allow to track changes in dependencies,
// and rerender the View element
_.extend(Backform.Control.prototype, {
initialize: function() {
BackformControlInit.apply(this, arguments);
// Listen to the dependent fields in the model for any change
var deps = this.field.get('deps');
var self = this;
if (deps && _.isArray(deps)) {
_.each(deps, function(d) {
attrArr = d.split('.');
name = attrArr.shift();
self.listenTo(self.model, "change:" + name, self.render);
});
}
},
remove: function() {
// Listen to the dependent fields in the model for any change
var self = this,
deps = self.field.get('deps');
self.stopListening(self.model, "change:" + name, self.render);
self.stopListening(self.model.errorModel, "change:" + name, self.updateInvalid);
if (deps && _.isArray(deps)) {
_.each(deps, function(d) {
attrArr = d.split('.');
name = attrArr.shift();
self.stopListening(self.model, "change:" + name, self.render);
});
}
if (BackformControlRemove) {
BackformControlRemove.apply(self, arguments);
} else {
Backbone.View.prototype.remove.apply(self, arguments);
}
},
template: _.template([
'',
'
',
' >',
' <%=value%>',
' ',
'
'
].join("\n")),
clearInvalid: function() {
this.$el.removeClass(Backform.errorClassName);
this.$el.find(".pgadmin-control-error-message").remove();
return this;
},
updateInvalid: function() {
var self = this,
errorModel = this.model.errorModel;
if (!(errorModel instanceof Backbone.Model)) return this;
this.clearInvalid();
/*
* Find input which have name attribute.
*/
this.$el.find(':input[name]').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;
self.$el.addClass(Backform.errorClassName).append(
$("").addClass('pgadmin-control-error-message col-xs-12 help-block').text(error)
);
});
},
/*
* Overriding the render function of the control to allow us to eval the
* values properly.
*/
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
}),
evalF = function(f, d, m) {
return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f);
};
// Evaluate the disabled, visible, and required option
_.extend(data, {
disabled: evalF(data.disabled, data, this.model),
visible: evalF(data.visible, data, this.model),
required: evalF(data.required, data, this.model)
});
// Clean up first
this.$el.removeClass(Backform.hiddenClassname);
if (!data.visible)
this.$el.addClass(Backform.hiddenClassname);
this.$el.html(this.template(data)).addClass(field.name);
this.updateInvalid();
return this;
}
});
/*
* Override the input control events in order to reslove the issue related to
* not updating the value sometimes in the input control.
*/
Backform.InputControl.prototype.events = {
"change input": "onChange",
"blur input": "onChange",
"focus input": "clearInvalid"
};
/*
* Override the textarea control events in order to resolve the issue related
* to not updating the value in model on certain browsers in few situations
* like copy/paste, deletion using backspace.
*
* Reference:
* http://stackoverflow.com/questions/11338592/how-can-i-bind-to-the-change-event-of-a-textarea-in-jquery
*/
Backform.TextareaControl.prototype.events = {
"change textarea": "onChange",
"keyup textarea": "onChange",
"paste textarea": "onChange",
"selectionchange textarea": "onChange",
"focus textarea": "clearInvalid"
};
/*
* Overriding the render function of the select control to allow us to use
* options as function, which should return array in the format of
* (label, value) pair.
*/
Backform.SelectControl.prototype.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
}),
evalF = function(f, d, m) {
return (_.isFunction(f) ? !!f.apply(d, [m]) : !!f);
};
// Evaluate the disabled, visible, and required option
_.extend(data, {
disabled: evalF(data.disabled, data, this.model),
visible: evalF(data.visible, data, this.model),
required: evalF(data.required, data, this.model)
});
// Evaluation the options
if (_.isFunction(data.options)) {
try {
data.options = data.options.apply(this)
} catch(e) {
// Do nothing
data.options = []
this.model.trigger('pgadmin-view:transform:error', m, self.field, e);
}
}
// Clean up first
this.$el.removeClass(Backform.hiddenClassname);
if (!data.visible)
this.$el.addClass(Backform.hiddenClassname);
this.$el.html(this.template(data)).addClass(field.name);
this.updateInvalid();
return this;
};
var ReadonlyOptionControl = Backform.ReadonlyOptionControl = Backform.SelectControl.extend({
template: _.template([
'',
'
',
'<% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>',
' <% if (option.value === rawValue) { %>',
' <%-option.label%>',
' <% } %>',
'<% } %>',
'
'
].join("\n")),
events: {},
getValueFromDOM: function() {
return this.formatter.toRaw(this.$el.find("span").text(), this.model);
}
});
/*
* Override the function 'updateInvalid' of the radio control to resolve an
* issue, which will not render the error block multiple times for each
* options.
*/
_.extend(
Backform.RadioControl.prototype, {
updateInvalid: function() {
var self = this,
errorModel = this.model.errorModel;
if (!(errorModel instanceof Backbone.Model)) return this;
this.clearInvalid();
/*
* Find input which have name attribute.
*/
this.$el.find(':input[name]').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;
self.$el.addClass(Backform.errorClassName).find(
'[type="radio"]'
).append(
$("").addClass(
'pgadmin-control-error-message col-xs-12 help-block'
).text(error));
});
}
});
// Requires the Bootstrap Switch to work.
var SwitchControl = Backform.SwitchControl = Backform.InputControl.extend({
defaults: {
label: "",
options: {
onText: 'True',
offText: 'False',
onColor: 'success',
offColor: 'default',
size: 'small'
},
extraClasses: []
},
template: _.template([
'',
'
',
'
',
' ',
'
',
'
'
].join("\n")),
getValueFromDOM: function() {
return this.formatter.toRaw(
this.$input.prop('checked'),
this.model
);
},
events: {'switchChange.bootstrapSwitch': 'onChange'},
render: function() {
var field = _.defaults(this.field.toJSON(), this.defaults, {options: $.fn.bootstrapSwitch.defaults}),
attributes = this.model.toJSON(),
attrArr = field.name.split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
rawValue = this.keyPathAccessor(attributes[name], path);
Backform.InputControl.prototype.render.apply(this, arguments);
this.$input = this.$el.find("input[type=checkbox]").first();
//Check & set additional properties
this.$input.bootstrapSwitch(
_.extend(field.options, {'state': rawValue})
);
return this;
}
});
// Backform Dialog view (in bootstrap tabbular form)
// A collection of field models.
var Dialog = Backform.Dialog = Backform.Form.extend({
/* Array of objects having attributes [label, fields] */
schema: undefined,
tagName: "form",
className: function() {
return 'col-sm-12 col-md-12 col-lg-12 col-xs-12';
},
tabPanelClassName: function() {
return Backform.tabClassName;
},
tabIndex: 0,
initialize: function(opts) {
var s = opts.schema;
if (s && _.isArray(s)) {
this.schema = _.each(s, function(o) {
if (o.fields && !(o.fields instanceof Backbone.Collection))
o.fields = new Backform.Fields(o.fields);
o.cId = o.cId || _.uniqueId('pgC_');
o.hId = o.hId || _.uniqueId('pgH_');
o.disabled = o.disabled || false;
});
if (opts.tabPanelClassName && _.isFunction(opts.tabPanelClassName)) {
this.tabPanelClassName = opts.tabPanelClassName;
}
}
this.model.errorModel = opts.errorModel || this.model.errorModel || new Backbone.Model();
this.controls = [];
},
template: {
'header': _.template([
'