rails-backbone-forms 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/rails/backbone/forms/version.rb +1 -1
- data/vendor/assets/javascripts/backbone-forms/adapters/backbone.bootstrap-modal.js +10 -50
- data/vendor/assets/javascripts/backbone-forms/adapters/backbone.bootstrap-modal.min.js +1 -1
- data/vendor/assets/javascripts/backbone-forms/backbone-forms.js +1788 -1768
- data/vendor/assets/javascripts/backbone-forms/editors/list.js +86 -46
- data/vendor/assets/javascripts/backbone-forms/editors/list.min.js +1 -1
- data/vendor/assets/javascripts/backbone-forms/templates/bootstrap.js +53 -78
- data/vendor/assets/javascripts/backbone-forms/templates/old.js +88 -0
- data/vendor/assets/stylesheets/backbone-forms/templates/{default.css → old.css} +0 -0
- metadata +3 -3
- data/vendor/assets/javascripts/backbone-forms/templates/default.js +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3fca4540b9490eebd58cf719e113313c678f9d0
|
4
|
+
data.tar.gz: e92315755428a19d134e791441583d0f87c420b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c55094e51fd63d02d758de064e0ccb13e737f9b37752e8518e1155a4a4e9c39af45cb1afda324f1972ee3b12ab6700d3ad2f32e449d6c94d749909ce6bd469c2
|
7
|
+
data.tar.gz: b7a499ac74e83c4e3b46b6ebfe6b94ca4742e0d0233143fd143b87b28ab9b4b4f555bdc3fb0dfea909a92dcd6a3d124c16b9fa71e07c620190bf489e1ef82586
|
@@ -54,32 +54,17 @@
|
|
54
54
|
event.preventDefault();
|
55
55
|
|
56
56
|
this.trigger('cancel');
|
57
|
-
|
58
|
-
if (this.options.content && this.options.content.trigger) {
|
59
|
-
this.options.content.trigger('cancel', this);
|
60
|
-
}
|
61
57
|
},
|
62
58
|
'click .cancel': function(event) {
|
63
59
|
event.preventDefault();
|
64
60
|
|
65
61
|
this.trigger('cancel');
|
66
|
-
|
67
|
-
if (this.options.content && this.options.content.trigger) {
|
68
|
-
this.options.content.trigger('cancel', this);
|
69
|
-
}
|
70
62
|
},
|
71
63
|
'click .ok': function(event) {
|
72
64
|
event.preventDefault();
|
73
65
|
|
74
66
|
this.trigger('ok');
|
75
|
-
|
76
|
-
if (this.options.content && this.options.content.trigger) {
|
77
|
-
this.options.content.trigger('ok', this);
|
78
|
-
}
|
79
|
-
|
80
|
-
if (this.options.okCloses) {
|
81
|
-
this.close();
|
82
|
-
}
|
67
|
+
this.close();
|
83
68
|
}
|
84
69
|
},
|
85
70
|
|
@@ -102,8 +87,6 @@
|
|
102
87
|
this.options = _.extend({
|
103
88
|
title: null,
|
104
89
|
okText: 'OK',
|
105
|
-
focusOk: true,
|
106
|
-
okCloses: true,
|
107
90
|
cancelText: 'Cancel',
|
108
91
|
allowCancel: true,
|
109
92
|
escape: true,
|
@@ -129,8 +112,7 @@
|
|
129
112
|
|
130
113
|
//Insert the main content if it's a view
|
131
114
|
if (content.$el) {
|
132
|
-
content.render();
|
133
|
-
$el.find('.modal-body').html(content.$el);
|
115
|
+
$el.find('.modal-body').html(content.render().$el);
|
134
116
|
}
|
135
117
|
|
136
118
|
if (options.animate) $el.addClass('fade');
|
@@ -152,20 +134,14 @@
|
|
152
134
|
$el = this.$el;
|
153
135
|
|
154
136
|
//Create it
|
155
|
-
$el.modal(
|
137
|
+
$el.modal({
|
156
138
|
keyboard: this.options.allowCancel,
|
157
139
|
backdrop: this.options.allowCancel ? true : 'static'
|
158
|
-
}
|
140
|
+
});
|
159
141
|
|
160
142
|
//Focus OK button
|
161
143
|
$el.one('shown', function() {
|
162
|
-
|
163
|
-
$el.find('.btn.ok').focus();
|
164
|
-
}
|
165
|
-
|
166
|
-
if (self.options.content && self.options.content.trigger) {
|
167
|
-
self.options.content.trigger('shown', self);
|
168
|
-
}
|
144
|
+
$el.find('.btn.ok').focus();
|
169
145
|
|
170
146
|
self.trigger('shown');
|
171
147
|
});
|
@@ -173,27 +149,19 @@
|
|
173
149
|
//Adjust the modal and backdrop z-index; for dealing with multiple modals
|
174
150
|
var numModals = Modal.count,
|
175
151
|
$backdrop = $('.modal-backdrop:eq('+numModals+')'),
|
176
|
-
backdropIndex =
|
177
|
-
elIndex =
|
152
|
+
backdropIndex = $backdrop.css('z-index'),
|
153
|
+
elIndex = $backdrop.css('z-index');
|
178
154
|
|
179
155
|
$backdrop.css('z-index', backdropIndex + numModals);
|
180
156
|
this.$el.css('z-index', elIndex + numModals);
|
181
157
|
|
182
158
|
if (this.options.allowCancel) {
|
183
159
|
$backdrop.one('click', function() {
|
184
|
-
if (self.options.content && self.options.content.trigger) {
|
185
|
-
self.options.content.trigger('cancel', self);
|
186
|
-
}
|
187
|
-
|
188
160
|
self.trigger('cancel');
|
189
161
|
});
|
190
162
|
|
191
163
|
$(document).one('keyup.dismiss.modal', function (e) {
|
192
164
|
e.which == 27 && self.trigger('cancel');
|
193
|
-
|
194
|
-
if (self.options.content && self.options.content.trigger) {
|
195
|
-
e.which == 27 && self.options.content.trigger('shown', self);
|
196
|
-
}
|
197
165
|
});
|
198
166
|
}
|
199
167
|
|
@@ -224,22 +192,14 @@
|
|
224
192
|
return;
|
225
193
|
}
|
226
194
|
|
227
|
-
$el.
|
228
|
-
// Ignore events propagated from interior objects, like bootstrap tooltips
|
229
|
-
if(e.target !== e.currentTarget){
|
230
|
-
return $el.one('hidden', onHidden);
|
231
|
-
}
|
232
|
-
self.remove();
|
195
|
+
$el.modal('hide');
|
233
196
|
|
234
|
-
|
235
|
-
|
236
|
-
}
|
197
|
+
$el.one('hidden', function() {
|
198
|
+
self.remove();
|
237
199
|
|
238
200
|
self.trigger('hidden');
|
239
201
|
});
|
240
202
|
|
241
|
-
$el.modal('hide');
|
242
|
-
|
243
203
|
Modal.count--;
|
244
204
|
},
|
245
205
|
|
@@ -1 +1 @@
|
|
1
|
-
(function(e,t,n){var r=t.templateSettings;t.templateSettings={interpolate:/\{\{(.+?)\}\}/g,evaluate:/<%([\s\S]+?)%>/g};var i=t.template(' <% if (title) { %> <div class="modal-header"> <% if (allowCancel) { %> <a class="close">×</a> <% } %> <h3>{{title}}</h3> </div> <% } %> <div class="modal-body">{{content}}</div> <div class="modal-footer"> <% if (allowCancel) { %> <% if (cancelText) { %> <a href="#" class="btn cancel">{{cancelText}}</a> <% } %> <% } %> <a href="#" class="btn ok btn-primary">{{okText}}</a> </div> ');t.templateSettings=r;var s=n.View.extend({className:"modal",events:{"click .close":function(e){e.preventDefault(),this.trigger("cancel")
|
1
|
+
(function(e,t,n){var r=t.templateSettings;t.templateSettings={interpolate:/\{\{(.+?)\}\}/g,evaluate:/<%([\s\S]+?)%>/g};var i=t.template(' <% if (title) { %> <div class="modal-header"> <% if (allowCancel) { %> <a class="close">×</a> <% } %> <h3>{{title}}</h3> </div> <% } %> <div class="modal-body">{{content}}</div> <div class="modal-footer"> <% if (allowCancel) { %> <% if (cancelText) { %> <a href="#" class="btn cancel">{{cancelText}}</a> <% } %> <% } %> <a href="#" class="btn ok btn-primary">{{okText}}</a> </div> ');t.templateSettings=r;var s=n.View.extend({className:"modal",events:{"click .close":function(e){e.preventDefault(),this.trigger("cancel")},"click .cancel":function(e){e.preventDefault(),this.trigger("cancel")},"click .ok":function(e){e.preventDefault(),this.trigger("ok"),this.close()}},initialize:function(e){this.options=t.extend({title:null,okText:"OK",cancelText:"Cancel",allowCancel:!0,escape:!0,animate:!1,template:i},e)},render:function(){var e=this.$el,t=this.options,n=t.content;e.html(t.template(t));var r=this.$content=e.find(".modal-body");return n.$el&&e.find(".modal-body").html(n.render().$el),t.animate&&e.addClass("fade"),this.isRendered=!0,this},open:function(t){this.isRendered||this.render();var n=this,r=this.$el;r.modal({keyboard:this.options.allowCancel,backdrop:this.options.allowCancel?!0:"static"}),r.one("shown",function(){r.find(".btn.ok").focus(),n.trigger("shown")});var i=s.count,o=e(".modal-backdrop:eq("+i+")"),u=o.css("z-index"),a=o.css("z-index");return o.css("z-index",u+i),this.$el.css("z-index",a+i),this.options.allowCancel&&(o.one("click",function(){n.trigger("cancel")}),e(document).one("keyup.dismiss.modal",function(e){e.which==27&&n.trigger("cancel")})),this.on("cancel",function(){n.close()}),s.count++,t&&n.on("ok",t),this},close:function(){var e=this,t=this.$el;if(this._preventClose){this._preventClose=!1;return}t.modal("hide"),t.one("hidden",function(){e.remove(),e.trigger("hidden")}),s.count--},preventClose:function(){this._preventClose=!0}},{count:0});typeof require=="function"&&typeof module!="undefined"&&exports&&(module.exports=s);if(typeof define=="function"&&define.amd)return define(function(){n.BootstrapModal=s});n.BootstrapModal=s})(jQuery,_,Backbone)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/**
|
2
|
-
* Backbone Forms v0.
|
2
|
+
* Backbone Forms v0.12.0
|
3
3
|
*
|
4
4
|
* Copyright (c) 2013 Charles Davison, Pow Media Ltd
|
5
5
|
*
|
@@ -30,594 +30,446 @@
|
|
30
30
|
//FORM
|
31
31
|
//==================================================================================================
|
32
32
|
|
33
|
-
var Form = (
|
34
|
-
|
35
|
-
return Backbone.View.extend({
|
36
|
-
|
37
|
-
hasFocus: false,
|
38
|
-
|
39
|
-
/**
|
40
|
-
* Creates a new form
|
41
|
-
*
|
42
|
-
* @param {Object} options
|
43
|
-
* @param {Model} [options.model] Model the form relates to. Required if options.data is not set
|
44
|
-
* @param {Object} [options.data] Date to populate the form. Required if options.model is not set
|
45
|
-
* @param {String[]} [options.fields] Fields to include in the form, in order
|
46
|
-
* @param {String[]|Object[]} [options.fieldsets] How to divide the fields up by section. E.g. [{ legend: 'Title', fields: ['field1', 'field2'] }]
|
47
|
-
* @param {String} [options.idPrefix] Prefix for editor IDs. By default, the model's CID is used.
|
48
|
-
* @param {String} [options.template] Form template key/name
|
49
|
-
* @param {String} [options.fieldsetTemplate] Fieldset template key/name
|
50
|
-
* @param {String} [options.fieldTemplate] Field template key/name
|
51
|
-
*
|
52
|
-
* @return {Form}
|
53
|
-
*/
|
54
|
-
initialize: function(options) {
|
55
|
-
//Check templates have been loaded
|
56
|
-
if (!Form.templates.form) throw new Error('Templates not loaded');
|
57
|
-
|
58
|
-
//Get the schema
|
59
|
-
this.schema = (function() {
|
60
|
-
if (options.schema) return options.schema;
|
61
|
-
|
62
|
-
var model = options.model;
|
63
|
-
if (!model) throw new Error('Could not find schema');
|
64
|
-
|
65
|
-
if (_.isFunction(model.schema)) return model.schema();
|
66
|
-
|
67
|
-
return model.schema;
|
68
|
-
})();
|
69
|
-
|
70
|
-
//Option defaults
|
71
|
-
options = _.extend({
|
72
|
-
template: 'form',
|
73
|
-
fieldsetTemplate: 'fieldset',
|
74
|
-
fieldTemplate: 'field'
|
75
|
-
}, options);
|
76
|
-
|
77
|
-
//Determine fieldsets
|
78
|
-
if (!options.fieldsets) {
|
79
|
-
var fields = options.fields || _.keys(this.schema);
|
80
|
-
|
81
|
-
options.fieldsets = [{ fields: fields }];
|
82
|
-
}
|
33
|
+
var Form = Backbone.View.extend({
|
83
34
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
35
|
+
/**
|
36
|
+
* Constructor
|
37
|
+
*
|
38
|
+
* @param {Object} [options.schema]
|
39
|
+
* @param {Backbone.Model} [options.model]
|
40
|
+
* @param {Object} [options.data]
|
41
|
+
* @param {String[]|Object[]} [options.fieldsets]
|
42
|
+
* @param {String[]} [options.fields]
|
43
|
+
* @param {String} [options.idPrefix]
|
44
|
+
* @param {Form.Field} [options.Field]
|
45
|
+
* @param {Form.Fieldset} [options.Fieldset]
|
46
|
+
* @param {Function} [options.template]
|
47
|
+
*/
|
48
|
+
initialize: function(options) {
|
49
|
+
var self = this;
|
90
50
|
|
91
|
-
|
92
|
-
* Renders the form and all fields
|
93
|
-
*/
|
94
|
-
render: function() {
|
95
|
-
var self = this,
|
96
|
-
options = this.options,
|
97
|
-
template = Form.templates[options.template];
|
51
|
+
options = options || {};
|
98
52
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
53
|
+
//Find the schema to use
|
54
|
+
var schema = this.schema = (function() {
|
55
|
+
//Prefer schema from options
|
56
|
+
if (options.schema) return _.result(options, 'schema');
|
103
57
|
|
104
|
-
//
|
105
|
-
var
|
58
|
+
//Then schema on model
|
59
|
+
var model = options.model;
|
60
|
+
if (model && model.schema) {
|
61
|
+
return (_.isFunction(model.schema)) ? model.schema() : model.schema;
|
62
|
+
}
|
106
63
|
|
107
|
-
|
108
|
-
|
109
|
-
|
64
|
+
//Then built-in schema
|
65
|
+
if (self.schema) {
|
66
|
+
return (_.isFunction(self.schema)) ? self.schema() : self.schema;
|
67
|
+
}
|
110
68
|
|
111
|
-
|
69
|
+
//Fallback to empty schema
|
70
|
+
return {};
|
71
|
+
})();
|
112
72
|
|
113
|
-
|
114
|
-
|
73
|
+
//Store important data
|
74
|
+
_.extend(this, _.pick(options, 'model', 'data', 'idPrefix'));
|
115
75
|
|
116
|
-
|
76
|
+
//Override defaults
|
77
|
+
var constructor = this.constructor;
|
78
|
+
this.template = options.template || constructor.template;
|
79
|
+
this.Fieldset = options.Fieldset || constructor.Fieldset;
|
80
|
+
this.Field = options.Field || constructor.Field;
|
81
|
+
this.NestedField = options.NestedField || constructor.NestedField;
|
117
82
|
|
118
|
-
|
119
|
-
|
83
|
+
//Check which fields will be included (defaults to all)
|
84
|
+
var selectedFields = this.selectedFields = options.fields || _.keys(schema);
|
120
85
|
|
121
|
-
|
122
|
-
|
123
|
-
*
|
124
|
-
* Valid fieldset definitions:
|
125
|
-
* ['field1', 'field2']
|
126
|
-
* { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
|
127
|
-
*
|
128
|
-
* @param {Object|Array} fieldset A fieldset definition
|
129
|
-
*
|
130
|
-
* @return {jQuery} The fieldset DOM element
|
131
|
-
*/
|
132
|
-
renderFieldset: function(fieldset) {
|
133
|
-
var self = this,
|
134
|
-
template = Form.templates[this.options.fieldsetTemplate],
|
135
|
-
schema = this.schema,
|
136
|
-
getNested = Form.helpers.getNested;
|
137
|
-
|
138
|
-
//Normalise to object
|
139
|
-
if (_.isArray(fieldset)) {
|
140
|
-
fieldset = { fields: fieldset };
|
141
|
-
}
|
86
|
+
//Create fields
|
87
|
+
var fields = this.fields = {};
|
142
88
|
|
143
|
-
|
144
|
-
var
|
145
|
-
|
146
|
-
|
147
|
-
})));
|
89
|
+
_.each(selectedFields, function(key) {
|
90
|
+
var fieldSchema = schema[key];
|
91
|
+
fields[key] = this.createField(key, fieldSchema);
|
92
|
+
}, this);
|
148
93
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
94
|
+
//Create fieldsets
|
95
|
+
var fieldsetSchema = options.fieldsets || [selectedFields],
|
96
|
+
fieldsets = this.fieldsets = [];
|
97
|
+
|
98
|
+
_.each(fieldsetSchema, function(itemSchema) {
|
99
|
+
this.fieldsets.push(this.createFieldset(itemSchema));
|
100
|
+
}, this);
|
101
|
+
},
|
102
|
+
|
103
|
+
/**
|
104
|
+
* Creates a Fieldset instance
|
105
|
+
*
|
106
|
+
* @param {String[]|Object[]} schema Fieldset schema
|
107
|
+
*
|
108
|
+
* @return {Form.Fieldset}
|
109
|
+
*/
|
110
|
+
createFieldset: function(schema) {
|
111
|
+
var options = {
|
112
|
+
schema: schema,
|
113
|
+
fields: this.fields
|
114
|
+
};
|
115
|
+
|
116
|
+
return new this.Fieldset(options);
|
117
|
+
},
|
157
118
|
|
158
|
-
|
119
|
+
/**
|
120
|
+
* Creates a Field instance
|
121
|
+
*
|
122
|
+
* @param {String} key
|
123
|
+
* @param {Object} schema Field schema
|
124
|
+
*
|
125
|
+
* @return {Form.Field}
|
126
|
+
*/
|
127
|
+
createField: function(key, schema) {
|
128
|
+
var options = {
|
129
|
+
form: this,
|
130
|
+
key: key,
|
131
|
+
schema: schema,
|
132
|
+
idPrefix: this.idPrefix
|
133
|
+
};
|
159
134
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
135
|
+
if (this.model) {
|
136
|
+
options.model = this.model;
|
137
|
+
} else if (this.data) {
|
138
|
+
options.value = this.data[key];
|
139
|
+
} else {
|
140
|
+
options.value = null;
|
141
|
+
}
|
166
142
|
|
167
|
-
|
168
|
-
var path = key.replace(/\./g, '.subSchema.');
|
169
|
-
return getNested(schema, path);
|
170
|
-
})();
|
143
|
+
var field = new this.Field(options);
|
171
144
|
|
172
|
-
|
145
|
+
this.listenTo(field.editor, 'all', this.handleEditorEvent);
|
173
146
|
|
174
|
-
|
175
|
-
|
147
|
+
return field;
|
148
|
+
},
|
176
149
|
|
177
|
-
|
178
|
-
|
150
|
+
/**
|
151
|
+
* Callback for when an editor event is fired.
|
152
|
+
* Re-triggers events on the form as key:event and triggers additional form-level events
|
153
|
+
*
|
154
|
+
* @param {String} event
|
155
|
+
* @param {Editor} editor
|
156
|
+
*/
|
157
|
+
handleEditorEvent: function(event, editor) {
|
158
|
+
//Re-trigger editor events on the form
|
159
|
+
var formEvent = editor.key+':'+event;
|
179
160
|
|
180
|
-
|
181
|
-
// args = ["change", editor]
|
182
|
-
var args = _.toArray(arguments);
|
183
|
-
args[0] = key + ':' + event;
|
184
|
-
args.splice(1, 0, this);
|
185
|
-
// args = ["key:change", this=form, editor]
|
161
|
+
this.trigger.call(this, formEvent, this, editor);
|
186
162
|
|
187
|
-
|
188
|
-
|
163
|
+
//Trigger additional events
|
164
|
+
switch (event) {
|
165
|
+
case 'change':
|
166
|
+
this.trigger('change', this);
|
167
|
+
break;
|
189
168
|
|
190
|
-
|
191
|
-
|
192
|
-
|
169
|
+
case 'focus':
|
170
|
+
if (!this.hasFocus) this.trigger('focus', this);
|
171
|
+
break;
|
193
172
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
}, self);
|
198
|
-
field.editor.on('blur', function() {
|
199
|
-
if (!this.hasFocus) return;
|
173
|
+
case 'blur':
|
174
|
+
if (this.hasFocus) {
|
175
|
+
//TODO: Is the timeout etc needed?
|
200
176
|
var self = this;
|
201
177
|
setTimeout(function() {
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
}, self);
|
178
|
+
var focusedField = _.find(self.fields, function(field) {
|
179
|
+
return field.editor.hasFocus;
|
180
|
+
});
|
206
181
|
|
207
|
-
|
208
|
-
|
182
|
+
if (!focusedField) self.trigger('blur', self);
|
183
|
+
}, 0);
|
209
184
|
}
|
210
|
-
|
185
|
+
break;
|
186
|
+
}
|
187
|
+
},
|
211
188
|
|
212
|
-
|
189
|
+
render: function() {
|
190
|
+
var self = this,
|
191
|
+
fields = this.fields;
|
213
192
|
|
214
|
-
|
215
|
-
|
193
|
+
//Render form
|
194
|
+
var $form = $($.trim(this.template(_.result(this, 'templateData'))));
|
216
195
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
* @param {Object} schema Field schema
|
222
|
-
*
|
223
|
-
* @return {Field} The field view
|
224
|
-
*/
|
225
|
-
createField: function(key, schema) {
|
226
|
-
schema.template = schema.template || this.options.fieldTemplate;
|
227
|
-
|
228
|
-
var options = {
|
229
|
-
form: this,
|
230
|
-
key: key,
|
231
|
-
schema: schema,
|
232
|
-
idPrefix: this.options.idPrefix,
|
233
|
-
template: this.options.fieldTemplate
|
234
|
-
};
|
196
|
+
//Render standalone editors
|
197
|
+
$form.find('[data-editors]').add($form).each(function(i, el) {
|
198
|
+
var $container = $(el),
|
199
|
+
selection = $container.attr('data-editors');
|
235
200
|
|
236
|
-
if (
|
237
|
-
options.model = this.model;
|
238
|
-
} else if (this.data) {
|
239
|
-
options.value = this.data[key];
|
240
|
-
} else {
|
241
|
-
options.value = null;
|
242
|
-
}
|
201
|
+
if (_.isUndefined(selection)) return;
|
243
202
|
|
244
|
-
|
245
|
-
|
203
|
+
//Work out which fields to include
|
204
|
+
var keys = (selection == '*')
|
205
|
+
? self.selectedFields || _.keys(fields)
|
206
|
+
: selection.split(',');
|
246
207
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
validate: function() {
|
253
|
-
var self = this,
|
254
|
-
fields = this.fields,
|
255
|
-
model = this.model,
|
256
|
-
errors = {};
|
257
|
-
|
258
|
-
//Collect errors from schema validation
|
259
|
-
_.each(fields, function(field) {
|
260
|
-
var error = field.validate();
|
261
|
-
if (error) {
|
262
|
-
errors[field.key] = error;
|
263
|
-
}
|
208
|
+
//Add them
|
209
|
+
_.each(keys, function(key) {
|
210
|
+
var field = fields[key];
|
211
|
+
|
212
|
+
$container.append(field.editor.render().el);
|
264
213
|
});
|
214
|
+
});
|
265
215
|
|
266
|
-
|
267
|
-
|
268
|
-
|
216
|
+
//Render standalone fields
|
217
|
+
$form.find('[data-fields]').add($form).each(function(i, el) {
|
218
|
+
var $container = $(el),
|
219
|
+
selection = $container.attr('data-fields');
|
269
220
|
|
270
|
-
|
271
|
-
var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
|
221
|
+
if (_.isUndefined(selection)) return;
|
272
222
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
}
|
223
|
+
//Work out which fields to include
|
224
|
+
var keys = (selection == '*')
|
225
|
+
? self.selectedFields || _.keys(fields)
|
226
|
+
: selection.split(',');
|
278
227
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
//Set error on field if there isn't one already
|
283
|
-
if (self.fields[key] && !errors[key]) {
|
284
|
-
self.fields[key].setError(val);
|
285
|
-
errors[key] = val;
|
286
|
-
}
|
287
|
-
|
288
|
-
else {
|
289
|
-
//Otherwise add to '_others' key
|
290
|
-
errors._others = errors._others || [];
|
291
|
-
var tmpErr = {};
|
292
|
-
tmpErr[key] = val;
|
293
|
-
errors._others.push(tmpErr);
|
294
|
-
}
|
295
|
-
});
|
296
|
-
}
|
297
|
-
}
|
298
|
-
}
|
228
|
+
//Add them
|
229
|
+
_.each(keys, function(key) {
|
230
|
+
var field = fields[key];
|
299
231
|
|
300
|
-
|
301
|
-
|
232
|
+
$container.append(field.render().el);
|
233
|
+
});
|
234
|
+
});
|
302
235
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
*
|
308
|
-
* @return {Object} Validation errors
|
309
|
-
*/
|
310
|
-
commit: function(options) {
|
311
|
-
//Validate
|
312
|
-
var errors = this.validate();
|
313
|
-
if (errors) return errors;
|
314
|
-
|
315
|
-
//Commit
|
316
|
-
var modelError;
|
317
|
-
|
318
|
-
var setOptions = _.extend({
|
319
|
-
error: function(model, e) {
|
320
|
-
modelError = e;
|
321
|
-
}
|
322
|
-
}, options);
|
236
|
+
//Render fieldsets
|
237
|
+
$form.find('[data-fieldsets]').add($form).each(function(i, el) {
|
238
|
+
var $container = $(el),
|
239
|
+
selection = $container.attr('data-fieldsets');
|
323
240
|
|
324
|
-
|
325
|
-
|
326
|
-
if (modelError) return modelError;
|
327
|
-
},
|
241
|
+
if (_.isUndefined(selection)) return;
|
328
242
|
|
329
|
-
|
330
|
-
|
331
|
-
* Use this method when passing data instead of objects
|
332
|
-
*
|
333
|
-
* @param {String} [key] Specific field value to get
|
334
|
-
*/
|
335
|
-
getValue: function(key) {
|
336
|
-
//Return only given key if specified
|
337
|
-
if (key) return this.fields[key].getValue();
|
338
|
-
|
339
|
-
//Otherwise return entire form
|
340
|
-
var values = {};
|
341
|
-
_.each(this.fields, function(field) {
|
342
|
-
values[field.key] = field.getValue();
|
243
|
+
_.each(self.fieldsets, function(fieldset) {
|
244
|
+
$container.append(fieldset.render().el);
|
343
245
|
});
|
246
|
+
});
|
344
247
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
* @param {Object|String} key New values to set, or property to set
|
351
|
-
* @param val Value to set
|
352
|
-
*/
|
353
|
-
setValue: function(prop, val) {
|
354
|
-
var data = {};
|
355
|
-
if (typeof prop === 'string') {
|
356
|
-
data[prop] = val;
|
357
|
-
} else {
|
358
|
-
data = prop;
|
359
|
-
}
|
360
|
-
|
361
|
-
var key;
|
362
|
-
for (key in this.schema) {
|
363
|
-
if (data[key] !== undefined) {
|
364
|
-
this.fields[key].setValue(data[key]);
|
365
|
-
}
|
366
|
-
}
|
367
|
-
},
|
248
|
+
//Set the main element
|
249
|
+
this.setElement($form);
|
250
|
+
|
251
|
+
//Set class
|
252
|
+
$form.addClass(this.className);
|
368
253
|
|
369
|
-
|
370
|
-
|
254
|
+
return this;
|
255
|
+
},
|
371
256
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
257
|
+
/**
|
258
|
+
* Validate the data
|
259
|
+
*
|
260
|
+
* @return {Object} Validation errors
|
261
|
+
*/
|
262
|
+
validate: function() {
|
263
|
+
var self = this,
|
264
|
+
fields = this.fields,
|
265
|
+
model = this.model,
|
266
|
+
errors = {};
|
267
|
+
|
268
|
+
//Collect errors from schema validation
|
269
|
+
_.each(fields, function(field) {
|
270
|
+
var error = field.validate();
|
271
|
+
if (error) {
|
272
|
+
errors[field.key] = error;
|
384
273
|
}
|
385
|
-
}
|
274
|
+
});
|
386
275
|
|
387
|
-
|
388
|
-
|
276
|
+
//Get errors from default Backbone model validator
|
277
|
+
if (model && model.validate) {
|
278
|
+
var modelErrors = model.validate(this.getValue());
|
389
279
|
|
390
|
-
|
280
|
+
if (modelErrors) {
|
281
|
+
var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
|
391
282
|
|
392
|
-
|
393
|
-
|
283
|
+
//If errors are not in object form then just store on the error object
|
284
|
+
if (!isDictionary) {
|
285
|
+
errors._others = errors._others || [];
|
286
|
+
errors._others.push(modelErrors);
|
287
|
+
}
|
394
288
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
289
|
+
//Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
|
290
|
+
if (isDictionary) {
|
291
|
+
_.each(modelErrors, function(val, key) {
|
292
|
+
//Set error on field if there isn't one already
|
293
|
+
if (fields[key] && !errors[key]) {
|
294
|
+
fields[key].setError(val);
|
295
|
+
errors[key] = val;
|
296
|
+
}
|
400
297
|
|
401
|
-
|
402
|
-
|
298
|
+
else {
|
299
|
+
//Otherwise add to '_others' key
|
300
|
+
errors._others = errors._others || [];
|
301
|
+
var tmpErr = {};
|
302
|
+
tmpErr[key] = val;
|
303
|
+
errors._others.push(tmpErr);
|
304
|
+
}
|
305
|
+
});
|
306
|
+
}
|
403
307
|
}
|
308
|
+
}
|
404
309
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
trigger: function(event) {
|
409
|
-
if (event === 'focus') {
|
410
|
-
this.hasFocus = true;
|
411
|
-
}
|
412
|
-
else if (event === 'blur') {
|
413
|
-
this.hasFocus = false;
|
414
|
-
}
|
310
|
+
return _.isEmpty(errors) ? null : errors;
|
311
|
+
},
|
415
312
|
|
416
|
-
|
417
|
-
|
418
|
-
|
313
|
+
/**
|
314
|
+
* Update the model with all latest values.
|
315
|
+
*
|
316
|
+
* @param {Object} [options] Options to pass to Model#set (e.g. { silent: true })
|
317
|
+
*
|
318
|
+
* @return {Object} Validation errors
|
319
|
+
*/
|
320
|
+
commit: function(options) {
|
321
|
+
//Validate
|
322
|
+
var errors = this.validate();
|
323
|
+
if (errors) return errors;
|
419
324
|
|
420
|
-
|
325
|
+
//Commit
|
326
|
+
var modelError;
|
421
327
|
|
328
|
+
var setOptions = _.extend({
|
329
|
+
error: function(model, e) {
|
330
|
+
modelError = e;
|
331
|
+
}
|
332
|
+
}, options);
|
422
333
|
|
423
|
-
|
424
|
-
|
425
|
-
|
334
|
+
this.model.set(this.getValue(), setOptions);
|
335
|
+
|
336
|
+
if (modelError) return modelError;
|
337
|
+
},
|
426
338
|
|
427
|
-
|
339
|
+
/**
|
340
|
+
* Get all the field values as an object.
|
341
|
+
* Use this method when passing data instead of objects
|
342
|
+
*
|
343
|
+
* @param {String} [key] Specific field value to get
|
344
|
+
*/
|
345
|
+
getValue: function(key) {
|
346
|
+
//Return only given key if specified
|
347
|
+
if (key) return this.fields[key].getValue();
|
348
|
+
|
349
|
+
//Otherwise return entire form
|
350
|
+
var values = {};
|
351
|
+
_.each(this.fields, function(field) {
|
352
|
+
values[field.key] = field.getValue();
|
353
|
+
});
|
428
354
|
|
429
|
-
|
355
|
+
return values;
|
356
|
+
},
|
430
357
|
|
431
358
|
/**
|
432
|
-
*
|
359
|
+
* Update field values, referenced by key
|
433
360
|
*
|
434
|
-
* @param {Object}
|
435
|
-
* @param
|
436
|
-
* @return {Mixed}
|
437
|
-
* @api private
|
361
|
+
* @param {Object|String} key New values to set, or property to set
|
362
|
+
* @param val Value to set
|
438
363
|
*/
|
439
|
-
|
440
|
-
var
|
441
|
-
|
442
|
-
|
443
|
-
|
364
|
+
setValue: function(prop, val) {
|
365
|
+
var data = {};
|
366
|
+
if (typeof prop === 'string') {
|
367
|
+
data[prop] = val;
|
368
|
+
} else {
|
369
|
+
data = prop;
|
444
370
|
}
|
445
|
-
|
446
|
-
|
447
|
-
|
371
|
+
|
372
|
+
var key;
|
373
|
+
for (key in this.schema) {
|
374
|
+
if (data[key] !== undefined) {
|
375
|
+
this.fields[key].setValue(data[key]);
|
376
|
+
}
|
377
|
+
}
|
378
|
+
},
|
379
|
+
|
448
380
|
/**
|
449
|
-
*
|
450
|
-
*
|
451
|
-
*
|
452
|
-
*
|
453
|
-
*
|
454
|
-
*
|
455
|
-
* @param {String} Key
|
456
|
-
* @return {String} Title
|
381
|
+
* Returns the editor for a given field key
|
382
|
+
*
|
383
|
+
* @param {String} key
|
384
|
+
*
|
385
|
+
* @return {Editor}
|
457
386
|
*/
|
458
|
-
|
459
|
-
|
460
|
-
|
387
|
+
getEditor: function(key) {
|
388
|
+
var field = this.fields[key];
|
389
|
+
if (!field) throw 'Field not found: '+key;
|
461
390
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
return str;
|
466
|
-
};
|
391
|
+
return field.editor;
|
392
|
+
},
|
467
393
|
|
468
394
|
/**
|
469
|
-
*
|
470
|
-
* to user's settings when done to avoid conflicts.
|
471
|
-
* @param {String} Template string
|
472
|
-
* @return {Template} Compiled template
|
395
|
+
* Gives the first editor in the form focus
|
473
396
|
*/
|
474
|
-
|
475
|
-
|
476
|
-
var _interpolateBackup = _.templateSettings.interpolate;
|
477
|
-
|
478
|
-
//Set custom template settings
|
479
|
-
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
|
397
|
+
focus: function() {
|
398
|
+
if (this.hasFocus) return;
|
480
399
|
|
481
|
-
|
400
|
+
//Get the first field
|
401
|
+
var fieldset = this.fieldsets[0],
|
402
|
+
field = fieldset.getFieldAt(0);
|
482
403
|
|
483
|
-
|
484
|
-
_.templateSettings.interpolate = _interpolateBackup;
|
404
|
+
if (!field) return;
|
485
405
|
|
486
|
-
|
487
|
-
|
406
|
+
//Set focus
|
407
|
+
field.editor.focus();
|
408
|
+
},
|
488
409
|
|
489
410
|
/**
|
490
|
-
*
|
491
|
-
* If context is passed in, the template will be evaluated.
|
492
|
-
* @param {String} Template string
|
493
|
-
* @param {Object} Optional; values to replace in template
|
494
|
-
* @return {Template|String} Compiled template or the evaluated string
|
411
|
+
* Removes focus from the currently focused editor
|
495
412
|
*/
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
if (!context) {
|
500
|
-
return template;
|
501
|
-
} else {
|
502
|
-
return template(context);
|
503
|
-
}
|
504
|
-
};
|
505
|
-
|
413
|
+
blur: function() {
|
414
|
+
if (!this.hasFocus) return;
|
506
415
|
|
507
|
-
|
508
|
-
|
509
|
-
* @param {Function} Template compiler function
|
510
|
-
*/
|
511
|
-
helpers.setTemplateCompiler = function(compiler) {
|
512
|
-
helpers.compileTemplate = compiler;
|
513
|
-
};
|
514
|
-
|
515
|
-
|
516
|
-
/**
|
517
|
-
* Sets the templates to be used.
|
518
|
-
*
|
519
|
-
* If the templates passed in are strings, they will be compiled, expecting Mustache style tags,
|
520
|
-
* i.e. <div>{{varName}}</div>
|
521
|
-
*
|
522
|
-
* You can also pass in previously compiled Underscore templates, in which case you can use any style
|
523
|
-
* tags.
|
524
|
-
*
|
525
|
-
* @param {Object} templates
|
526
|
-
* @param {Object} classNames
|
527
|
-
*/
|
528
|
-
helpers.setTemplates = function(templates, classNames) {
|
529
|
-
var createTemplate = helpers.createTemplate;
|
530
|
-
|
531
|
-
Form.templates = Form.templates || {};
|
532
|
-
Form.classNames = Form.classNames || {};
|
533
|
-
|
534
|
-
//Set templates, compiling them if necessary
|
535
|
-
_.each(templates, function(template, key, index) {
|
536
|
-
if (_.isString(template)) template = createTemplate(template);
|
537
|
-
|
538
|
-
Form.templates[key] = template;
|
416
|
+
var focusedField = _.find(this.fields, function(field) {
|
417
|
+
return field.editor.hasFocus;
|
539
418
|
});
|
540
|
-
|
541
|
-
//Set class names
|
542
|
-
_.extend(Form.classNames, classNames);
|
543
|
-
};
|
544
|
-
|
545
|
-
|
546
|
-
/**
|
547
|
-
* Return the editor constructor for a given schema 'type'.
|
548
|
-
* Accepts strings for the default editors, or the reference to the constructor function
|
549
|
-
* for custom editors
|
550
|
-
*
|
551
|
-
* @param {String|Function} The schema type e.g. 'Text', 'Select', or the editor constructor e.g. editors.Date
|
552
|
-
* @param {Object} Options to pass to editor, including required 'key', 'schema'
|
553
|
-
* @return {Mixed} An instance of the mapped editor
|
554
|
-
*/
|
555
|
-
helpers.createEditor = function(schemaType, options) {
|
556
|
-
var constructorFn;
|
557
419
|
|
558
|
-
if (
|
559
|
-
|
560
|
-
} else {
|
561
|
-
constructorFn = schemaType;
|
562
|
-
}
|
420
|
+
if (focusedField) focusedField.editor.blur();
|
421
|
+
},
|
563
422
|
|
564
|
-
return new constructorFn(options);
|
565
|
-
};
|
566
|
-
|
567
|
-
|
568
423
|
/**
|
569
|
-
*
|
424
|
+
* Manages the hasFocus property
|
570
425
|
*
|
571
|
-
* @param {
|
572
|
-
* @return {Function}
|
426
|
+
* @param {String} event
|
573
427
|
*/
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
//Convert regular expressions to validators
|
578
|
-
if (_.isRegExp(validator)) {
|
579
|
-
return validators.regexp({ regexp: validator });
|
580
|
-
}
|
581
|
-
|
582
|
-
//Use a built-in validator if given a string
|
583
|
-
if (_.isString(validator)) {
|
584
|
-
if (!validators[validator]) throw new Error('Validator "'+validator+'" not found');
|
585
|
-
|
586
|
-
return validators[validator]();
|
428
|
+
trigger: function(event) {
|
429
|
+
if (event === 'focus') {
|
430
|
+
this.hasFocus = true;
|
587
431
|
}
|
588
|
-
|
589
|
-
|
590
|
-
if (_.isFunction(validator)) return validator;
|
591
|
-
|
592
|
-
//Use a customised built-in validator if given an object
|
593
|
-
if (_.isObject(validator) && validator.type) {
|
594
|
-
var config = validator;
|
595
|
-
|
596
|
-
return validators[config.type](config);
|
432
|
+
else if (event === 'blur') {
|
433
|
+
this.hasFocus = false;
|
597
434
|
}
|
598
|
-
|
599
|
-
//Unkown validator type
|
600
|
-
throw new Error('Invalid validator: ' + validator);
|
601
|
-
};
|
602
435
|
|
436
|
+
return Backbone.View.prototype.trigger.apply(this, arguments);
|
437
|
+
},
|
603
438
|
|
604
439
|
/**
|
605
|
-
*
|
440
|
+
* Override default remove function in order to remove embedded views
|
606
441
|
*
|
607
|
-
*
|
608
|
-
*
|
442
|
+
* TODO: If editors are included directly with data-editors="x", they need to be removed
|
443
|
+
* May be best to use XView to manage adding/removing views
|
609
444
|
*/
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
}
|
614
|
-
|
615
|
-
|
445
|
+
remove: function() {
|
446
|
+
_.each(this.fieldsets, function(fieldset) {
|
447
|
+
fieldset.remove();
|
448
|
+
});
|
449
|
+
|
450
|
+
_.each(this.fields, function(field) {
|
451
|
+
field.remove();
|
452
|
+
});
|
616
453
|
|
454
|
+
Backbone.View.prototype.remove.call(this);
|
455
|
+
}
|
617
456
|
|
618
|
-
|
457
|
+
}, {
|
619
458
|
|
620
|
-
|
459
|
+
//STATICS
|
460
|
+
template: _.template('\
|
461
|
+
<form data-fieldsets></form>\
|
462
|
+
', null, this.templateSettings),
|
463
|
+
|
464
|
+
templateSettings: {
|
465
|
+
evaluate: /<%([\s\S]+?)%>/g,
|
466
|
+
interpolate: /<%=([\s\S]+?)%>/g,
|
467
|
+
escape: /<%-([\s\S]+?)%>/g
|
468
|
+
},
|
469
|
+
|
470
|
+
editors: {}
|
471
|
+
|
472
|
+
});
|
621
473
|
|
622
474
|
|
623
475
|
//==================================================================================================
|
@@ -633,7 +485,7 @@ Form.validators = (function() {
|
|
633
485
|
regexp: 'Invalid',
|
634
486
|
email: 'Invalid email address',
|
635
487
|
url: 'Invalid URL',
|
636
|
-
match: 'Must match field "
|
488
|
+
match: _.template('Must match field "<%= field %>"', null, Form.templateSettings)
|
637
489
|
};
|
638
490
|
|
639
491
|
validators.required = function(options) {
|
@@ -647,7 +499,7 @@ Form.validators = (function() {
|
|
647
499
|
|
648
500
|
var err = {
|
649
501
|
type: options.type,
|
650
|
-
message:
|
502
|
+
message: _.isFunction(options.message) ? options.message(options) : options.message
|
651
503
|
};
|
652
504
|
|
653
505
|
if (value === null || value === undefined || value === false || value === '') return err;
|
@@ -667,7 +519,7 @@ Form.validators = (function() {
|
|
667
519
|
|
668
520
|
var err = {
|
669
521
|
type: options.type,
|
670
|
-
message:
|
522
|
+
message: _.isFunction(options.message) ? options.message(options) : options.message
|
671
523
|
};
|
672
524
|
|
673
525
|
//Don't check empty values (add a 'required' validator for this)
|
@@ -710,7 +562,7 @@ Form.validators = (function() {
|
|
710
562
|
|
711
563
|
var err = {
|
712
564
|
type: options.type,
|
713
|
-
message:
|
565
|
+
message: _.isFunction(options.message) ? options.message(options) : options.message
|
714
566
|
};
|
715
567
|
|
716
568
|
//Don't check empty values (add a 'required' validator for this)
|
@@ -727,1639 +579,1807 @@ Form.validators = (function() {
|
|
727
579
|
|
728
580
|
|
729
581
|
//==================================================================================================
|
730
|
-
//
|
582
|
+
//FIELDSET
|
731
583
|
//==================================================================================================
|
732
584
|
|
733
|
-
Form.
|
734
|
-
|
735
|
-
var helpers = Form.helpers,
|
736
|
-
templates = Form.templates;
|
737
|
-
|
738
|
-
return Backbone.View.extend({
|
739
|
-
|
740
|
-
/**
|
741
|
-
* @param {Object} Options
|
742
|
-
* Required:
|
743
|
-
* key {String} : The model attribute key
|
744
|
-
* Optional:
|
745
|
-
* schema {Object} : Schema for the field
|
746
|
-
* value {Mixed} : Pass value when not using a model. Use getValue() to get out value
|
747
|
-
* model {Backbone.Model} : Use instead of value, and use commit().
|
748
|
-
* idPrefix {String} : Prefix to add to the editor DOM element's ID
|
749
|
-
*/
|
750
|
-
/**
|
751
|
-
* Creates a new field
|
752
|
-
*
|
753
|
-
* @param {Object} options
|
754
|
-
* @param {Object} [options.schema] Field schema. Defaults to { type: 'Text' }
|
755
|
-
* @param {Model} [options.model] Model the field relates to. Required if options.data is not set.
|
756
|
-
* @param {String} [options.key] Model key/attribute the field relates to.
|
757
|
-
* @param {Mixed} [options.value] Field value. Required if options.model is not set.
|
758
|
-
* @param {String} [options.idPrefix] Prefix for the editor ID. By default, the model's CID is used.
|
759
|
-
*
|
760
|
-
* @return {Field}
|
761
|
-
*/
|
762
|
-
initialize: function(options) {
|
763
|
-
options = options || {};
|
764
|
-
|
765
|
-
this.form = options.form;
|
766
|
-
this.key = options.key;
|
767
|
-
this.value = options.value;
|
768
|
-
this.model = options.model;
|
769
|
-
|
770
|
-
//Turn schema shorthand notation (e.g. 'Text') into schema object
|
771
|
-
if (_.isString(options.schema)) options.schema = { type: options.schema };
|
772
|
-
|
773
|
-
//Set schema defaults
|
774
|
-
this.schema = _.extend({
|
775
|
-
type: 'Text',
|
776
|
-
title: helpers.keyToTitle(this.key),
|
777
|
-
template: 'field'
|
778
|
-
}, options.schema);
|
779
|
-
},
|
780
|
-
|
781
|
-
|
782
|
-
/**
|
783
|
-
* Provides the context for rendering the field
|
784
|
-
* Override this to extend the default context
|
785
|
-
*
|
786
|
-
* @param {Object} schema
|
787
|
-
* @param {View} editor
|
788
|
-
*
|
789
|
-
* @return {Object} Locals passed to the template
|
790
|
-
*/
|
791
|
-
renderingContext: function(schema, editor) {
|
792
|
-
return {
|
793
|
-
key: this.key,
|
794
|
-
title: schema.title,
|
795
|
-
id: editor.id,
|
796
|
-
type: schema.type,
|
797
|
-
editor: '<b class="bbf-tmp-editor"></b>',
|
798
|
-
help: '<b class="bbf-tmp-help"></b>',
|
799
|
-
error: '<b class="bbf-tmp-error"></b>'
|
800
|
-
};
|
801
|
-
},
|
585
|
+
Form.Fieldset = Backbone.View.extend({
|
802
586
|
|
587
|
+
/**
|
588
|
+
* Constructor
|
589
|
+
*
|
590
|
+
* Valid fieldset schemas:
|
591
|
+
* ['field1', 'field2']
|
592
|
+
* { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
|
593
|
+
*
|
594
|
+
* @param {String[]|Object[]} options.schema Fieldset schema
|
595
|
+
* @param {Object} options.fields Form fields
|
596
|
+
*/
|
597
|
+
initialize: function(options) {
|
598
|
+
options = options || {};
|
803
599
|
|
804
|
-
|
805
|
-
|
806
|
-
*/
|
807
|
-
render: function() {
|
808
|
-
var schema = this.schema,
|
809
|
-
templates = Form.templates;
|
810
|
-
|
811
|
-
//Standard options that will go to all editors
|
812
|
-
var options = {
|
813
|
-
form: this.form,
|
814
|
-
key: this.key,
|
815
|
-
schema: schema,
|
816
|
-
idPrefix: this.options.idPrefix,
|
817
|
-
id: this.getId()
|
818
|
-
};
|
600
|
+
//Create the full fieldset schema, merging defaults etc.
|
601
|
+
var schema = this.schema = this.createSchema(options.schema);
|
819
602
|
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
603
|
+
//Store the fields for this fieldset
|
604
|
+
this.fields = _.pick(options.fields, schema.fields);
|
605
|
+
|
606
|
+
//Override defaults
|
607
|
+
this.template = options.template || this.constructor.template;
|
608
|
+
},
|
826
609
|
|
827
|
-
|
828
|
-
|
610
|
+
/**
|
611
|
+
* Creates the full fieldset schema, normalising, merging defaults etc.
|
612
|
+
*
|
613
|
+
* @param {String[]|Object[]} schema
|
614
|
+
*
|
615
|
+
* @return {Object}
|
616
|
+
*/
|
617
|
+
createSchema: function(schema) {
|
618
|
+
//Normalise to object
|
619
|
+
if (_.isArray(schema)) {
|
620
|
+
schema = { fields: schema };
|
621
|
+
}
|
829
622
|
|
830
|
-
|
831
|
-
|
623
|
+
//Add null legend to prevent template error
|
624
|
+
schema.legend = schema.legend || null;
|
832
625
|
|
833
|
-
|
834
|
-
|
835
|
-
$field.find('label[for="'+editor.id+'"]').first().remove();
|
836
|
-
}
|
626
|
+
return schema;
|
627
|
+
},
|
837
628
|
|
838
|
-
|
839
|
-
|
629
|
+
/**
|
630
|
+
* Returns the field for a given index
|
631
|
+
*
|
632
|
+
* @param {Number} index
|
633
|
+
*
|
634
|
+
* @return {Field}
|
635
|
+
*/
|
636
|
+
getFieldAt: function(index) {
|
637
|
+
var key = this.schema.fields[index];
|
840
638
|
|
841
|
-
|
842
|
-
|
843
|
-
this.$help.empty();
|
844
|
-
if (this.schema.help) this.$help.html(this.schema.help);
|
639
|
+
return this.fields[key];
|
640
|
+
},
|
845
641
|
|
846
|
-
|
847
|
-
|
848
|
-
|
642
|
+
/**
|
643
|
+
* Returns data to pass to template
|
644
|
+
*
|
645
|
+
* @return {Object}
|
646
|
+
*/
|
647
|
+
templateData: function() {
|
648
|
+
return this.schema;
|
649
|
+
},
|
849
650
|
|
850
|
-
|
851
|
-
|
651
|
+
/**
|
652
|
+
* Renders the fieldset and fields
|
653
|
+
*
|
654
|
+
* @return {Fieldset} this
|
655
|
+
*/
|
656
|
+
render: function() {
|
657
|
+
var schema = this.schema,
|
658
|
+
fields = this.fields;
|
852
659
|
|
853
|
-
|
854
|
-
|
660
|
+
//Render fieldset
|
661
|
+
var $fieldset = $($.trim(this.template(_.result(this, 'templateData'))));
|
855
662
|
|
856
|
-
|
857
|
-
|
663
|
+
//Render fields
|
664
|
+
$fieldset.find('[data-fields]').add($fieldset).each(function(i, el) {
|
665
|
+
var $container = $(el),
|
666
|
+
selection = $container.attr('data-fields');
|
858
667
|
|
859
|
-
return
|
860
|
-
},
|
668
|
+
if (_.isUndefined(selection)) return;
|
861
669
|
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
*/
|
867
|
-
getId: function() {
|
868
|
-
var prefix = this.options.idPrefix,
|
869
|
-
id = this.key;
|
670
|
+
_.each(fields, function(field) {
|
671
|
+
$container.append(field.render().el);
|
672
|
+
});
|
673
|
+
});
|
870
674
|
|
871
|
-
|
872
|
-
id = id.replace(/\./g, '_');
|
675
|
+
this.setElement($fieldset);
|
873
676
|
|
874
|
-
|
875
|
-
|
876
|
-
if (_.isNull(prefix)) return id;
|
677
|
+
return this;
|
678
|
+
},
|
877
679
|
|
878
|
-
|
879
|
-
|
680
|
+
/**
|
681
|
+
* Remove embedded views then self
|
682
|
+
*/
|
683
|
+
remove: function() {
|
684
|
+
_.each(this.fields, function(field) {
|
685
|
+
field.remove();
|
686
|
+
});
|
880
687
|
|
881
|
-
|
882
|
-
|
688
|
+
Backbone.View.prototype.remove.call(this);
|
689
|
+
}
|
690
|
+
|
691
|
+
}, {
|
692
|
+
//STATICS
|
883
693
|
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
694
|
+
template: _.template('\
|
695
|
+
<fieldset data-fields>\
|
696
|
+
<% if (legend) { %>\
|
697
|
+
<legend><%= legend %></legend>\
|
698
|
+
<% } %>\
|
699
|
+
</fieldset>\
|
700
|
+
', null, Form.templateSettings)
|
891
701
|
|
892
|
-
|
893
|
-
this.setError(error.message);
|
894
|
-
} else {
|
895
|
-
this.clearError();
|
896
|
-
}
|
702
|
+
});
|
897
703
|
|
898
|
-
return error;
|
899
|
-
},
|
900
704
|
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
* @param {String} msg Error message
|
905
|
-
*/
|
906
|
-
setError: function(msg) {
|
907
|
-
//Object and NestedModel types set their own errors internally
|
908
|
-
if (this.editor.hasNestedForm) return;
|
705
|
+
//==================================================================================================
|
706
|
+
//FIELD
|
707
|
+
//==================================================================================================
|
909
708
|
|
910
|
-
|
709
|
+
Form.Field = Backbone.View.extend({
|
911
710
|
|
912
|
-
|
711
|
+
/**
|
712
|
+
* Constructor
|
713
|
+
*
|
714
|
+
* @param {Object} options.key
|
715
|
+
* @param {Object} options.form
|
716
|
+
* @param {Object} [options.schema]
|
717
|
+
* @param {Function} [options.schema.template]
|
718
|
+
* @param {Backbone.Model} [options.model]
|
719
|
+
* @param {Object} [options.value]
|
720
|
+
* @param {String} [options.idPrefix]
|
721
|
+
* @param {Function} [options.template]
|
722
|
+
* @param {Function} [options.errorClassName]
|
723
|
+
*/
|
724
|
+
initialize: function(options) {
|
725
|
+
options = options || {};
|
913
726
|
|
914
|
-
|
915
|
-
|
916
|
-
} else if (this.$help) {
|
917
|
-
this.$help.html(msg);
|
918
|
-
}
|
919
|
-
},
|
727
|
+
//Store important data
|
728
|
+
_.extend(this, _.pick(options, 'form', 'key', 'model', 'value', 'idPrefix'));
|
920
729
|
|
921
|
-
|
922
|
-
|
923
|
-
*/
|
924
|
-
clearError: function() {
|
925
|
-
var errClass = Form.classNames.error;
|
730
|
+
//Create the full field schema, merging defaults etc.
|
731
|
+
var schema = this.schema = this.createSchema(options.schema);
|
926
732
|
|
927
|
-
|
733
|
+
//Override defaults
|
734
|
+
this.template = options.template || schema.template || this.constructor.template;
|
735
|
+
this.errorClassName = options.errorClassName || this.constructor.errorClassName;
|
928
736
|
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
} else if (this.$help) {
|
933
|
-
this.$help.empty();
|
737
|
+
//Create editor
|
738
|
+
this.editor = this.createEditor();
|
739
|
+
},
|
934
740
|
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
741
|
+
/**
|
742
|
+
* Creates the full field schema, merging defaults etc.
|
743
|
+
*
|
744
|
+
* @param {Object|String} schema
|
745
|
+
*
|
746
|
+
* @return {Object}
|
747
|
+
*/
|
748
|
+
createSchema: function(schema) {
|
749
|
+
if (_.isString(schema)) schema = { type: schema };
|
940
750
|
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
},
|
751
|
+
//Set defaults
|
752
|
+
schema = _.extend({
|
753
|
+
type: 'Text',
|
754
|
+
title: this.createTitle()
|
755
|
+
}, schema);
|
947
756
|
|
948
|
-
|
949
|
-
|
950
|
-
*
|
951
|
-
* @return {Mixed}
|
952
|
-
*/
|
953
|
-
getValue: function() {
|
954
|
-
return this.editor.getValue();
|
955
|
-
},
|
757
|
+
//Get the real constructor function i.e. if type is a string such as 'Text'
|
758
|
+
schema.type = (_.isString(schema.type)) ? Form.editors[schema.type] : schema.type;
|
956
759
|
|
957
|
-
|
958
|
-
|
959
|
-
*
|
960
|
-
* @param {Mixed} value
|
961
|
-
*/
|
962
|
-
setValue: function(value) {
|
963
|
-
this.editor.setValue(value);
|
964
|
-
},
|
760
|
+
return schema;
|
761
|
+
},
|
965
762
|
|
966
|
-
|
967
|
-
|
968
|
-
|
763
|
+
/**
|
764
|
+
* Creates the editor specified in the schema; either an editor string name or
|
765
|
+
* a constructor function
|
766
|
+
*
|
767
|
+
* @return {View}
|
768
|
+
*/
|
769
|
+
createEditor: function() {
|
770
|
+
var options = _.extend(
|
771
|
+
_.pick(this, 'schema', 'form', 'key', 'model', 'value'),
|
772
|
+
{ id: this.createEditorId() }
|
773
|
+
);
|
969
774
|
|
970
|
-
|
971
|
-
|
972
|
-
|
775
|
+
var constructorFn = this.schema.type;
|
776
|
+
|
777
|
+
return new constructorFn(options);
|
778
|
+
},
|
973
779
|
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
780
|
+
/**
|
781
|
+
* Creates the ID that will be assigned to the editor
|
782
|
+
*
|
783
|
+
* @return {String}
|
784
|
+
*/
|
785
|
+
createEditorId: function() {
|
786
|
+
var prefix = this.idPrefix,
|
787
|
+
id = this.key;
|
979
788
|
|
980
|
-
|
981
|
-
|
789
|
+
//Replace periods with underscores (e.g. for when using paths)
|
790
|
+
id = id.replace(/\./g, '_');
|
982
791
|
|
983
|
-
|
792
|
+
//If a specific ID prefix is set, use it
|
793
|
+
if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
|
794
|
+
if (_.isNull(prefix)) return id;
|
984
795
|
|
985
|
-
|
796
|
+
//Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
|
797
|
+
if (this.model) return this.model.cid + '_' + id;
|
986
798
|
|
799
|
+
return id;
|
800
|
+
},
|
987
801
|
|
988
|
-
|
989
|
-
|
990
|
-
|
802
|
+
/**
|
803
|
+
* Create the default field title (label text) from the key name.
|
804
|
+
* (Converts 'camelCase' to 'Camel Case')
|
805
|
+
*
|
806
|
+
* @return {String}
|
807
|
+
*/
|
808
|
+
createTitle: function() {
|
809
|
+
var str = this.key;
|
991
810
|
|
992
|
-
|
811
|
+
//Add spaces
|
812
|
+
str = str.replace(/([A-Z])/g, ' $1');
|
993
813
|
|
994
|
-
|
814
|
+
//Uppercase first character
|
815
|
+
str = str.replace(/^./, function(str) { return str.toUpperCase(); });
|
995
816
|
|
996
|
-
|
817
|
+
return str;
|
818
|
+
},
|
997
819
|
|
998
820
|
/**
|
999
|
-
*
|
821
|
+
* Returns the data to be passed to the template
|
1000
822
|
*
|
1001
|
-
* @
|
1002
|
-
* Optional:
|
1003
|
-
* model {Backbone.Model} : Use instead of value, and use commit().
|
1004
|
-
* key {String} : The model attribute key. Required when using 'model'
|
1005
|
-
* value {String} : When not using a model. If neither provided, defaultValue will be used.
|
1006
|
-
* schema {Object} : May be required by some editors
|
823
|
+
* @return {Object}
|
1007
824
|
*/
|
1008
|
-
|
825
|
+
templateData: function() {
|
826
|
+
var schema = this.schema;
|
827
|
+
|
828
|
+
return {
|
829
|
+
help: schema.help || '',
|
830
|
+
title: schema.title,
|
831
|
+
fieldAttrs: schema.fieldAttrs,
|
832
|
+
editorAttrs: schema.editorAttrs,
|
833
|
+
key: this.key,
|
834
|
+
editorId: this.editor.id
|
835
|
+
};
|
836
|
+
},
|
1009
837
|
|
1010
|
-
|
838
|
+
/**
|
839
|
+
* Render the field and editor
|
840
|
+
*
|
841
|
+
* @return {Field} self
|
842
|
+
*/
|
843
|
+
render: function() {
|
844
|
+
var schema = this.schema,
|
845
|
+
editor = this.editor;
|
1011
846
|
|
1012
|
-
|
847
|
+
//Only render the editor if Hidden
|
848
|
+
if (schema.type == Form.editors.Hidden) {
|
849
|
+
return this.setElement(editor.render().el);
|
850
|
+
}
|
1013
851
|
|
1014
|
-
|
1015
|
-
|
852
|
+
//Render field
|
853
|
+
var $field = $($.trim(this.template(_.result(this, 'templateData'))));
|
1016
854
|
|
1017
|
-
|
1018
|
-
|
855
|
+
if (schema.fieldClass) $field.addClass(schema.fieldClass);
|
856
|
+
if (schema.fieldAttrs) $field.attr(schema.fieldAttrs);
|
1019
857
|
|
1020
|
-
|
858
|
+
//Render editor
|
859
|
+
$field.find('[data-editor]').add($field).each(function(i, el) {
|
860
|
+
var $container = $(el),
|
861
|
+
selection = $container.attr('data-editor');
|
1021
862
|
|
1022
|
-
|
1023
|
-
}
|
1024
|
-
else if (options.value) {
|
1025
|
-
this.value = options.value;
|
1026
|
-
}
|
863
|
+
if (_.isUndefined(selection)) return;
|
1027
864
|
|
1028
|
-
|
865
|
+
$container.append(editor.render().el);
|
866
|
+
});
|
1029
867
|
|
1030
|
-
|
1031
|
-
this.form = options.form;
|
1032
|
-
this.schema = options.schema || {};
|
1033
|
-
this.validators = options.validators || this.schema.validators;
|
868
|
+
this.setElement($field);
|
1034
869
|
|
1035
|
-
|
1036
|
-
|
870
|
+
return this;
|
871
|
+
},
|
1037
872
|
|
1038
|
-
|
1039
|
-
|
873
|
+
/**
|
874
|
+
* Check the validity of the field
|
875
|
+
*
|
876
|
+
* @return {String}
|
877
|
+
*/
|
878
|
+
validate: function() {
|
879
|
+
var error = this.editor.validate();
|
1040
880
|
|
1041
|
-
|
1042
|
-
|
1043
|
-
}
|
881
|
+
if (error) {
|
882
|
+
this.setError(error.message);
|
883
|
+
} else {
|
884
|
+
this.clearError();
|
885
|
+
}
|
1044
886
|
|
1045
|
-
|
1046
|
-
|
1047
|
-
},
|
887
|
+
return error;
|
888
|
+
},
|
1048
889
|
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
890
|
+
/**
|
891
|
+
* Set the field into an error state, adding the error class and setting the error message
|
892
|
+
*
|
893
|
+
* @param {String} msg Error message
|
894
|
+
*/
|
895
|
+
setError: function(msg) {
|
896
|
+
//Nested form editors (e.g. Object) set their errors internally
|
897
|
+
if (this.editor.hasNestedForm) return;
|
1052
898
|
|
1053
|
-
|
1054
|
-
|
1055
|
-
},
|
899
|
+
//Add error CSS class
|
900
|
+
this.$el.addClass(this.errorClassName);
|
1056
901
|
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
902
|
+
//Set error message
|
903
|
+
this.$('[data-error]').html(msg);
|
904
|
+
},
|
1060
905
|
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
*/
|
1068
|
-
getName: function() {
|
1069
|
-
var key = this.key || '';
|
1070
|
-
|
1071
|
-
//Replace periods with underscores (e.g. for when using paths)
|
1072
|
-
return key.replace(/\./g, '_');
|
1073
|
-
},
|
906
|
+
/**
|
907
|
+
* Clear the error state and reset the help message
|
908
|
+
*/
|
909
|
+
clearError: function() {
|
910
|
+
//Remove error CSS class
|
911
|
+
this.$el.removeClass(this.errorClassName);
|
1074
912
|
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
*
|
1079
|
-
* @return {Mixed} error
|
1080
|
-
*/
|
1081
|
-
commit: function(options) {
|
1082
|
-
var error = this.validate();
|
1083
|
-
if (error) return error;
|
1084
|
-
|
1085
|
-
this.listenTo(this.model, 'invalid', function(model, e) {
|
1086
|
-
error = e;
|
1087
|
-
});
|
1088
|
-
this.model.set(this.key, this.getValue(), options);
|
913
|
+
//Clear error message
|
914
|
+
this.$('[data-error]').empty();
|
915
|
+
},
|
1089
916
|
|
1090
|
-
|
1091
|
-
|
917
|
+
/**
|
918
|
+
* Update the model with the new value from the editor
|
919
|
+
*
|
920
|
+
* @return {Mixed}
|
921
|
+
*/
|
922
|
+
commit: function() {
|
923
|
+
return this.editor.commit();
|
924
|
+
},
|
1092
925
|
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
error = null,
|
1102
|
-
value = this.getValue(),
|
1103
|
-
formValues = this.form ? this.form.getValue() : {},
|
1104
|
-
validators = this.validators,
|
1105
|
-
getValidator = Form.helpers.getValidator;
|
1106
|
-
|
1107
|
-
if (validators) {
|
1108
|
-
//Run through validators until an error is found
|
1109
|
-
_.every(validators, function(validator) {
|
1110
|
-
error = getValidator(validator)(value, formValues);
|
1111
|
-
|
1112
|
-
return error ? false : true;
|
1113
|
-
});
|
1114
|
-
}
|
926
|
+
/**
|
927
|
+
* Get the value from the editor
|
928
|
+
*
|
929
|
+
* @return {Mixed}
|
930
|
+
*/
|
931
|
+
getValue: function() {
|
932
|
+
return this.editor.getValue();
|
933
|
+
},
|
1115
934
|
|
1116
|
-
|
1117
|
-
|
935
|
+
/**
|
936
|
+
* Set/change the value of the editor
|
937
|
+
*
|
938
|
+
* @param {Mixed} value
|
939
|
+
*/
|
940
|
+
setValue: function(value) {
|
941
|
+
this.editor.setValue(value);
|
942
|
+
},
|
1118
943
|
|
944
|
+
/**
|
945
|
+
* Give the editor focus
|
946
|
+
*/
|
947
|
+
focus: function() {
|
948
|
+
this.editor.focus();
|
949
|
+
},
|
1119
950
|
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
}
|
951
|
+
/**
|
952
|
+
* Remove focus from the editor
|
953
|
+
*/
|
954
|
+
blur: function() {
|
955
|
+
this.editor.blur();
|
956
|
+
},
|
1127
957
|
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
//TEXT
|
1134
|
-
editors.Text = editors.Base.extend({
|
1135
|
-
|
1136
|
-
tagName: 'input',
|
1137
|
-
|
1138
|
-
defaultValue: '',
|
1139
|
-
|
1140
|
-
previousValue: '',
|
1141
|
-
|
1142
|
-
events: {
|
1143
|
-
'keyup': 'determineChange',
|
1144
|
-
'keypress': function(event) {
|
1145
|
-
var self = this;
|
1146
|
-
setTimeout(function() {
|
1147
|
-
self.determineChange();
|
1148
|
-
}, 0);
|
1149
|
-
},
|
1150
|
-
'select': function(event) {
|
1151
|
-
this.trigger('select', this);
|
1152
|
-
},
|
1153
|
-
'focus': function(event) {
|
1154
|
-
this.trigger('focus', this);
|
1155
|
-
},
|
1156
|
-
'blur': function(event) {
|
1157
|
-
this.trigger('blur', this);
|
1158
|
-
}
|
1159
|
-
},
|
958
|
+
/**
|
959
|
+
* Remove the field and editor views
|
960
|
+
*/
|
961
|
+
remove: function() {
|
962
|
+
this.editor.remove();
|
1160
963
|
|
1161
|
-
|
1162
|
-
|
964
|
+
Backbone.View.prototype.remove.call(this);
|
965
|
+
}
|
1163
966
|
|
1164
|
-
|
967
|
+
}, {
|
968
|
+
//STATICS
|
1165
969
|
|
1166
|
-
|
1167
|
-
|
970
|
+
template: _.template('\
|
971
|
+
<div>\
|
972
|
+
<label for="<%= editorId %>"><%= title %></label>\
|
973
|
+
<div>\
|
974
|
+
<span data-editor></span>\
|
975
|
+
<div data-error></div>\
|
976
|
+
<div><%= help %></div>\
|
977
|
+
</div>\
|
978
|
+
</div>\
|
979
|
+
', null, Form.templateSettings),
|
1168
980
|
|
1169
|
-
|
1170
|
-
|
981
|
+
/**
|
982
|
+
* CSS class name added to the field when there is a validation error
|
983
|
+
*/
|
984
|
+
errorClassName: 'error'
|
1171
985
|
|
1172
|
-
|
1173
|
-
},
|
986
|
+
});
|
1174
987
|
|
1175
|
-
/**
|
1176
|
-
* Adds the editor to the DOM
|
1177
|
-
*/
|
1178
|
-
render: function() {
|
1179
|
-
this.setValue(this.value);
|
1180
988
|
|
1181
|
-
|
1182
|
-
|
989
|
+
//==================================================================================================
|
990
|
+
//NESTEDFIELD
|
991
|
+
//==================================================================================================
|
1183
992
|
|
1184
|
-
|
1185
|
-
var currentValue = this.$el.val();
|
1186
|
-
var changed = (currentValue !== this.previousValue);
|
993
|
+
Form.NestedField = Form.Field.extend({
|
1187
994
|
|
1188
|
-
|
1189
|
-
|
995
|
+
template: _.template($.trim('\
|
996
|
+
<div>\
|
997
|
+
<span data-editor></span>\
|
998
|
+
<% if (help) { %>\
|
999
|
+
<div><%= help %></div>\
|
1000
|
+
<% } %>\
|
1001
|
+
<div data-error></div>\
|
1002
|
+
</div>\
|
1003
|
+
'), null, Form.templateSettings)
|
1190
1004
|
|
1191
|
-
|
1192
|
-
}
|
1193
|
-
},
|
1005
|
+
});
|
1194
1006
|
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1007
|
+
/**
|
1008
|
+
* Base editor (interface). To be extended, not used directly
|
1009
|
+
*
|
1010
|
+
* @param {Object} options
|
1011
|
+
* @param {String} [options.id] Editor ID
|
1012
|
+
* @param {Model} [options.model] Use instead of value, and use commit()
|
1013
|
+
* @param {String} [options.key] The model attribute key. Required when using 'model'
|
1014
|
+
* @param {Mixed} [options.value] When not using a model. If neither provided, defaultValue will be used
|
1015
|
+
* @param {Object} [options.schema] Field schema; may be required by some editors
|
1016
|
+
* @param {Object} [options.validators] Validators; falls back to those stored on schema
|
1017
|
+
* @param {Object} [options.form] The form
|
1018
|
+
*/
|
1019
|
+
Form.Editor = Form.editors.Base = Backbone.View.extend({
|
1202
1020
|
|
1203
|
-
|
1204
|
-
* Sets the value of the form element
|
1205
|
-
* @param {String}
|
1206
|
-
*/
|
1207
|
-
setValue: function(value) {
|
1208
|
-
this.$el.val(value);
|
1209
|
-
},
|
1021
|
+
defaultValue: null,
|
1210
1022
|
|
1211
|
-
|
1212
|
-
if (this.hasFocus) return;
|
1023
|
+
hasFocus: false,
|
1213
1024
|
|
1214
|
-
|
1215
|
-
}
|
1025
|
+
initialize: function(options) {
|
1026
|
+
var options = options || {};
|
1216
1027
|
|
1217
|
-
|
1218
|
-
|
1028
|
+
//Set initial value
|
1029
|
+
if (options.model) {
|
1030
|
+
if (!options.key) throw "Missing option: 'key'";
|
1219
1031
|
|
1220
|
-
this
|
1221
|
-
},
|
1032
|
+
this.model = options.model;
|
1222
1033
|
|
1223
|
-
|
1224
|
-
this.$el.select();
|
1034
|
+
this.value = this.model.get(options.key);
|
1225
1035
|
}
|
1036
|
+
else if (options.value) {
|
1037
|
+
this.value = options.value;
|
1038
|
+
}
|
1039
|
+
|
1040
|
+
if (this.value === undefined) this.value = this.defaultValue;
|
1041
|
+
|
1042
|
+
//Store important data
|
1043
|
+
_.extend(this, _.pick(options, 'key', 'form'));
|
1226
1044
|
|
1227
|
-
|
1045
|
+
var schema = this.schema = options.schema || {};
|
1228
1046
|
|
1047
|
+
this.validators = options.validators || schema.validators;
|
1048
|
+
|
1049
|
+
//Main attributes
|
1050
|
+
this.$el.attr('id', this.id);
|
1051
|
+
this.$el.attr('name', this.getName());
|
1052
|
+
if (schema.editorClass) this.$el.addClass(schema.editorClass);
|
1053
|
+
if (schema.editorAttrs) this.$el.attr(schema.editorAttrs);
|
1054
|
+
},
|
1229
1055
|
|
1230
1056
|
/**
|
1231
|
-
*
|
1232
|
-
*
|
1057
|
+
* Get the value for the form input 'name' attribute
|
1058
|
+
*
|
1059
|
+
* @return {String}
|
1060
|
+
*
|
1061
|
+
* @api private
|
1233
1062
|
*/
|
1234
|
-
|
1063
|
+
getName: function() {
|
1064
|
+
var key = this.key || '';
|
1235
1065
|
|
1236
|
-
|
1066
|
+
//Replace periods with underscores (e.g. for when using paths)
|
1067
|
+
return key.replace(/\./g, '_');
|
1068
|
+
},
|
1237
1069
|
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1070
|
+
/**
|
1071
|
+
* Get editor value
|
1072
|
+
* Extend and override this method to reflect changes in the DOM
|
1073
|
+
*
|
1074
|
+
* @return {Mixed}
|
1075
|
+
*/
|
1076
|
+
getValue: function() {
|
1077
|
+
return this.value;
|
1078
|
+
},
|
1241
1079
|
|
1242
|
-
|
1243
|
-
|
1080
|
+
/**
|
1081
|
+
* Set editor value
|
1082
|
+
* Extend and override this method to reflect changes in the DOM
|
1083
|
+
*
|
1084
|
+
* @param {Mixed} value
|
1085
|
+
*/
|
1086
|
+
setValue: function(value) {
|
1087
|
+
this.value = value;
|
1088
|
+
},
|
1244
1089
|
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1090
|
+
/**
|
1091
|
+
* Give the editor focus
|
1092
|
+
* Extend and override this method
|
1093
|
+
*/
|
1094
|
+
focus: function() {
|
1095
|
+
throw 'Not implemented';
|
1096
|
+
},
|
1097
|
+
|
1098
|
+
/**
|
1099
|
+
* Remove focus from the editor
|
1100
|
+
* Extend and override this method
|
1101
|
+
*/
|
1102
|
+
blur: function() {
|
1103
|
+
throw 'Not implemented';
|
1104
|
+
},
|
1248
1105
|
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
//Allow backspace
|
1261
|
-
if (event.charCode === 0) {
|
1262
|
-
delayedDetermineChange();
|
1263
|
-
return;
|
1264
|
-
}
|
1106
|
+
/**
|
1107
|
+
* Update the model with the current value
|
1108
|
+
*
|
1109
|
+
* @param {Object} [options] Options to pass to model.set()
|
1110
|
+
* @param {Boolean} [options.validate] Set to true to trigger built-in model validation
|
1111
|
+
*
|
1112
|
+
* @return {Mixed} error
|
1113
|
+
*/
|
1114
|
+
commit: function(options) {
|
1115
|
+
var error = this.validate();
|
1116
|
+
if (error) return error;
|
1265
1117
|
|
1266
|
-
|
1267
|
-
|
1118
|
+
this.listenTo(this.model, 'invalid', function(model, e) {
|
1119
|
+
error = e;
|
1120
|
+
});
|
1121
|
+
this.model.set(this.key, this.getValue(), options);
|
1268
1122
|
|
1269
|
-
|
1123
|
+
if (error) return error;
|
1124
|
+
},
|
1270
1125
|
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1126
|
+
/**
|
1127
|
+
* Check validity
|
1128
|
+
*
|
1129
|
+
* @return {Object|Undefined}
|
1130
|
+
*/
|
1131
|
+
validate: function() {
|
1132
|
+
var $el = this.$el,
|
1133
|
+
error = null,
|
1134
|
+
value = this.getValue(),
|
1135
|
+
formValues = this.form ? this.form.getValue() : {},
|
1136
|
+
validators = this.validators,
|
1137
|
+
getValidator = this.getValidator;
|
1138
|
+
|
1139
|
+
if (validators) {
|
1140
|
+
//Run through validators until an error is found
|
1141
|
+
_.every(validators, function(validator) {
|
1142
|
+
error = getValidator(validator)(value, formValues);
|
1143
|
+
|
1144
|
+
return error ? false : true;
|
1145
|
+
});
|
1146
|
+
}
|
1278
1147
|
|
1279
|
-
|
1280
|
-
|
1148
|
+
return error;
|
1149
|
+
},
|
1281
1150
|
|
1282
|
-
|
1283
|
-
|
1151
|
+
/**
|
1152
|
+
* Set this.hasFocus, or call parent trigger()
|
1153
|
+
*
|
1154
|
+
* @param {String} event
|
1155
|
+
*/
|
1156
|
+
trigger: function(event) {
|
1157
|
+
if (event === 'focus') {
|
1158
|
+
this.hasFocus = true;
|
1159
|
+
}
|
1160
|
+
else if (event === 'blur') {
|
1161
|
+
this.hasFocus = false;
|
1162
|
+
}
|
1284
1163
|
|
1285
|
-
|
1286
|
-
|
1287
|
-
if (_.isNumber(value)) return value;
|
1164
|
+
return Backbone.View.prototype.trigger.apply(this, arguments);
|
1165
|
+
},
|
1288
1166
|
|
1289
|
-
|
1167
|
+
/**
|
1168
|
+
* Returns a validation function based on the type defined in the schema
|
1169
|
+
*
|
1170
|
+
* @param {RegExp|String|Function} validator
|
1171
|
+
* @return {Function}
|
1172
|
+
*/
|
1173
|
+
getValidator: function(validator) {
|
1174
|
+
var validators = Form.validators;
|
1290
1175
|
|
1291
|
-
|
1292
|
-
|
1176
|
+
//Convert regular expressions to validators
|
1177
|
+
if (_.isRegExp(validator)) {
|
1178
|
+
return validators.regexp({ regexp: validator });
|
1179
|
+
}
|
1180
|
+
|
1181
|
+
//Use a built-in validator if given a string
|
1182
|
+
if (_.isString(validator)) {
|
1183
|
+
if (!validators[validator]) throw new Error('Validator "'+validator+'" not found');
|
1184
|
+
|
1185
|
+
return validators[validator]();
|
1186
|
+
}
|
1293
1187
|
|
1294
|
-
|
1188
|
+
//Functions can be used directly
|
1189
|
+
if (_.isFunction(validator)) return validator;
|
1295
1190
|
|
1296
|
-
|
1191
|
+
//Use a customised built-in validator if given an object
|
1192
|
+
if (_.isObject(validator) && validator.type) {
|
1193
|
+
var config = validator;
|
1194
|
+
|
1195
|
+
return validators[config.type](config);
|
1297
1196
|
}
|
1197
|
+
|
1198
|
+
//Unkown validator type
|
1199
|
+
throw new Error('Invalid validator: ' + validator);
|
1200
|
+
}
|
1201
|
+
});
|
1298
1202
|
|
1299
|
-
|
1203
|
+
/**
|
1204
|
+
* Text
|
1205
|
+
*
|
1206
|
+
* Text input with focus, blur and change events
|
1207
|
+
*/
|
1208
|
+
Form.editors.Text = Form.Editor.extend({
|
1300
1209
|
|
1210
|
+
tagName: 'input',
|
1301
1211
|
|
1302
|
-
|
1303
|
-
editors.Password = editors.Text.extend({
|
1212
|
+
defaultValue: '',
|
1304
1213
|
|
1305
|
-
|
1306
|
-
editors.Text.prototype.initialize.call(this, options);
|
1214
|
+
previousValue: '',
|
1307
1215
|
|
1308
|
-
|
1216
|
+
events: {
|
1217
|
+
'keyup': 'determineChange',
|
1218
|
+
'keypress': function(event) {
|
1219
|
+
var self = this;
|
1220
|
+
setTimeout(function() {
|
1221
|
+
self.determineChange();
|
1222
|
+
}, 0);
|
1223
|
+
},
|
1224
|
+
'select': function(event) {
|
1225
|
+
this.trigger('select', this);
|
1226
|
+
},
|
1227
|
+
'focus': function(event) {
|
1228
|
+
this.trigger('focus', this);
|
1229
|
+
},
|
1230
|
+
'blur': function(event) {
|
1231
|
+
this.trigger('blur', this);
|
1309
1232
|
}
|
1233
|
+
},
|
1310
1234
|
|
1311
|
-
|
1235
|
+
initialize: function(options) {
|
1236
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1312
1237
|
|
1238
|
+
var schema = this.schema;
|
1313
1239
|
|
1314
|
-
|
1315
|
-
|
1240
|
+
//Allow customising text type (email, phone etc.) for HTML5 browsers
|
1241
|
+
var type = 'text';
|
1316
1242
|
|
1317
|
-
|
1243
|
+
if (schema && schema.editorAttrs && schema.editorAttrs.type) type = schema.editorAttrs.type;
|
1244
|
+
if (schema && schema.dataType) type = schema.dataType;
|
1318
1245
|
|
1319
|
-
|
1246
|
+
this.$el.attr('type', type);
|
1247
|
+
},
|
1320
1248
|
|
1249
|
+
/**
|
1250
|
+
* Adds the editor to the DOM
|
1251
|
+
*/
|
1252
|
+
render: function() {
|
1253
|
+
this.setValue(this.value);
|
1321
1254
|
|
1322
|
-
|
1323
|
-
|
1255
|
+
return this;
|
1256
|
+
},
|
1324
1257
|
|
1325
|
-
|
1258
|
+
determineChange: function(event) {
|
1259
|
+
var currentValue = this.$el.val();
|
1260
|
+
var changed = (currentValue !== this.previousValue);
|
1326
1261
|
|
1327
|
-
|
1262
|
+
if (changed) {
|
1263
|
+
this.previousValue = currentValue;
|
1328
1264
|
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
},
|
1333
|
-
'focus': function(event) {
|
1334
|
-
this.trigger('focus', this);
|
1335
|
-
},
|
1336
|
-
'blur': function(event) {
|
1337
|
-
this.trigger('blur', this);
|
1338
|
-
}
|
1339
|
-
},
|
1265
|
+
this.trigger('change', this);
|
1266
|
+
}
|
1267
|
+
},
|
1340
1268
|
|
1341
|
-
|
1342
|
-
|
1269
|
+
/**
|
1270
|
+
* Returns the current editor value
|
1271
|
+
* @return {String}
|
1272
|
+
*/
|
1273
|
+
getValue: function() {
|
1274
|
+
return this.$el.val();
|
1275
|
+
},
|
1343
1276
|
|
1344
|
-
|
1345
|
-
|
1277
|
+
/**
|
1278
|
+
* Sets the value of the form element
|
1279
|
+
* @param {String}
|
1280
|
+
*/
|
1281
|
+
setValue: function(value) {
|
1282
|
+
this.$el.val(value);
|
1283
|
+
},
|
1346
1284
|
|
1347
|
-
|
1348
|
-
|
1349
|
-
*/
|
1350
|
-
render: function() {
|
1351
|
-
this.setValue(this.value);
|
1285
|
+
focus: function() {
|
1286
|
+
if (this.hasFocus) return;
|
1352
1287
|
|
1353
|
-
|
1354
|
-
|
1288
|
+
this.$el.focus();
|
1289
|
+
},
|
1355
1290
|
|
1356
|
-
|
1357
|
-
|
1358
|
-
},
|
1291
|
+
blur: function() {
|
1292
|
+
if (!this.hasFocus) return;
|
1359
1293
|
|
1360
|
-
|
1361
|
-
|
1362
|
-
this.$el.prop('checked', true);
|
1363
|
-
}
|
1364
|
-
},
|
1294
|
+
this.$el.blur();
|
1295
|
+
},
|
1365
1296
|
|
1366
|
-
|
1367
|
-
|
1297
|
+
select: function() {
|
1298
|
+
this.$el.select();
|
1299
|
+
}
|
1368
1300
|
|
1369
|
-
|
1370
|
-
},
|
1301
|
+
});
|
1371
1302
|
|
1372
|
-
|
1373
|
-
|
1303
|
+
/**
|
1304
|
+
* TextArea editor
|
1305
|
+
*/
|
1306
|
+
Form.editors.TextArea = Form.editors.Text.extend({
|
1374
1307
|
|
1375
|
-
|
1376
|
-
|
1308
|
+
tagName: 'textarea',
|
1309
|
+
|
1310
|
+
/**
|
1311
|
+
* Override Text constructor so type property isn't set (issue #261)
|
1312
|
+
*/
|
1313
|
+
initialize: function(options) {
|
1314
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1315
|
+
}
|
1377
1316
|
|
1378
|
-
|
1317
|
+
});
|
1379
1318
|
|
1319
|
+
/**
|
1320
|
+
* Password editor
|
1321
|
+
*/
|
1322
|
+
Form.editors.Password = Form.editors.Text.extend({
|
1380
1323
|
|
1381
|
-
|
1382
|
-
|
1324
|
+
initialize: function(options) {
|
1325
|
+
Form.editors.Text.prototype.initialize.call(this, options);
|
1383
1326
|
|
1384
|
-
|
1327
|
+
this.$el.attr('type', 'password');
|
1328
|
+
}
|
1385
1329
|
|
1386
|
-
|
1387
|
-
editors.Text.prototype.initialize.call(this, options);
|
1330
|
+
});
|
1388
1331
|
|
1389
|
-
|
1390
|
-
|
1332
|
+
/**
|
1333
|
+
* NUMBER
|
1334
|
+
*
|
1335
|
+
* Normal text input that only allows a number. Letters etc. are not entered.
|
1336
|
+
*/
|
1337
|
+
Form.editors.Number = Form.editors.Text.extend({
|
1391
1338
|
|
1392
|
-
|
1393
|
-
return this.value;
|
1394
|
-
},
|
1339
|
+
defaultValue: 0,
|
1395
1340
|
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1341
|
+
events: _.extend({}, Form.editors.Text.prototype.events, {
|
1342
|
+
'keypress': 'onKeyPress',
|
1343
|
+
'change': 'onKeyPress'
|
1344
|
+
}),
|
1399
1345
|
|
1400
|
-
|
1346
|
+
initialize: function(options) {
|
1347
|
+
Form.editors.Text.prototype.initialize.call(this, options);
|
1401
1348
|
|
1402
|
-
|
1349
|
+
this.$el.attr('type', 'number');
|
1350
|
+
this.$el.attr('step', 'any');
|
1351
|
+
},
|
1403
1352
|
|
1404
|
-
|
1353
|
+
/**
|
1354
|
+
* Check value is numeric
|
1355
|
+
*/
|
1356
|
+
onKeyPress: function(event) {
|
1357
|
+
var self = this,
|
1358
|
+
delayedDetermineChange = function() {
|
1359
|
+
setTimeout(function() {
|
1360
|
+
self.determineChange();
|
1361
|
+
}, 0);
|
1362
|
+
};
|
1405
1363
|
|
1364
|
+
//Allow backspace
|
1365
|
+
if (event.charCode === 0) {
|
1366
|
+
delayedDetermineChange();
|
1367
|
+
return;
|
1406
1368
|
}
|
1407
1369
|
|
1408
|
-
|
1370
|
+
//Get the whole new value so that we can prevent things like double decimals points etc.
|
1371
|
+
var newVal = this.$el.val()
|
1372
|
+
if( event.charCode != undefined ) {
|
1373
|
+
newVal = newVal + String.fromCharCode(event.charCode);
|
1374
|
+
}
|
1409
1375
|
|
1376
|
+
var numeric = /^[0-9]*\.?[0-9]*?$/.test(newVal);
|
1410
1377
|
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
* or a Backbone collection. If a collection, the models must implement a toString() method
|
1419
|
-
*/
|
1420
|
-
editors.Select = editors.Base.extend({
|
1378
|
+
if (numeric) {
|
1379
|
+
delayedDetermineChange();
|
1380
|
+
}
|
1381
|
+
else {
|
1382
|
+
event.preventDefault();
|
1383
|
+
}
|
1384
|
+
},
|
1421
1385
|
|
1422
|
-
|
1386
|
+
getValue: function() {
|
1387
|
+
var value = this.$el.val();
|
1423
1388
|
|
1424
|
-
|
1425
|
-
|
1426
|
-
this.trigger('change', this);
|
1427
|
-
},
|
1428
|
-
'focus': function(event) {
|
1429
|
-
this.trigger('focus', this);
|
1430
|
-
},
|
1431
|
-
'blur': function(event) {
|
1432
|
-
this.trigger('blur', this);
|
1433
|
-
}
|
1434
|
-
},
|
1389
|
+
return value === "" ? null : parseFloat(value, 10);
|
1390
|
+
},
|
1435
1391
|
|
1436
|
-
|
1437
|
-
|
1392
|
+
setValue: function(value) {
|
1393
|
+
value = (function() {
|
1394
|
+
if (_.isNumber(value)) return value;
|
1438
1395
|
|
1439
|
-
if (
|
1440
|
-
},
|
1396
|
+
if (_.isString(value) && value !== '') return parseFloat(value, 10);
|
1441
1397
|
|
1442
|
-
|
1443
|
-
|
1398
|
+
return null;
|
1399
|
+
})();
|
1444
1400
|
|
1445
|
-
|
1446
|
-
},
|
1401
|
+
if (_.isNaN(value)) value = null;
|
1447
1402
|
|
1448
|
-
|
1449
|
-
|
1450
|
-
*
|
1451
|
-
* @param {Mixed} options
|
1452
|
-
*/
|
1453
|
-
setOptions: function(options) {
|
1454
|
-
var self = this;
|
1403
|
+
Form.editors.Text.prototype.setValue.call(this, value);
|
1404
|
+
}
|
1455
1405
|
|
1456
|
-
|
1457
|
-
if (options instanceof Backbone.Collection) {
|
1458
|
-
var collection = options;
|
1406
|
+
});
|
1459
1407
|
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
collection.fetch({
|
1465
|
-
success: function(collection) {
|
1466
|
-
self.renderOptions(options);
|
1467
|
-
}
|
1468
|
-
});
|
1469
|
-
}
|
1470
|
-
}
|
1408
|
+
/**
|
1409
|
+
* Hidden editor
|
1410
|
+
*/
|
1411
|
+
Form.editors.Hidden = Form.editors.Text.extend({
|
1471
1412
|
|
1472
|
-
|
1473
|
-
else if (_.isFunction(options)) {
|
1474
|
-
options(function(result) {
|
1475
|
-
self.renderOptions(result);
|
1476
|
-
}, self);
|
1477
|
-
}
|
1413
|
+
defaultValue: '',
|
1478
1414
|
|
1479
|
-
|
1480
|
-
|
1481
|
-
this.renderOptions(options);
|
1482
|
-
}
|
1483
|
-
},
|
1415
|
+
initialize: function(options) {
|
1416
|
+
Form.editors.Text.prototype.initialize.call(this, options);
|
1484
1417
|
|
1485
|
-
|
1486
|
-
|
1487
|
-
* @param {Mixed} Options as a simple array e.g. ['option1', 'option2']
|
1488
|
-
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
|
1489
|
-
* or as a string of <option> HTML to insert into the <select>
|
1490
|
-
*/
|
1491
|
-
renderOptions: function(options) {
|
1492
|
-
var $select = this.$el,
|
1493
|
-
html;
|
1418
|
+
this.$el.attr('type', 'hidden');
|
1419
|
+
},
|
1494
1420
|
|
1495
|
-
|
1421
|
+
focus: function() {
|
1496
1422
|
|
1497
|
-
|
1498
|
-
$select.html(html);
|
1423
|
+
},
|
1499
1424
|
|
1500
|
-
|
1501
|
-
this.setValue(this.value);
|
1502
|
-
},
|
1425
|
+
blur: function() {
|
1503
1426
|
|
1504
|
-
|
1505
|
-
var html;
|
1506
|
-
//Accept string of HTML
|
1507
|
-
if (_.isString(options)) {
|
1508
|
-
html = options;
|
1509
|
-
}
|
1427
|
+
}
|
1510
1428
|
|
1511
|
-
|
1512
|
-
else if (_.isArray(options)) {
|
1513
|
-
html = this._arrayToHtml(options);
|
1514
|
-
}
|
1429
|
+
});
|
1515
1430
|
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1431
|
+
/**
|
1432
|
+
* Checkbox editor
|
1433
|
+
*
|
1434
|
+
* Creates a single checkbox, i.e. boolean value
|
1435
|
+
*/
|
1436
|
+
Form.editors.Checkbox = Form.editors.Base.extend({
|
1520
1437
|
|
1521
|
-
|
1522
|
-
var newOptions;
|
1523
|
-
|
1524
|
-
options(function(opts) {
|
1525
|
-
newOptions = opts;
|
1526
|
-
}, this);
|
1527
|
-
|
1528
|
-
html = this._getOptionsHtml(newOptions);
|
1529
|
-
}
|
1438
|
+
defaultValue: false,
|
1530
1439
|
|
1531
|
-
|
1532
|
-
},
|
1440
|
+
tagName: 'input',
|
1533
1441
|
|
1534
|
-
|
1535
|
-
|
1442
|
+
events: {
|
1443
|
+
'click': function(event) {
|
1444
|
+
this.trigger('change', this);
|
1536
1445
|
},
|
1537
|
-
|
1538
|
-
|
1539
|
-
this.$el.val(value);
|
1446
|
+
'focus': function(event) {
|
1447
|
+
this.trigger('focus', this);
|
1540
1448
|
},
|
1449
|
+
'blur': function(event) {
|
1450
|
+
this.trigger('blur', this);
|
1451
|
+
}
|
1452
|
+
},
|
1541
1453
|
|
1542
|
-
|
1543
|
-
|
1454
|
+
initialize: function(options) {
|
1455
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1544
1456
|
|
1545
|
-
|
1546
|
-
|
1457
|
+
this.$el.attr('type', 'checkbox');
|
1458
|
+
},
|
1547
1459
|
|
1548
|
-
|
1549
|
-
|
1460
|
+
/**
|
1461
|
+
* Adds the editor to the DOM
|
1462
|
+
*/
|
1463
|
+
render: function() {
|
1464
|
+
this.setValue(this.value);
|
1550
1465
|
|
1551
|
-
|
1552
|
-
|
1466
|
+
return this;
|
1467
|
+
},
|
1553
1468
|
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
* @return {String}
|
1558
|
-
*/
|
1559
|
-
_collectionToHtml: function(collection) {
|
1560
|
-
//Convert collection to array first
|
1561
|
-
var array = [];
|
1562
|
-
collection.each(function(model) {
|
1563
|
-
array.push({ val: model.id, label: model.toString() });
|
1564
|
-
});
|
1469
|
+
getValue: function() {
|
1470
|
+
return this.$el.prop('checked');
|
1471
|
+
},
|
1565
1472
|
|
1566
|
-
|
1567
|
-
|
1473
|
+
setValue: function(value) {
|
1474
|
+
if (value) {
|
1475
|
+
this.$el.prop('checked', true);
|
1476
|
+
}else{
|
1477
|
+
this.$el.prop('checked', false);
|
1478
|
+
}
|
1479
|
+
},
|
1568
1480
|
|
1569
|
-
|
1570
|
-
|
1481
|
+
focus: function() {
|
1482
|
+
if (this.hasFocus) return;
|
1571
1483
|
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
else {
|
1594
|
-
html.push('<option>'+option+'</option>');
|
1595
|
-
}
|
1596
|
-
}, this);
|
1484
|
+
this.$el.focus();
|
1485
|
+
},
|
1486
|
+
|
1487
|
+
blur: function() {
|
1488
|
+
if (!this.hasFocus) return;
|
1489
|
+
|
1490
|
+
this.$el.blur();
|
1491
|
+
}
|
1492
|
+
|
1493
|
+
});
|
1494
|
+
|
1495
|
+
/**
|
1496
|
+
* Select editor
|
1497
|
+
*
|
1498
|
+
* Renders a <select> with given options
|
1499
|
+
*
|
1500
|
+
* Requires an 'options' value on the schema.
|
1501
|
+
* Can be an array of options, a function that calls back with the array of options, a string of HTML
|
1502
|
+
* or a Backbone collection. If a collection, the models must implement a toString() method
|
1503
|
+
*/
|
1504
|
+
Form.editors.Select = Form.editors.Base.extend({
|
1597
1505
|
|
1598
|
-
|
1506
|
+
tagName: 'select',
|
1507
|
+
|
1508
|
+
events: {
|
1509
|
+
'change': function(event) {
|
1510
|
+
this.trigger('change', this);
|
1511
|
+
},
|
1512
|
+
'focus': function(event) {
|
1513
|
+
this.trigger('focus', this);
|
1514
|
+
},
|
1515
|
+
'blur': function(event) {
|
1516
|
+
this.trigger('blur', this);
|
1599
1517
|
}
|
1518
|
+
},
|
1519
|
+
|
1520
|
+
initialize: function(options) {
|
1521
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1600
1522
|
|
1601
|
-
|
1523
|
+
if (!this.schema || !this.schema.options) throw "Missing required 'schema.options'";
|
1524
|
+
},
|
1602
1525
|
|
1526
|
+
render: function() {
|
1527
|
+
this.setOptions(this.schema.options);
|
1603
1528
|
|
1529
|
+
return this;
|
1530
|
+
},
|
1604
1531
|
|
1605
1532
|
/**
|
1606
|
-
*
|
1533
|
+
* Sets the options that populate the <select>
|
1607
1534
|
*
|
1608
|
-
*
|
1609
|
-
*
|
1610
|
-
* Requires an 'options' value on the schema.
|
1611
|
-
* Can be an array of options, a function that calls back with the array of options, a string of HTML
|
1612
|
-
* or a Backbone collection. If a collection, the models must implement a toString() method
|
1535
|
+
* @param {Mixed} options
|
1613
1536
|
*/
|
1614
|
-
|
1537
|
+
setOptions: function(options) {
|
1538
|
+
var self = this;
|
1615
1539
|
|
1616
|
-
|
1617
|
-
|
1540
|
+
//If a collection was passed, check if it needs fetching
|
1541
|
+
if (options instanceof Backbone.Collection) {
|
1542
|
+
var collection = options;
|
1618
1543
|
|
1619
|
-
|
1620
|
-
|
1621
|
-
this.
|
1622
|
-
}
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
1627
|
-
|
1628
|
-
if (!this.hasFocus) return;
|
1629
|
-
var self = this;
|
1630
|
-
setTimeout(function() {
|
1631
|
-
if (self.$('input[type=radio]:focus')[0]) return;
|
1632
|
-
self.trigger('blur', self);
|
1633
|
-
}, 0);
|
1544
|
+
//Don't do the fetch if it's already populated
|
1545
|
+
if (collection.length > 0) {
|
1546
|
+
this.renderOptions(options);
|
1547
|
+
} else {
|
1548
|
+
collection.fetch({
|
1549
|
+
success: function(collection) {
|
1550
|
+
self.renderOptions(options);
|
1551
|
+
}
|
1552
|
+
});
|
1634
1553
|
}
|
1635
|
-
}
|
1554
|
+
}
|
1636
1555
|
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1556
|
+
//If a function was passed, run it to get the options
|
1557
|
+
else if (_.isFunction(options)) {
|
1558
|
+
options(function(result) {
|
1559
|
+
self.renderOptions(result);
|
1560
|
+
}, self);
|
1561
|
+
}
|
1640
1562
|
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1563
|
+
//Otherwise, ready to go straight to renderOptions
|
1564
|
+
else {
|
1565
|
+
this.renderOptions(options);
|
1566
|
+
}
|
1567
|
+
},
|
1644
1568
|
|
1645
|
-
|
1646
|
-
|
1569
|
+
/**
|
1570
|
+
* Adds the <option> html to the DOM
|
1571
|
+
* @param {Mixed} Options as a simple array e.g. ['option1', 'option2']
|
1572
|
+
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
|
1573
|
+
* or as a string of <option> HTML to insert into the <select>
|
1574
|
+
*/
|
1575
|
+
renderOptions: function(options) {
|
1576
|
+
var $select = this.$el,
|
1577
|
+
html;
|
1647
1578
|
|
1648
|
-
|
1649
|
-
if (checked[0]) {
|
1650
|
-
checked.focus();
|
1651
|
-
return;
|
1652
|
-
}
|
1579
|
+
html = this._getOptionsHtml(options);
|
1653
1580
|
|
1654
|
-
|
1655
|
-
|
1581
|
+
//Insert options
|
1582
|
+
$select.html(html);
|
1656
1583
|
|
1657
|
-
|
1658
|
-
|
1584
|
+
//Select correct option
|
1585
|
+
this.setValue(this.value);
|
1586
|
+
},
|
1659
1587
|
|
1660
|
-
|
1661
|
-
|
1588
|
+
_getOptionsHtml: function(options) {
|
1589
|
+
var html;
|
1590
|
+
//Accept string of HTML
|
1591
|
+
if (_.isString(options)) {
|
1592
|
+
html = options;
|
1593
|
+
}
|
1662
1594
|
|
1663
|
-
|
1664
|
-
|
1665
|
-
|
1666
|
-
|
1667
|
-
* @return {String} HTML
|
1668
|
-
*/
|
1669
|
-
_arrayToHtml: function (array) {
|
1670
|
-
var html = [];
|
1671
|
-
var self = this;
|
1595
|
+
//Or array
|
1596
|
+
else if (_.isArray(options)) {
|
1597
|
+
html = this._arrayToHtml(options);
|
1598
|
+
}
|
1672
1599
|
|
1673
|
-
|
1674
|
-
|
1675
|
-
|
1676
|
-
|
1677
|
-
itemHtml += ('<input type="radio" name="'+self.id+'" value="'+val+'" id="'+self.id+'-'+index+'" />');
|
1678
|
-
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>');
|
1679
|
-
}
|
1680
|
-
else {
|
1681
|
-
itemHtml += ('<input type="radio" name="'+self.id+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
|
1682
|
-
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
|
1683
|
-
}
|
1684
|
-
itemHtml += '</li>';
|
1685
|
-
html.push(itemHtml);
|
1686
|
-
});
|
1600
|
+
//Or Backbone collection
|
1601
|
+
else if (options instanceof Backbone.Collection) {
|
1602
|
+
html = this._collectionToHtml(options);
|
1603
|
+
}
|
1687
1604
|
|
1688
|
-
|
1605
|
+
else if (_.isFunction(options)) {
|
1606
|
+
var newOptions;
|
1607
|
+
|
1608
|
+
options(function(opts) {
|
1609
|
+
newOptions = opts;
|
1610
|
+
}, this);
|
1611
|
+
|
1612
|
+
html = this._getOptionsHtml(newOptions);
|
1689
1613
|
}
|
1690
1614
|
|
1691
|
-
|
1615
|
+
return html;
|
1616
|
+
},
|
1617
|
+
|
1618
|
+
getValue: function() {
|
1619
|
+
return this.$el.val();
|
1620
|
+
},
|
1621
|
+
|
1622
|
+
setValue: function(value) {
|
1623
|
+
this.$el.val(value);
|
1624
|
+
},
|
1625
|
+
|
1626
|
+
focus: function() {
|
1627
|
+
if (this.hasFocus) return;
|
1628
|
+
|
1629
|
+
this.$el.focus();
|
1630
|
+
},
|
1692
1631
|
|
1632
|
+
blur: function() {
|
1633
|
+
if (!this.hasFocus) return;
|
1693
1634
|
|
1635
|
+
this.$el.blur();
|
1636
|
+
},
|
1694
1637
|
|
1695
1638
|
/**
|
1696
|
-
*
|
1697
|
-
*
|
1698
|
-
*
|
1699
|
-
* Requires an 'options' value on the schema.
|
1700
|
-
* Can be an array of options, a function that calls back with the array of options, a string of HTML
|
1701
|
-
* or a Backbone collection. If a collection, the models must implement a toString() method
|
1639
|
+
* Transforms a collection into HTML ready to use in the renderOptions method
|
1640
|
+
* @param {Backbone.Collection}
|
1641
|
+
* @return {String}
|
1702
1642
|
*/
|
1703
|
-
|
1643
|
+
_collectionToHtml: function(collection) {
|
1644
|
+
//Convert collection to array first
|
1645
|
+
var array = [];
|
1646
|
+
collection.each(function(model) {
|
1647
|
+
array.push({ val: model.id, label: model.toString() });
|
1648
|
+
});
|
1704
1649
|
|
1705
|
-
|
1706
|
-
|
1650
|
+
//Now convert to HTML
|
1651
|
+
var html = this._arrayToHtml(array);
|
1707
1652
|
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
|
1653
|
+
return html;
|
1654
|
+
},
|
1655
|
+
|
1656
|
+
/**
|
1657
|
+
* Create the <option> HTML
|
1658
|
+
* @param {Array} Options as a simple array e.g. ['option1', 'option2']
|
1659
|
+
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
|
1660
|
+
* @return {String} HTML
|
1661
|
+
*/
|
1662
|
+
_arrayToHtml: function(array) {
|
1663
|
+
var html = [];
|
1664
|
+
|
1665
|
+
//Generate HTML
|
1666
|
+
_.each(array, function(option) {
|
1667
|
+
if (_.isObject(option)) {
|
1668
|
+
if (option.group) {
|
1669
|
+
html.push('<optgroup label="'+option.group+'">');
|
1670
|
+
html.push(this._getOptionsHtml(option.options))
|
1671
|
+
html.push('</optgroup>');
|
1672
|
+
} else {
|
1673
|
+
var val = (option.val || option.val === 0) ? option.val : '';
|
1674
|
+
html.push('<option value="'+val+'">'+option.label+'</option>');
|
1675
|
+
}
|
1723
1676
|
}
|
1724
|
-
|
1677
|
+
else {
|
1678
|
+
html.push('<option>'+option+'</option>');
|
1679
|
+
}
|
1680
|
+
}, this);
|
1725
1681
|
|
1726
|
-
|
1727
|
-
|
1728
|
-
this.$('input[type=checkbox]:checked').each(function() {
|
1729
|
-
values.push($(this).val());
|
1730
|
-
});
|
1731
|
-
return values;
|
1732
|
-
},
|
1682
|
+
return html.join('');
|
1683
|
+
}
|
1733
1684
|
|
1734
|
-
|
1735
|
-
if (!_.isArray(values)) values = [values];
|
1736
|
-
this.$('input[type=checkbox]').val(values);
|
1737
|
-
},
|
1685
|
+
});
|
1738
1686
|
|
1739
|
-
|
1740
|
-
|
1687
|
+
/**
|
1688
|
+
* Radio editor
|
1689
|
+
*
|
1690
|
+
* Renders a <ul> with given options represented as <li> objects containing radio buttons
|
1691
|
+
*
|
1692
|
+
* Requires an 'options' value on the schema.
|
1693
|
+
* Can be an array of options, a function that calls back with the array of options, a string of HTML
|
1694
|
+
* or a Backbone collection. If a collection, the models must implement a toString() method
|
1695
|
+
*/
|
1696
|
+
Form.editors.Radio = Form.editors.Select.extend({
|
1741
1697
|
|
1742
|
-
|
1743
|
-
},
|
1698
|
+
tagName: 'ul',
|
1744
1699
|
|
1745
|
-
|
1700
|
+
events: {
|
1701
|
+
'change input[type=radio]': function() {
|
1702
|
+
this.trigger('change', this);
|
1703
|
+
},
|
1704
|
+
'focus input[type=radio]': function() {
|
1705
|
+
if (this.hasFocus) return;
|
1706
|
+
this.trigger('focus', this);
|
1707
|
+
},
|
1708
|
+
'blur input[type=radio]': function() {
|
1746
1709
|
if (!this.hasFocus) return;
|
1710
|
+
var self = this;
|
1711
|
+
setTimeout(function() {
|
1712
|
+
if (self.$('input[type=radio]:focus')[0]) return;
|
1713
|
+
self.trigger('blur', self);
|
1714
|
+
}, 0);
|
1715
|
+
}
|
1716
|
+
},
|
1747
1717
|
|
1748
|
-
|
1749
|
-
|
1718
|
+
getValue: function() {
|
1719
|
+
return this.$('input[type=radio]:checked').val();
|
1720
|
+
},
|
1750
1721
|
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
1754
|
-
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
|
1755
|
-
* @return {String} HTML
|
1756
|
-
*/
|
1757
|
-
_arrayToHtml: function (array) {
|
1758
|
-
var html = [];
|
1759
|
-
var self = this;
|
1722
|
+
setValue: function(value) {
|
1723
|
+
this.$('input[type=radio]').val([value]);
|
1724
|
+
},
|
1760
1725
|
|
1761
|
-
|
1762
|
-
|
1763
|
-
if (_.isObject(option)) {
|
1764
|
-
var val = (option.val || option.val === 0) ? option.val : '';
|
1765
|
-
itemHtml += ('<input type="checkbox" name="'+self.id+'" value="'+val+'" id="'+self.id+'-'+index+'" />');
|
1766
|
-
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>');
|
1767
|
-
}
|
1768
|
-
else {
|
1769
|
-
itemHtml += ('<input type="checkbox" name="'+self.id+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
|
1770
|
-
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
|
1771
|
-
}
|
1772
|
-
itemHtml += '</li>';
|
1773
|
-
html.push(itemHtml);
|
1774
|
-
});
|
1726
|
+
focus: function() {
|
1727
|
+
if (this.hasFocus) return;
|
1775
1728
|
|
1776
|
-
|
1729
|
+
var checked = this.$('input[type=radio]:checked');
|
1730
|
+
if (checked[0]) {
|
1731
|
+
checked.focus();
|
1732
|
+
return;
|
1777
1733
|
}
|
1778
1734
|
|
1779
|
-
|
1735
|
+
this.$('input[type=radio]').first().focus();
|
1736
|
+
},
|
1780
1737
|
|
1738
|
+
blur: function() {
|
1739
|
+
if (!this.hasFocus) return;
|
1781
1740
|
|
1741
|
+
this.$('input[type=radio]:focus').blur();
|
1742
|
+
},
|
1782
1743
|
|
1783
1744
|
/**
|
1784
|
-
*
|
1785
|
-
*
|
1786
|
-
*
|
1787
|
-
*
|
1788
|
-
* @param {Object} options
|
1789
|
-
* @param {Object} options.schema The schema for the object
|
1790
|
-
* @param {Object} options.schema.subSchema The schema for the nested form
|
1745
|
+
* Create the radio list HTML
|
1746
|
+
* @param {Array} Options as a simple array e.g. ['option1', 'option2']
|
1747
|
+
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
|
1748
|
+
* @return {String} HTML
|
1791
1749
|
*/
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1750
|
+
_arrayToHtml: function (array) {
|
1751
|
+
var html = [];
|
1752
|
+
var self = this;
|
1753
|
+
|
1754
|
+
_.each(array, function(option, index) {
|
1755
|
+
var itemHtml = '<li>';
|
1756
|
+
if (_.isObject(option)) {
|
1757
|
+
var val = (option.val || option.val === 0) ? option.val : '';
|
1758
|
+
itemHtml += ('<input type="radio" name="'+self.getName()+'" value="'+val+'" id="'+self.id+'-'+index+'" />');
|
1759
|
+
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>');
|
1760
|
+
}
|
1761
|
+
else {
|
1762
|
+
itemHtml += ('<input type="radio" name="'+self.getName()+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
|
1763
|
+
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
|
1764
|
+
}
|
1765
|
+
itemHtml += '</li>';
|
1766
|
+
html.push(itemHtml);
|
1767
|
+
});
|
1768
|
+
|
1769
|
+
return html.join('');
|
1770
|
+
}
|
1795
1771
|
|
1796
|
-
|
1772
|
+
});
|
1797
1773
|
|
1798
|
-
|
1799
|
-
|
1800
|
-
|
1774
|
+
/**
|
1775
|
+
* Checkboxes editor
|
1776
|
+
*
|
1777
|
+
* Renders a <ul> with given options represented as <li> objects containing checkboxes
|
1778
|
+
*
|
1779
|
+
* Requires an 'options' value on the schema.
|
1780
|
+
* Can be an array of options, a function that calls back with the array of options, a string of HTML
|
1781
|
+
* or a Backbone collection. If a collection, the models must implement a toString() method
|
1782
|
+
*/
|
1783
|
+
Form.editors.Checkboxes = Form.editors.Select.extend({
|
1801
1784
|
|
1802
|
-
|
1803
|
-
editors.Base.prototype.initialize.call(this, options);
|
1785
|
+
tagName: 'ul',
|
1804
1786
|
|
1805
|
-
|
1806
|
-
|
1787
|
+
events: {
|
1788
|
+
'click input[type=checkbox]': function() {
|
1789
|
+
this.trigger('change', this);
|
1790
|
+
},
|
1791
|
+
'focus input[type=checkbox]': function() {
|
1792
|
+
if (this.hasFocus) return;
|
1793
|
+
this.trigger('focus', this);
|
1807
1794
|
},
|
1795
|
+
'blur input[type=checkbox]': function() {
|
1796
|
+
if (!this.hasFocus) return;
|
1797
|
+
var self = this;
|
1798
|
+
setTimeout(function() {
|
1799
|
+
if (self.$('input[type=checkbox]:focus')[0]) return;
|
1800
|
+
self.trigger('blur', self);
|
1801
|
+
}, 0);
|
1802
|
+
}
|
1803
|
+
},
|
1808
1804
|
|
1809
|
-
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
1815
|
-
|
1816
|
-
});
|
1805
|
+
getValue: function() {
|
1806
|
+
var values = [];
|
1807
|
+
this.$('input[type=checkbox]:checked').each(function() {
|
1808
|
+
values.push($(this).val());
|
1809
|
+
});
|
1810
|
+
return values;
|
1811
|
+
},
|
1817
1812
|
|
1818
|
-
|
1813
|
+
setValue: function(values) {
|
1814
|
+
if (!_.isArray(values)) values = [values];
|
1815
|
+
this.$('input[type=checkbox]').val(values);
|
1816
|
+
},
|
1819
1817
|
|
1820
|
-
|
1818
|
+
focus: function() {
|
1819
|
+
if (this.hasFocus) return;
|
1821
1820
|
|
1822
|
-
|
1821
|
+
this.$('input[type=checkbox]').first().focus();
|
1822
|
+
},
|
1823
1823
|
|
1824
|
-
|
1825
|
-
|
1824
|
+
blur: function() {
|
1825
|
+
if (!this.hasFocus) return;
|
1826
1826
|
|
1827
|
-
|
1828
|
-
|
1827
|
+
this.$('input[type=checkbox]:focus').blur();
|
1828
|
+
},
|
1829
1829
|
|
1830
|
-
|
1831
|
-
|
1830
|
+
/**
|
1831
|
+
* Create the checkbox list HTML
|
1832
|
+
* @param {Array} Options as a simple array e.g. ['option1', 'option2']
|
1833
|
+
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
|
1834
|
+
* @return {String} HTML
|
1835
|
+
*/
|
1836
|
+
_arrayToHtml: function (array) {
|
1837
|
+
var html = [];
|
1838
|
+
var self = this;
|
1839
|
+
|
1840
|
+
_.each(array, function(option, index) {
|
1841
|
+
var itemHtml = '<li>';
|
1842
|
+
if (_.isObject(option)) {
|
1843
|
+
var val = (option.val || option.val === 0) ? option.val : '';
|
1844
|
+
itemHtml += ('<input type="checkbox" name="'+self.getName()+'" value="'+val+'" id="'+self.id+'-'+index+'" />');
|
1845
|
+
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>');
|
1846
|
+
}
|
1847
|
+
else {
|
1848
|
+
itemHtml += ('<input type="checkbox" name="'+self.getName()+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
|
1849
|
+
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
|
1850
|
+
}
|
1851
|
+
itemHtml += '</li>';
|
1852
|
+
html.push(itemHtml);
|
1853
|
+
});
|
1832
1854
|
|
1833
|
-
|
1834
|
-
|
1855
|
+
return html.join('');
|
1856
|
+
}
|
1835
1857
|
|
1836
|
-
|
1837
|
-
},
|
1858
|
+
});
|
1838
1859
|
|
1839
|
-
|
1840
|
-
|
1860
|
+
/**
|
1861
|
+
* Object editor
|
1862
|
+
*
|
1863
|
+
* Creates a child form. For editing Javascript objects
|
1864
|
+
*
|
1865
|
+
* @param {Object} options
|
1866
|
+
* @param {Form} options.form The form this editor belongs to; used to determine the constructor for the nested form
|
1867
|
+
* @param {Object} options.schema The schema for the object
|
1868
|
+
* @param {Object} options.schema.subSchema The schema for the nested form
|
1869
|
+
*/
|
1870
|
+
Form.editors.Object = Form.editors.Base.extend({
|
1871
|
+
//Prevent error classes being set on the main control; they are internally on the individual fields
|
1872
|
+
hasNestedForm: true,
|
1873
|
+
|
1874
|
+
initialize: function(options) {
|
1875
|
+
//Set default value for the instance so it's not a shared object
|
1876
|
+
this.value = {};
|
1877
|
+
|
1878
|
+
//Init
|
1879
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1880
|
+
|
1881
|
+
//Check required options
|
1882
|
+
if (!this.form) throw 'Missing required option "form"';
|
1883
|
+
if (!this.schema.subSchema) throw new Error("Missing required 'schema.subSchema' option for Object editor");
|
1884
|
+
},
|
1885
|
+
|
1886
|
+
render: function() {
|
1887
|
+
//Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
|
1888
|
+
var NestedForm = this.form.constructor;
|
1889
|
+
|
1890
|
+
//Create the nested form
|
1891
|
+
this.nestedForm = new NestedForm({
|
1892
|
+
schema: this.schema.subSchema,
|
1893
|
+
data: this.value,
|
1894
|
+
idPrefix: this.id + '_',
|
1895
|
+
Field: NestedForm.NestedField
|
1896
|
+
});
|
1841
1897
|
|
1842
|
-
|
1843
|
-
},
|
1898
|
+
this._observeFormEvents();
|
1844
1899
|
|
1845
|
-
|
1846
|
-
if (!this.hasFocus) return;
|
1900
|
+
this.$el.html(this.nestedForm.render().el);
|
1847
1901
|
|
1848
|
-
|
1849
|
-
},
|
1902
|
+
if (this.hasFocus) this.trigger('blur', this);
|
1850
1903
|
|
1851
|
-
|
1852
|
-
|
1904
|
+
return this;
|
1905
|
+
},
|
1853
1906
|
|
1854
|
-
|
1855
|
-
|
1907
|
+
getValue: function() {
|
1908
|
+
if (this.nestedForm) return this.nestedForm.getValue();
|
1856
1909
|
|
1857
|
-
|
1858
|
-
|
1859
|
-
},
|
1910
|
+
return this.value;
|
1911
|
+
},
|
1860
1912
|
|
1861
|
-
|
1862
|
-
|
1863
|
-
// args = ["key:change", form, fieldEditor]
|
1864
|
-
var args = _.toArray(arguments);
|
1865
|
-
args[1] = this;
|
1866
|
-
// args = ["key:change", this=objectEditor, fieldEditor]
|
1913
|
+
setValue: function(value) {
|
1914
|
+
this.value = value;
|
1867
1915
|
|
1868
|
-
|
1869
|
-
|
1870
|
-
}
|
1916
|
+
this.render();
|
1917
|
+
},
|
1871
1918
|
|
1872
|
-
|
1919
|
+
focus: function() {
|
1920
|
+
if (this.hasFocus) return;
|
1873
1921
|
|
1922
|
+
this.nestedForm.focus();
|
1923
|
+
},
|
1874
1924
|
|
1925
|
+
blur: function() {
|
1926
|
+
if (!this.hasFocus) return;
|
1875
1927
|
|
1876
|
-
|
1877
|
-
|
1878
|
-
*
|
1879
|
-
* Creates a child form. For editing nested Backbone models
|
1880
|
-
*
|
1881
|
-
* Special options:
|
1882
|
-
* schema.model: Embedded model constructor
|
1883
|
-
*/
|
1884
|
-
editors.NestedModel = editors.Object.extend({
|
1885
|
-
initialize: function(options) {
|
1886
|
-
editors.Base.prototype.initialize.call(this, options);
|
1928
|
+
this.nestedForm.blur();
|
1929
|
+
},
|
1887
1930
|
|
1888
|
-
|
1889
|
-
|
1890
|
-
},
|
1931
|
+
remove: function() {
|
1932
|
+
this.nestedForm.remove();
|
1891
1933
|
|
1892
|
-
|
1893
|
-
|
1894
|
-
key = this.key,
|
1895
|
-
nestedModel = this.schema.model;
|
1934
|
+
Backbone.View.prototype.remove.call(this);
|
1935
|
+
},
|
1896
1936
|
|
1897
|
-
|
1898
|
-
|
1937
|
+
validate: function() {
|
1938
|
+
return this.nestedForm.validate();
|
1939
|
+
},
|
1899
1940
|
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
|
1941
|
+
_observeFormEvents: function() {
|
1942
|
+
if (!this.nestedForm) return;
|
1943
|
+
|
1944
|
+
this.nestedForm.on('all', function() {
|
1945
|
+
// args = ["key:change", form, fieldEditor]
|
1946
|
+
var args = _.toArray(arguments);
|
1947
|
+
args[1] = this;
|
1948
|
+
// args = ["key:change", this=objectEditor, fieldEditor]
|
1949
|
+
|
1950
|
+
this.trigger.apply(this, args);
|
1951
|
+
}, this);
|
1952
|
+
}
|
1953
|
+
|
1954
|
+
});
|
1955
|
+
|
1956
|
+
/**
|
1957
|
+
* NestedModel editor
|
1958
|
+
*
|
1959
|
+
* Creates a child form. For editing nested Backbone models
|
1960
|
+
*
|
1961
|
+
* Special options:
|
1962
|
+
* schema.model: Embedded model constructor
|
1963
|
+
*/
|
1964
|
+
Form.editors.NestedModel = Form.editors.Object.extend({
|
1965
|
+
initialize: function(options) {
|
1966
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1905
1967
|
|
1906
|
-
|
1968
|
+
if (!this.form) throw 'Missing required option "form"';
|
1969
|
+
if (!options.schema.model) throw 'Missing required "schema.model" option for NestedModel editor';
|
1970
|
+
},
|
1907
1971
|
|
1908
|
-
|
1909
|
-
|
1972
|
+
render: function() {
|
1973
|
+
//Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
|
1974
|
+
var NestedForm = this.form.constructor;
|
1910
1975
|
|
1911
|
-
|
1976
|
+
var data = this.value || {},
|
1977
|
+
key = this.key,
|
1978
|
+
nestedModel = this.schema.model;
|
1912
1979
|
|
1913
|
-
|
1914
|
-
|
1980
|
+
//Wrap the data in a model if it isn't already a model instance
|
1981
|
+
var modelInstance = (data.constructor === nestedModel) ? data : new nestedModel(data);
|
1915
1982
|
|
1916
|
-
|
1917
|
-
|
1918
|
-
|
1919
|
-
|
1920
|
-
|
1921
|
-
*/
|
1922
|
-
commit: function() {
|
1923
|
-
var error = this.form.commit();
|
1924
|
-
if (error) {
|
1925
|
-
this.$el.addClass('error');
|
1926
|
-
return error;
|
1927
|
-
}
|
1983
|
+
this.nestedForm = new NestedForm({
|
1984
|
+
model: modelInstance,
|
1985
|
+
idPrefix: this.id + '_',
|
1986
|
+
fieldTemplate: 'nestedField'
|
1987
|
+
});
|
1928
1988
|
|
1929
|
-
|
1930
|
-
}
|
1989
|
+
this._observeFormEvents();
|
1931
1990
|
|
1932
|
-
|
1991
|
+
//Render form
|
1992
|
+
this.$el.html(this.nestedForm.render().el);
|
1933
1993
|
|
1994
|
+
if (this.hasFocus) this.trigger('blur', this);
|
1934
1995
|
|
1996
|
+
return this;
|
1997
|
+
},
|
1935
1998
|
|
1936
1999
|
/**
|
1937
|
-
*
|
1938
|
-
*
|
1939
|
-
* Schema options
|
1940
|
-
* @param {Number|String} [options.schema.yearStart] First year in list. Default: 100 years ago
|
1941
|
-
* @param {Number|String} [options.schema.yearEnd] Last year in list. Default: current year
|
2000
|
+
* Update the embedded model, checking for nested validation errors and pass them up
|
2001
|
+
* Then update the main model if all OK
|
1942
2002
|
*
|
1943
|
-
*
|
1944
|
-
* @param {Boolean} [options.showMonthNames] Use month names instead of numbers. Default: true
|
1945
|
-
* @param {String[]} [options.monthNames] Month names. Default: Full English names
|
2003
|
+
* @return {Error|null} Validation error or null
|
1946
2004
|
*/
|
1947
|
-
|
2005
|
+
commit: function() {
|
2006
|
+
var error = this.nestedForm.commit();
|
2007
|
+
if (error) {
|
2008
|
+
this.$el.addClass('error');
|
2009
|
+
return error;
|
2010
|
+
}
|
1948
2011
|
|
1949
|
-
|
1950
|
-
|
1951
|
-
this.updateHidden();
|
1952
|
-
this.trigger('change', this);
|
1953
|
-
},
|
1954
|
-
'focus select': function() {
|
1955
|
-
if (this.hasFocus) return;
|
1956
|
-
this.trigger('focus', this);
|
1957
|
-
},
|
1958
|
-
'blur select': function() {
|
1959
|
-
if (!this.hasFocus) return;
|
1960
|
-
var self = this;
|
1961
|
-
setTimeout(function() {
|
1962
|
-
if (self.$('select:focus')[0]) return;
|
1963
|
-
self.trigger('blur', self);
|
1964
|
-
}, 0);
|
1965
|
-
}
|
1966
|
-
},
|
2012
|
+
return Form.editors.Object.prototype.commit.call(this);
|
2013
|
+
}
|
1967
2014
|
|
1968
|
-
|
1969
|
-
options = options || {};
|
2015
|
+
});
|
1970
2016
|
|
1971
|
-
|
2017
|
+
/**
|
2018
|
+
* Date editor
|
2019
|
+
*
|
2020
|
+
* Schema options
|
2021
|
+
* @param {Number|String} [options.schema.yearStart] First year in list. Default: 100 years ago
|
2022
|
+
* @param {Number|String} [options.schema.yearEnd] Last year in list. Default: current year
|
2023
|
+
*
|
2024
|
+
* Config options (if not set, defaults to options stored on the main Date class)
|
2025
|
+
* @param {Boolean} [options.showMonthNames] Use month names instead of numbers. Default: true
|
2026
|
+
* @param {String[]} [options.monthNames] Month names. Default: Full English names
|
2027
|
+
*/
|
2028
|
+
Form.editors.Date = Form.editors.Base.extend({
|
1972
2029
|
|
1973
|
-
|
1974
|
-
|
2030
|
+
events: {
|
2031
|
+
'change select': function() {
|
2032
|
+
this.updateHidden();
|
2033
|
+
this.trigger('change', this);
|
2034
|
+
},
|
2035
|
+
'focus select': function() {
|
2036
|
+
if (this.hasFocus) return;
|
2037
|
+
this.trigger('focus', this);
|
2038
|
+
},
|
2039
|
+
'blur select': function() {
|
2040
|
+
if (!this.hasFocus) return;
|
2041
|
+
var self = this;
|
2042
|
+
setTimeout(function() {
|
2043
|
+
if (self.$('select:focus')[0]) return;
|
2044
|
+
self.trigger('blur', self);
|
2045
|
+
}, 0);
|
2046
|
+
}
|
2047
|
+
},
|
1975
2048
|
|
1976
|
-
|
1977
|
-
|
1978
|
-
monthNames: Self.monthNames,
|
1979
|
-
showMonthNames: Self.showMonthNames
|
1980
|
-
}, options);
|
2049
|
+
initialize: function(options) {
|
2050
|
+
options = options || {};
|
1981
2051
|
|
1982
|
-
|
1983
|
-
this.schema = _.extend({
|
1984
|
-
yearStart: today.getFullYear() - 100,
|
1985
|
-
yearEnd: today.getFullYear()
|
1986
|
-
}, options.schema || {});
|
2052
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
1987
2053
|
|
1988
|
-
|
1989
|
-
|
1990
|
-
this.value = new Date(this.value);
|
1991
|
-
}
|
2054
|
+
var Self = Form.editors.Date,
|
2055
|
+
today = new Date();
|
1992
2056
|
|
1993
|
-
|
1994
|
-
|
1995
|
-
|
1996
|
-
|
1997
|
-
|
2057
|
+
//Option defaults
|
2058
|
+
this.options = _.extend({
|
2059
|
+
monthNames: Self.monthNames,
|
2060
|
+
showMonthNames: Self.showMonthNames
|
2061
|
+
}, options);
|
1998
2062
|
|
1999
|
-
|
2000
|
-
|
2001
|
-
|
2063
|
+
//Schema defaults
|
2064
|
+
this.schema = _.extend({
|
2065
|
+
yearStart: today.getFullYear() - 100,
|
2066
|
+
yearEnd: today.getFullYear()
|
2067
|
+
}, options.schema || {});
|
2002
2068
|
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
2069
|
+
//Cast to Date
|
2070
|
+
if (this.value && !_.isDate(this.value)) {
|
2071
|
+
this.value = new Date(this.value);
|
2072
|
+
}
|
2006
2073
|
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2074
|
+
//Set default date
|
2075
|
+
if (!this.value) {
|
2076
|
+
var date = new Date();
|
2077
|
+
date.setSeconds(0);
|
2078
|
+
date.setMilliseconds(0);
|
2010
2079
|
|
2011
|
-
|
2012
|
-
|
2013
|
-
return '<option value="'+month+'">' + value + '</option>';
|
2014
|
-
});
|
2080
|
+
this.value = date;
|
2081
|
+
}
|
2015
2082
|
|
2016
|
-
|
2017
|
-
|
2018
|
-
|
2019
|
-
var yearsOptions = _.map(yearRange, function(year) {
|
2020
|
-
return '<option value="'+year+'">' + year + '</option>';
|
2021
|
-
});
|
2083
|
+
//Template
|
2084
|
+
this.template = options.template || this.constructor.template;
|
2085
|
+
},
|
2022
2086
|
|
2023
|
-
|
2024
|
-
|
2025
|
-
|
2026
|
-
months: monthsOptions.join(''),
|
2027
|
-
years: yearsOptions.join('')
|
2028
|
-
}));
|
2087
|
+
render: function() {
|
2088
|
+
var options = this.options,
|
2089
|
+
schema = this.schema;
|
2029
2090
|
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
this.$year = $el.find('select[data-type="year"]');
|
2091
|
+
var datesOptions = _.map(_.range(1, 32), function(date) {
|
2092
|
+
return '<option value="'+date+'">' + date + '</option>';
|
2093
|
+
});
|
2034
2094
|
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2095
|
+
var monthsOptions = _.map(_.range(0, 12), function(month) {
|
2096
|
+
var value = (options.showMonthNames)
|
2097
|
+
? options.monthNames[month]
|
2098
|
+
: (month + 1);
|
2038
2099
|
|
2039
|
-
|
2040
|
-
|
2100
|
+
return '<option value="'+month+'">' + value + '</option>';
|
2101
|
+
});
|
2041
2102
|
|
2042
|
-
|
2043
|
-
|
2044
|
-
|
2103
|
+
var yearRange = (schema.yearStart < schema.yearEnd)
|
2104
|
+
? _.range(schema.yearStart, schema.yearEnd + 1)
|
2105
|
+
: _.range(schema.yearStart, schema.yearEnd - 1, -1);
|
2045
2106
|
|
2046
|
-
|
2107
|
+
var yearsOptions = _.map(yearRange, function(year) {
|
2108
|
+
return '<option value="'+year+'">' + year + '</option>';
|
2109
|
+
});
|
2047
2110
|
|
2048
|
-
|
2049
|
-
|
2111
|
+
//Render the selects
|
2112
|
+
var $el = $($.trim(this.template({
|
2113
|
+
dates: datesOptions.join(''),
|
2114
|
+
months: monthsOptions.join(''),
|
2115
|
+
years: yearsOptions.join('')
|
2116
|
+
})));
|
2050
2117
|
|
2051
|
-
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
var year = this.$year.val(),
|
2056
|
-
month = this.$month.val(),
|
2057
|
-
date = this.$date.val();
|
2118
|
+
//Store references to selects
|
2119
|
+
this.$date = $el.find('[data-type="date"]');
|
2120
|
+
this.$month = $el.find('[data-type="month"]');
|
2121
|
+
this.$year = $el.find('[data-type="year"]');
|
2058
2122
|
|
2059
|
-
|
2123
|
+
//Create the hidden field to store values in case POSTed to server
|
2124
|
+
this.$hidden = $('<input type="hidden" name="'+this.key+'" />');
|
2125
|
+
$el.append(this.$hidden);
|
2060
2126
|
|
2061
|
-
|
2062
|
-
|
2127
|
+
//Set value on this and hidden field
|
2128
|
+
this.setValue(this.value);
|
2063
2129
|
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
this.$date.val(date.getDate());
|
2069
|
-
this.$month.val(date.getMonth());
|
2070
|
-
this.$year.val(date.getFullYear());
|
2130
|
+
//Remove the wrapper tag
|
2131
|
+
this.setElement($el);
|
2132
|
+
this.$el.attr('id', this.id);
|
2133
|
+
this.$el.attr('name', this.getName());
|
2071
2134
|
|
2072
|
-
|
2073
|
-
},
|
2135
|
+
if (this.hasFocus) this.trigger('blur', this);
|
2074
2136
|
|
2075
|
-
|
2076
|
-
|
2137
|
+
return this;
|
2138
|
+
},
|
2077
2139
|
|
2078
|
-
|
2079
|
-
|
2140
|
+
/**
|
2141
|
+
* @return {Date} Selected date
|
2142
|
+
*/
|
2143
|
+
getValue: function() {
|
2144
|
+
var year = this.$year.val(),
|
2145
|
+
month = this.$month.val(),
|
2146
|
+
date = this.$date.val();
|
2080
2147
|
|
2081
|
-
|
2082
|
-
if (!this.hasFocus) return;
|
2148
|
+
if (!year || !month || !date) return null;
|
2083
2149
|
|
2084
|
-
|
2085
|
-
|
2150
|
+
return new Date(year, month, date);
|
2151
|
+
},
|
2086
2152
|
|
2087
|
-
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2153
|
+
/**
|
2154
|
+
* @param {Date} date
|
2155
|
+
*/
|
2156
|
+
setValue: function(date) {
|
2157
|
+
this.$date.val(date.getDate());
|
2158
|
+
this.$month.val(date.getMonth());
|
2159
|
+
this.$year.val(date.getFullYear());
|
2094
2160
|
|
2095
|
-
|
2096
|
-
|
2161
|
+
this.updateHidden();
|
2162
|
+
},
|
2097
2163
|
|
2098
|
-
|
2099
|
-
|
2164
|
+
focus: function() {
|
2165
|
+
if (this.hasFocus) return;
|
2100
2166
|
|
2101
|
-
|
2102
|
-
|
2167
|
+
this.$('select').first().focus();
|
2168
|
+
},
|
2103
2169
|
|
2104
|
-
|
2105
|
-
|
2106
|
-
monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
2107
|
-
});
|
2170
|
+
blur: function() {
|
2171
|
+
if (!this.hasFocus) return;
|
2108
2172
|
|
2173
|
+
this.$('select:focus').blur();
|
2174
|
+
},
|
2109
2175
|
|
2110
2176
|
/**
|
2111
|
-
*
|
2112
|
-
*
|
2113
|
-
* @param {Editor} [options.DateEditor] Date editor view to use (not definition)
|
2114
|
-
* @param {Number} [options.schema.minsInterval] Interval between minutes. Default: 15
|
2177
|
+
* Update the hidden input which is maintained for when submitting a form
|
2178
|
+
* via a normal browser POST
|
2115
2179
|
*/
|
2116
|
-
|
2180
|
+
updateHidden: function() {
|
2181
|
+
var val = this.getValue();
|
2117
2182
|
|
2118
|
-
|
2119
|
-
'change select': function() {
|
2120
|
-
this.updateHidden();
|
2121
|
-
this.trigger('change', this);
|
2122
|
-
},
|
2123
|
-
'focus select': function() {
|
2124
|
-
if (this.hasFocus) return;
|
2125
|
-
this.trigger('focus', this);
|
2126
|
-
},
|
2127
|
-
'blur select': function() {
|
2128
|
-
if (!this.hasFocus) return;
|
2129
|
-
var self = this;
|
2130
|
-
setTimeout(function() {
|
2131
|
-
if (self.$('select:focus')[0]) return;
|
2132
|
-
self.trigger('blur', self);
|
2133
|
-
}, 0);
|
2134
|
-
}
|
2135
|
-
},
|
2183
|
+
if (_.isDate(val)) val = val.toISOString();
|
2136
2184
|
|
2137
|
-
|
2138
|
-
|
2185
|
+
this.$hidden.val(val);
|
2186
|
+
}
|
2139
2187
|
|
2140
|
-
|
2188
|
+
}, {
|
2189
|
+
//STATICS
|
2190
|
+
template: _.template('\
|
2191
|
+
<div>\
|
2192
|
+
<select data-type="date"><%= dates %></select>\
|
2193
|
+
<select data-type="month"><%= months %></select>\
|
2194
|
+
<select data-type="year"><%= years %></select>\
|
2195
|
+
</div>\
|
2196
|
+
', null, Form.templateSettings),
|
2141
2197
|
|
2142
|
-
|
2143
|
-
|
2144
|
-
DateEditor: editors.DateTime.DateEditor
|
2145
|
-
}, options);
|
2198
|
+
//Whether to show month names instead of numbers
|
2199
|
+
showMonthNames: true,
|
2146
2200
|
|
2147
|
-
|
2148
|
-
|
2149
|
-
|
2150
|
-
|
2201
|
+
//Month names to use if showMonthNames is true
|
2202
|
+
//Replace for localisation, e.g. Form.editors.Date.monthNames = ['Janvier', 'Fevrier'...]
|
2203
|
+
monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
2204
|
+
});
|
2151
2205
|
|
2152
|
-
|
2153
|
-
|
2206
|
+
/**
|
2207
|
+
* DateTime editor
|
2208
|
+
*
|
2209
|
+
* @param {Editor} [options.DateEditor] Date editor view to use (not definition)
|
2210
|
+
* @param {Number} [options.schema.minsInterval] Interval between minutes. Default: 15
|
2211
|
+
*/
|
2212
|
+
Form.editors.DateTime = Form.editors.Base.extend({
|
2154
2213
|
|
2155
|
-
|
2214
|
+
events: {
|
2215
|
+
'change select': function() {
|
2216
|
+
this.updateHidden();
|
2217
|
+
this.trigger('change', this);
|
2156
2218
|
},
|
2219
|
+
'focus select': function() {
|
2220
|
+
if (this.hasFocus) return;
|
2221
|
+
this.trigger('focus', this);
|
2222
|
+
},
|
2223
|
+
'blur select': function() {
|
2224
|
+
if (!this.hasFocus) return;
|
2225
|
+
var self = this;
|
2226
|
+
setTimeout(function() {
|
2227
|
+
if (self.$('select:focus')[0]) return;
|
2228
|
+
self.trigger('blur', self);
|
2229
|
+
}, 0);
|
2230
|
+
}
|
2231
|
+
},
|
2157
2232
|
|
2158
|
-
|
2159
|
-
|
2160
|
-
return n < 10 ? '0' + n : n;
|
2161
|
-
}
|
2162
|
-
|
2163
|
-
var schema = this.schema;
|
2164
|
-
|
2165
|
-
//Create options
|
2166
|
-
var hoursOptions = _.map(_.range(0, 24), function(hour) {
|
2167
|
-
return '<option value="'+hour+'">' + pad(hour) + '</option>';
|
2168
|
-
});
|
2169
|
-
|
2170
|
-
var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) {
|
2171
|
-
return '<option value="'+min+'">' + pad(min) + '</option>';
|
2172
|
-
});
|
2233
|
+
initialize: function(options) {
|
2234
|
+
options = options || {};
|
2173
2235
|
|
2174
|
-
|
2175
|
-
var $el = Form.helpers.parseHTML(Form.templates.dateTime({
|
2176
|
-
date: '<b class="bbf-tmp"></b>',
|
2177
|
-
hours: hoursOptions.join(),
|
2178
|
-
mins: minsOptions.join()
|
2179
|
-
}));
|
2236
|
+
Form.editors.Base.prototype.initialize.call(this, options);
|
2180
2237
|
|
2181
|
-
|
2182
|
-
|
2238
|
+
//Option defaults
|
2239
|
+
this.options = _.extend({
|
2240
|
+
DateEditor: Form.editors.DateTime.DateEditor
|
2241
|
+
}, options);
|
2183
2242
|
|
2184
|
-
|
2185
|
-
|
2186
|
-
|
2243
|
+
//Schema defaults
|
2244
|
+
this.schema = _.extend({
|
2245
|
+
minsInterval: 15
|
2246
|
+
}, options.schema || {});
|
2187
2247
|
|
2188
|
-
|
2189
|
-
|
2248
|
+
//Create embedded date editor
|
2249
|
+
this.dateEditor = new this.options.DateEditor(options);
|
2190
2250
|
|
2191
|
-
|
2192
|
-
this.setValue(this.value);
|
2251
|
+
this.value = this.dateEditor.value;
|
2193
2252
|
|
2194
|
-
|
2195
|
-
|
2253
|
+
//Template
|
2254
|
+
this.template = options.template || this.constructor.template;
|
2255
|
+
},
|
2196
2256
|
|
2197
|
-
|
2257
|
+
render: function() {
|
2258
|
+
function pad(n) {
|
2259
|
+
return n < 10 ? '0' + n : n;
|
2260
|
+
}
|
2198
2261
|
|
2199
|
-
|
2200
|
-
},
|
2262
|
+
var schema = this.schema;
|
2201
2263
|
|
2202
|
-
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
var date = this.dateEditor.getValue();
|
2264
|
+
//Create options
|
2265
|
+
var hoursOptions = _.map(_.range(0, 24), function(hour) {
|
2266
|
+
return '<option value="'+hour+'">' + pad(hour) + '</option>';
|
2267
|
+
});
|
2207
2268
|
|
2208
|
-
|
2209
|
-
|
2269
|
+
var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) {
|
2270
|
+
return '<option value="'+min+'">' + pad(min) + '</option>';
|
2271
|
+
});
|
2210
2272
|
|
2211
|
-
|
2273
|
+
//Render time selects
|
2274
|
+
var $el = $($.trim(this.template({
|
2275
|
+
hours: hoursOptions.join(),
|
2276
|
+
mins: minsOptions.join()
|
2277
|
+
})));
|
2212
2278
|
|
2213
|
-
|
2214
|
-
|
2279
|
+
//Include the date editor
|
2280
|
+
$el.find('[data-date]').append(this.dateEditor.render().el);
|
2215
2281
|
|
2216
|
-
|
2217
|
-
|
2282
|
+
//Store references to selects
|
2283
|
+
this.$hour = $el.find('select[data-type="hour"]');
|
2284
|
+
this.$min = $el.find('select[data-type="min"]');
|
2218
2285
|
|
2219
|
-
|
2220
|
-
|
2286
|
+
//Get the hidden date field to store values in case POSTed to server
|
2287
|
+
this.$hidden = $el.find('input[type="hidden"]');
|
2221
2288
|
|
2222
|
-
|
2289
|
+
//Set time
|
2290
|
+
this.setValue(this.value);
|
2223
2291
|
|
2224
|
-
|
2225
|
-
|
2292
|
+
this.setElement($el);
|
2293
|
+
this.$el.attr('id', this.id);
|
2294
|
+
this.$el.attr('name', this.getName());
|
2226
2295
|
|
2227
|
-
|
2228
|
-
},
|
2296
|
+
if (this.hasFocus) this.trigger('blur', this);
|
2229
2297
|
|
2230
|
-
|
2231
|
-
|
2298
|
+
return this;
|
2299
|
+
},
|
2232
2300
|
|
2233
|
-
|
2234
|
-
|
2301
|
+
/**
|
2302
|
+
* @return {Date} Selected datetime
|
2303
|
+
*/
|
2304
|
+
getValue: function() {
|
2305
|
+
var date = this.dateEditor.getValue();
|
2235
2306
|
|
2236
|
-
|
2237
|
-
|
2307
|
+
var hour = this.$hour.val(),
|
2308
|
+
min = this.$min.val();
|
2238
2309
|
|
2239
|
-
|
2240
|
-
},
|
2310
|
+
if (!date || !hour || !min) return null;
|
2241
2311
|
|
2242
|
-
|
2243
|
-
|
2244
|
-
* via a normal browser POST
|
2245
|
-
*/
|
2246
|
-
updateHidden: function() {
|
2247
|
-
var val = this.getValue();
|
2248
|
-
if (_.isDate(val)) val = val.toISOString();
|
2312
|
+
date.setHours(hour);
|
2313
|
+
date.setMinutes(min);
|
2249
2314
|
|
2250
|
-
|
2251
|
-
|
2315
|
+
return date;
|
2316
|
+
},
|
2252
2317
|
|
2253
|
-
|
2254
|
-
|
2255
|
-
|
2256
|
-
|
2257
|
-
|
2318
|
+
/**
|
2319
|
+
* @param {Date}
|
2320
|
+
*/
|
2321
|
+
setValue: function(date) {
|
2322
|
+
if (!_.isDate(date)) date = new Date(date);
|
2258
2323
|
|
2259
|
-
|
2260
|
-
}
|
2324
|
+
this.dateEditor.setValue(date);
|
2261
2325
|
|
2262
|
-
|
2263
|
-
|
2326
|
+
this.$hour.val(date.getHours());
|
2327
|
+
this.$min.val(date.getMinutes());
|
2264
2328
|
|
2265
|
-
|
2266
|
-
|
2267
|
-
});
|
2329
|
+
this.updateHidden();
|
2330
|
+
},
|
2268
2331
|
|
2269
|
-
|
2332
|
+
focus: function() {
|
2333
|
+
if (this.hasFocus) return;
|
2270
2334
|
|
2271
|
-
|
2335
|
+
this.$('select').first().focus();
|
2336
|
+
},
|
2272
2337
|
|
2338
|
+
blur: function() {
|
2339
|
+
if (!this.hasFocus) return;
|
2273
2340
|
|
2274
|
-
|
2275
|
-
|
2276
|
-
//Add function shortcuts
|
2277
|
-
Form.setTemplates = Form.helpers.setTemplates;
|
2278
|
-
Form.setTemplateCompiler = Form.helpers.setTemplateCompiler;
|
2341
|
+
this.$('select:focus').blur();
|
2342
|
+
},
|
2279
2343
|
|
2280
|
-
|
2344
|
+
/**
|
2345
|
+
* Update the hidden input which is maintained for when submitting a form
|
2346
|
+
* via a normal browser POST
|
2347
|
+
*/
|
2348
|
+
updateHidden: function() {
|
2349
|
+
var val = this.getValue();
|
2350
|
+
if (_.isDate(val)) val = val.toISOString();
|
2281
2351
|
|
2352
|
+
this.$hidden.val(val);
|
2353
|
+
},
|
2282
2354
|
|
2283
|
-
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2288
|
-
<form class="bbf-form">{{fieldsets}}</form>\
|
2289
|
-
',
|
2290
|
-
|
2291
|
-
fieldset: '\
|
2292
|
-
<fieldset>\
|
2293
|
-
<legend>{{legend}}</legend>\
|
2294
|
-
<ul>{{fields}}</ul>\
|
2295
|
-
</fieldset>\
|
2296
|
-
',
|
2297
|
-
|
2298
|
-
field: '\
|
2299
|
-
<li class="bbf-field field-{{key}}">\
|
2300
|
-
<label for="{{id}}">{{title}}</label>\
|
2301
|
-
<div class="bbf-editor">{{editor}}</div>\
|
2302
|
-
<div class="bbf-help">{{help}}</div>\
|
2303
|
-
<div class="bbf-error">{{error}}</div>\
|
2304
|
-
</li>\
|
2305
|
-
',
|
2306
|
-
|
2307
|
-
nestedField: '\
|
2308
|
-
<li class="bbf-field bbf-nested-field field-{{key}}" title="{{title}}">\
|
2309
|
-
<label for="{{id}}">{{title}}</label>\
|
2310
|
-
<div class="bbf-editor">{{editor}}</div>\
|
2311
|
-
<div class="bbf-help">{{help}}</div>\
|
2312
|
-
<div class="bbf-error">{{error}}</div>\
|
2313
|
-
</li>\
|
2314
|
-
',
|
2315
|
-
|
2316
|
-
list: '\
|
2317
|
-
<div class="bbf-list">\
|
2318
|
-
<ul>{{items}}</ul>\
|
2319
|
-
<div class="bbf-actions"><button type="button" data-action="add">Add</div>\
|
2320
|
-
</div>\
|
2321
|
-
',
|
2322
|
-
|
2323
|
-
listItem: '\
|
2324
|
-
<li>\
|
2325
|
-
<button type="button" data-action="remove" class="bbf-remove">×</button>\
|
2326
|
-
<div class="bbf-editor-container">{{editor}}</div>\
|
2327
|
-
</li>\
|
2328
|
-
',
|
2329
|
-
|
2330
|
-
date: '\
|
2331
|
-
<div class="bbf-date">\
|
2332
|
-
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
2333
|
-
<select data-type="month" class="bbf-month">{{months}}</select>\
|
2334
|
-
<select data-type="year" class="bbf-year">{{years}}</select>\
|
2335
|
-
</div>\
|
2336
|
-
',
|
2337
|
-
|
2338
|
-
dateTime: '\
|
2339
|
-
<div class="bbf-datetime">\
|
2340
|
-
<div class="bbf-date-container">{{date}}</div>\
|
2341
|
-
<select data-type="hour">{{hours}}</select>\
|
2342
|
-
:\
|
2343
|
-
<select data-type="min">{{mins}}</select>\
|
2344
|
-
</div>\
|
2345
|
-
',
|
2355
|
+
/**
|
2356
|
+
* Remove the Date editor before removing self
|
2357
|
+
*/
|
2358
|
+
remove: function() {
|
2359
|
+
this.dateEditor.remove();
|
2346
2360
|
|
2347
|
-
|
2348
|
-
|
2349
|
-
{{summary}}\
|
2350
|
-
</div>\
|
2351
|
-
'
|
2352
|
-
}, {
|
2361
|
+
Form.editors.Base.prototype.remove.call(this);
|
2362
|
+
}
|
2353
2363
|
|
2354
|
-
|
2355
|
-
|
2364
|
+
}, {
|
2365
|
+
//STATICS
|
2366
|
+
template: _.template('\
|
2367
|
+
<div class="bbf-datetime">\
|
2368
|
+
<div class="bbf-date-container" data-date></div>\
|
2369
|
+
<select data-type="hour"><%= hours %></select>\
|
2370
|
+
:\
|
2371
|
+
<select data-type="min"><%= mins %></select>\
|
2372
|
+
</div>\
|
2373
|
+
', null, Form.templateSettings),
|
2356
2374
|
|
2357
|
-
|
2375
|
+
//The date editor to use (constructor function, not instance)
|
2376
|
+
DateEditor: Form.editors.Date
|
2377
|
+
});
|
2358
2378
|
|
2359
2379
|
|
2360
2380
|
|
2361
2381
|
//Metadata
|
2362
|
-
Form.VERSION = '0.
|
2382
|
+
Form.VERSION = '0.12.0';
|
2363
2383
|
|
2364
2384
|
|
2365
2385
|
//Exports
|