rails-backbone-forms 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 055fbaf8a365f673eaba42bb32c713f5a0d7e510
4
- data.tar.gz: 2867fd620be058089746f1698710a5bbe222dcb7
3
+ metadata.gz: e3fca4540b9490eebd58cf719e113313c678f9d0
4
+ data.tar.gz: e92315755428a19d134e791441583d0f87c420b8
5
5
  SHA512:
6
- metadata.gz: 231ce5d33546d9460ca5916d6e56b20286785a356311db9583efacbe211459d1210e74372d523f84dba4610696d0e9ef6b7b2134ba9ea469ac0da5451169fccf
7
- data.tar.gz: 4f15426f3a6b89abc2c940bdb9050196a418921d542735ce2c9f9a19a5744eaec52e2e9322265a78993779ed60d41312b5e266e6e7cd604e9d287d553a328858
6
+ metadata.gz: c55094e51fd63d02d758de064e0ccb13e737f9b37752e8518e1155a4a4e9c39af45cb1afda324f1972ee3b12ab6700d3ad2f32e449d6c94d749909ce6bd469c2
7
+ data.tar.gz: b7a499ac74e83c4e3b46b6ebfe6b94ca4742e0d0233143fd143b87b28ab9b4b4f555bdc3fb0dfea909a92dcd6a3d124c16b9fa71e07c620190bf489e1ef82586
@@ -1,7 +1,7 @@
1
1
  module Rails
2
2
  module Backbone
3
3
  module Forms
4
- VERSION = '0.11.1'
4
+ VERSION = '0.12.0'
5
5
  end
6
6
  end
7
7
  end
@@ -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(_.extend({
137
+ $el.modal({
156
138
  keyboard: this.options.allowCancel,
157
139
  backdrop: this.options.allowCancel ? true : 'static'
158
- }, this.options.modalOptions));
140
+ });
159
141
 
160
142
  //Focus OK button
161
143
  $el.one('shown', function() {
162
- if (self.options.focusOk) {
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 = parseInt($backdrop.css('z-index'),10),
177
- elIndex = parseInt($backdrop.css('z-index'), 10);
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.one('hidden', function onHidden(e) {
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
- if (self.options.content && self.options.content.trigger) {
235
- self.options.content.trigger('hidden', self);
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"),this.options.content&&this.options.content.trigger&&this.options.content.trigger("cancel",this)},"click .cancel":function(e){e.preventDefault(),this.trigger("cancel"),this.options.content&&this.options.content.trigger&&this.options.content.trigger("cancel",this)},"click .ok":function(e){e.preventDefault(),this.trigger("ok"),this.options.content&&this.options.content.trigger&&this.options.content.trigger("ok",this),this.options.okCloses&&this.close()}},initialize:function(e){this.options=t.extend({title:null,okText:"OK",focusOk:!0,okCloses:!0,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&&(n.render(),e.find(".modal-body").html(n.$el)),t.animate&&e.addClass("fade"),this.isRendered=!0,this},open:function(n){this.isRendered||this.render();var r=this,i=this.$el;i.modal(t.extend({keyboard:this.options.allowCancel,backdrop:this.options.allowCancel?!0:"static"},this.options.modalOptions)),i.one("shown",function(){r.options.focusOk&&i.find(".btn.ok").focus(),r.options.content&&r.options.content.trigger&&r.options.content.trigger("shown",r),r.trigger("shown")});var o=s.count,u=e(".modal-backdrop:eq("+o+")"),a=parseInt(u.css("z-index"),10),f=parseInt(u.css("z-index"),10);return u.css("z-index",a+o),this.$el.css("z-index",f+o),this.options.allowCancel&&(u.one("click",function(){r.options.content&&r.options.content.trigger&&r.options.content.trigger("cancel",r),r.trigger("cancel")}),e(document).one("keyup.dismiss.modal",function(e){e.which==27&&r.trigger("cancel"),r.options.content&&r.options.content.trigger&&e.which==27&&r.options.content.trigger("shown",r)})),this.on("cancel",function(){r.close()}),s.count++,n&&r.on("ok",n),this},close:function(){var e=this,t=this.$el;if(this._preventClose){this._preventClose=!1;return}t.one("hidden",function n(r){if(r.target!==r.currentTarget)return t.one("hidden",n);e.remove(),e.options.content&&e.options.content.trigger&&e.options.content.trigger("hidden",e),e.trigger("hidden")}),t.modal("hide"),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
+ (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.11.0
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 = (function() {
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
- //Store main attributes
85
- this.options = options;
86
- this.model = options.model;
87
- this.data = options.data;
88
- this.fields = {};
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
- //Create el from template
100
- var $form = Form.helpers.parseHTML(template({
101
- fieldsets: '<b class="bbf-tmp"></b>'
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
- //Render fieldsets
105
- var $fieldsetContainer = $('.bbf-tmp', $form);
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
- _.each(options.fieldsets, function(fieldset) {
108
- $fieldsetContainer.append(self.renderFieldset(fieldset));
109
- });
64
+ //Then built-in schema
65
+ if (self.schema) {
66
+ return (_.isFunction(self.schema)) ? self.schema() : self.schema;
67
+ }
110
68
 
111
- $fieldsetContainer.children().unwrap();
69
+ //Fallback to empty schema
70
+ return {};
71
+ })();
112
72
 
113
- //Set the template contents as the main element; removes the wrapper element
114
- this.setElement($form);
73
+ //Store important data
74
+ _.extend(this, _.pick(options, 'model', 'data', 'idPrefix'));
115
75
 
116
- if (this.hasFocus) this.trigger('blur', this);
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
- return this;
119
- },
83
+ //Check which fields will be included (defaults to all)
84
+ var selectedFields = this.selectedFields = options.fields || _.keys(schema);
120
85
 
121
- /**
122
- * Renders a fieldset and the fields within it
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
- //Concatenating HTML as strings won't work so we need to insert field elements into a placeholder
144
- var $fieldset = Form.helpers.parseHTML(template(_.extend({}, fieldset, {
145
- legend: '<b class="bbf-tmp-legend"></b>',
146
- fields: '<b class="bbf-tmp-fields"></b>'
147
- })));
89
+ _.each(selectedFields, function(key) {
90
+ var fieldSchema = schema[key];
91
+ fields[key] = this.createField(key, fieldSchema);
92
+ }, this);
148
93
 
149
- //Set legend
150
- if (fieldset.legend) {
151
- $fieldset.find('.bbf-tmp-legend').replaceWith(fieldset.legend);
152
- }
153
- //or remove the containing tag if there isn't a legend
154
- else {
155
- $fieldset.find('.bbf-tmp-legend').parent().remove();
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
- var $fieldsContainer = $('.bbf-tmp-fields', $fieldset);
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
- //Render fields
161
- _.each(fieldset.fields, function(key) {
162
- //Get the field schema
163
- var itemSchema = (function() {
164
- //Return a normal key or path key
165
- if (schema[key]) return schema[key];
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
- //Return a nested schema, i.e. Object
168
- var path = key.replace(/\./g, '.subSchema.');
169
- return getNested(schema, path);
170
- })();
143
+ var field = new this.Field(options);
171
144
 
172
- if (!itemSchema) throw "Field '"+key+"' not found in schema";
145
+ this.listenTo(field.editor, 'all', this.handleEditorEvent);
173
146
 
174
- //Create the field
175
- var field = self.fields[key] = self.createField(key, itemSchema);
147
+ return field;
148
+ },
176
149
 
177
- //Render the fields with editors, apart from Hidden fields
178
- var fieldEl = field.render().el;
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
- field.editor.on('all', function(event) {
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
- this.trigger.apply(this, args);
188
- }, self);
163
+ //Trigger additional events
164
+ switch (event) {
165
+ case 'change':
166
+ this.trigger('change', this);
167
+ break;
189
168
 
190
- field.editor.on('change', function() {
191
- this.trigger('change', self);
192
- }, self);
169
+ case 'focus':
170
+ if (!this.hasFocus) this.trigger('focus', this);
171
+ break;
193
172
 
194
- field.editor.on('focus', function() {
195
- if (this.hasFocus) return;
196
- this.trigger('focus', this);
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
- if (_.find(self.fields, function(field) { return field.editor.hasFocus; })) return;
203
- self.trigger('blur', self);
204
- }, 0);
205
- }, self);
178
+ var focusedField = _.find(self.fields, function(field) {
179
+ return field.editor.hasFocus;
180
+ });
206
181
 
207
- if (itemSchema.type !== 'Hidden') {
208
- $fieldsContainer.append(fieldEl);
182
+ if (!focusedField) self.trigger('blur', self);
183
+ }, 0);
209
184
  }
210
- });
185
+ break;
186
+ }
187
+ },
211
188
 
212
- $fieldsContainer = $fieldsContainer.children().unwrap();
189
+ render: function() {
190
+ var self = this,
191
+ fields = this.fields;
213
192
 
214
- return $fieldset;
215
- },
193
+ //Render form
194
+ var $form = $($.trim(this.template(_.result(this, 'templateData'))));
216
195
 
217
- /**
218
- * Renders a field and returns it
219
- *
220
- * @param {String} key The key for the field in the form schema
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 (this.model) {
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
- return new Form.Field(options);
245
- },
203
+ //Work out which fields to include
204
+ var keys = (selection == '*')
205
+ ? self.selectedFields || _.keys(fields)
206
+ : selection.split(',');
246
207
 
247
- /**
248
- * Validate the data
249
- *
250
- * @return {Object} Validation errors
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
- //Get errors from default Backbone model validator
267
- if (model && model.validate) {
268
- var modelErrors = model.validate(this.getValue());
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
- if (modelErrors) {
271
- var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
221
+ if (_.isUndefined(selection)) return;
272
222
 
273
- //If errors are not in object form then just store on the error object
274
- if (!isDictionary) {
275
- errors._others = errors._others || [];
276
- errors._others.push(modelErrors);
277
- }
223
+ //Work out which fields to include
224
+ var keys = (selection == '*')
225
+ ? self.selectedFields || _.keys(fields)
226
+ : selection.split(',');
278
227
 
279
- //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
280
- if (isDictionary) {
281
- _.each(modelErrors, function(val, key) {
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
- return _.isEmpty(errors) ? null : errors;
301
- },
232
+ $container.append(field.render().el);
233
+ });
234
+ });
302
235
 
303
- /**
304
- * Update the model with all latest values.
305
- *
306
- * @param {Object} [options] Options to pass to Model#set (e.g. { silent: true })
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
- this.model.set(this.getValue(), setOptions);
325
-
326
- if (modelError) return modelError;
327
- },
241
+ if (_.isUndefined(selection)) return;
328
242
 
329
- /**
330
- * Get all the field values as an object.
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
- return values;
346
- },
347
-
348
- /**
349
- * Update field values, referenced by key
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
- focus: function() {
370
- if (this.hasFocus) return;
254
+ return this;
255
+ },
371
256
 
372
- var fieldset = this.options.fieldsets[0];
373
- if (fieldset) {
374
- var field;
375
- if (_.isArray(fieldset)) {
376
- field = fieldset[0];
377
- }
378
- else {
379
- field = fieldset.fields[0];
380
- }
381
- if (field) {
382
- this.fields[field].editor.focus();
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
- blur: function() {
388
- if (!this.hasFocus) return;
276
+ //Get errors from default Backbone model validator
277
+ if (model && model.validate) {
278
+ var modelErrors = model.validate(this.getValue());
389
279
 
390
- var focusedField = _.find(this.fields, function(field) { return field.editor.hasFocus; });
280
+ if (modelErrors) {
281
+ var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
391
282
 
392
- if (focusedField) focusedField.editor.blur();
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
- * Override default remove function in order to remove embedded views
397
- */
398
- remove: function() {
399
- var fields = this.fields;
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
- for (var key in fields) {
402
- fields[key].remove();
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
- Backbone.View.prototype.remove.call(this);
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
- return Backbone.View.prototype.trigger.apply(this, arguments);
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
- //HELPERS
425
- //==================================================================================================
334
+ this.model.set(this.getValue(), setOptions);
335
+
336
+ if (modelError) return modelError;
337
+ },
426
338
 
427
- Form.helpers = (function() {
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
- var helpers = {};
355
+ return values;
356
+ },
430
357
 
431
358
  /**
432
- * Gets a nested attribute using a path e.g. 'user.name'
359
+ * Update field values, referenced by key
433
360
  *
434
- * @param {Object} obj Object to fetch attribute from
435
- * @param {String} path Attribute path e.g. 'user.name'
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
- helpers.getNested = function(obj, path) {
440
- var fields = path.split(".");
441
- var result = obj;
442
- for (var i = 0, n = fields.length; i < n; i++) {
443
- result = result[fields[i]];
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
- return result;
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
- * This function is used to transform the key from a schema into the title used in a label.
450
- * (If a specific title is provided it will be used instead).
451
- *
452
- * By default this converts a camelCase string into words, i.e. Camel Case
453
- * If you have a different naming convention for schema keys, replace this function.
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
- helpers.keyToTitle = function(str) {
459
- //Add spaces
460
- str = str.replace(/([A-Z])/g, ' $1');
387
+ getEditor: function(key) {
388
+ var field = this.fields[key];
389
+ if (!field) throw 'Field not found: '+key;
461
390
 
462
- //Uppercase first character
463
- str = str.replace(/^./, function(str) { return str.toUpperCase(); });
464
-
465
- return str;
466
- };
391
+ return field.editor;
392
+ },
467
393
 
468
394
  /**
469
- * Helper to compile a template with the {{mustache}} style tags. Template settings are reset
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
- helpers.compileTemplate = function(str) {
475
- //Store user's template options
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
- var template = _.template(str);
400
+ //Get the first field
401
+ var fieldset = this.fieldsets[0],
402
+ field = fieldset.getFieldAt(0);
482
403
 
483
- //Reset to users' template settings
484
- _.templateSettings.interpolate = _interpolateBackup;
404
+ if (!field) return;
485
405
 
486
- return template;
487
- };
406
+ //Set focus
407
+ field.editor.focus();
408
+ },
488
409
 
489
410
  /**
490
- * Helper to create a template with the {{mustache}} style tags.
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
- helpers.createTemplate = function(str, context) {
497
- var template = helpers.compileTemplate($.trim(str));
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
- * Sets the template compiler to the given function
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 (_.isString(schemaType)) {
559
- constructorFn = Form.editors[schemaType];
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
- * Returns a validation function based on the type defined in the schema
424
+ * Manages the hasFocus property
570
425
  *
571
- * @param {RegExp|String|Function} validator
572
- * @return {Function}
426
+ * @param {String} event
573
427
  */
574
- helpers.getValidator = function(validator) {
575
- var validators = Form.validators;
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
- //Functions can be used directly
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
- * Given an HTML string, return a jQuery-wrapped array of DOM nodes.
440
+ * Override default remove function in order to remove embedded views
606
441
  *
607
- * @param {String} html
608
- * @return {Object}
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
- helpers.parseHTML = function(html) {
611
- if ($.parseHTML !== undefined) {
612
- return $($.parseHTML(html));
613
- }
614
- return $(html);
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
- return helpers;
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 "{{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: Form.helpers.createTemplate(options.message, options)
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: Form.helpers.createTemplate(options.message, options)
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: Form.helpers.createTemplate(options.message, options)
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
- //FIELD
582
+ //FIELDSET
731
583
  //==================================================================================================
732
584
 
733
- Form.Field = (function() {
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
- * Renders the field
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
- //Decide on data delivery type to pass to editors
821
- if (this.model) {
822
- options.model = this.model;
823
- } else {
824
- options.value = this.value;
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
- //Decide on the editor to use
828
- var editor = this.editor = helpers.createEditor(schema.type, options);
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
- //Create the element
831
- var $field = Form.helpers.parseHTML(templates[schema.template](this.renderingContext(schema, editor)));
623
+ //Add null legend to prevent template error
624
+ schema.legend = schema.legend || null;
832
625
 
833
- //Remove <label> if it's not wanted
834
- if (schema.title === false) {
835
- $field.find('label[for="'+editor.id+'"]').first().remove();
836
- }
626
+ return schema;
627
+ },
837
628
 
838
- //Render editor
839
- $field.find('.bbf-tmp-editor').replaceWith(editor.render().el);
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
- //Set help text
842
- this.$help = $('.bbf-tmp-help', $field).parent();
843
- this.$help.empty();
844
- if (this.schema.help) this.$help.html(this.schema.help);
639
+ return this.fields[key];
640
+ },
845
641
 
846
- //Create error container
847
- this.$error = $($('.bbf-tmp-error', $field).parent()[0]);
848
- if (this.$error) this.$error.empty();
642
+ /**
643
+ * Returns data to pass to template
644
+ *
645
+ * @return {Object}
646
+ */
647
+ templateData: function() {
648
+ return this.schema;
649
+ },
849
650
 
850
- //Add custom CSS class names
851
- if (this.schema.fieldClass) $field.addClass(this.schema.fieldClass);
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
- //Add custom attributes
854
- if (this.schema.fieldAttrs) $field.attr(this.schema.fieldAttrs);
660
+ //Render fieldset
661
+ var $fieldset = $($.trim(this.template(_.result(this, 'templateData'))));
855
662
 
856
- //Replace the generated wrapper tag
857
- this.setElement($field);
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 this;
860
- },
668
+ if (_.isUndefined(selection)) return;
861
669
 
862
- /**
863
- * Creates the ID that will be assigned to the editor
864
- *
865
- * @return {String}
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
- //Replace periods with underscores (e.g. for when using paths)
872
- id = id.replace(/\./g, '_');
675
+ this.setElement($fieldset);
873
676
 
874
- //If a specific ID prefix is set, use it
875
- if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
876
- if (_.isNull(prefix)) return id;
677
+ return this;
678
+ },
877
679
 
878
- //Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
879
- if (this.model) return this.model.cid + '_' + id;
680
+ /**
681
+ * Remove embedded views then self
682
+ */
683
+ remove: function() {
684
+ _.each(this.fields, function(field) {
685
+ field.remove();
686
+ });
880
687
 
881
- return id;
882
- },
688
+ Backbone.View.prototype.remove.call(this);
689
+ }
690
+
691
+ }, {
692
+ //STATICS
883
693
 
884
- /**
885
- * Check the validity of the field
886
- *
887
- * @return {String}
888
- */
889
- validate: function() {
890
- var error = this.editor.validate();
694
+ template: _.template('\
695
+ <fieldset data-fields>\
696
+ <% if (legend) { %>\
697
+ <legend><%= legend %></legend>\
698
+ <% } %>\
699
+ </fieldset>\
700
+ ', null, Form.templateSettings)
891
701
 
892
- if (error) {
893
- this.setError(error.message);
894
- } else {
895
- this.clearError();
896
- }
702
+ });
897
703
 
898
- return error;
899
- },
900
704
 
901
- /**
902
- * Set the field into an error state, adding the error class and setting the error message
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
- var errClass = Form.classNames.error;
709
+ Form.Field = Backbone.View.extend({
911
710
 
912
- this.$el.addClass(errClass);
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
- if (this.$error) {
915
- this.$error.html(msg);
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
- * Clear the error state and reset the help message
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
- this.$el.removeClass(errClass);
733
+ //Override defaults
734
+ this.template = options.template || schema.template || this.constructor.template;
735
+ this.errorClassName = options.errorClassName || this.constructor.errorClassName;
928
736
 
929
- // some fields (e.g., Hidden), may not have a help el
930
- if (this.$error) {
931
- this.$error.empty();
932
- } else if (this.$help) {
933
- this.$help.empty();
737
+ //Create editor
738
+ this.editor = this.createEditor();
739
+ },
934
740
 
935
- //Reset help text if available
936
- var helpMsg = this.schema.help;
937
- if (helpMsg) this.$help.html(helpMsg);
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
- * Update the model with the new value from the editor
943
- */
944
- commit: function() {
945
- return this.editor.commit();
946
- },
751
+ //Set defaults
752
+ schema = _.extend({
753
+ type: 'Text',
754
+ title: this.createTitle()
755
+ }, schema);
947
756
 
948
- /**
949
- * Get the value from the editor
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
- * Set/change the value of the editor
959
- *
960
- * @param {Mixed} value
961
- */
962
- setValue: function(value) {
963
- this.editor.setValue(value);
964
- },
760
+ return schema;
761
+ },
965
762
 
966
- focus: function() {
967
- this.editor.focus();
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
- blur: function() {
971
- this.editor.blur();
972
- },
775
+ var constructorFn = this.schema.type;
776
+
777
+ return new constructorFn(options);
778
+ },
973
779
 
974
- /**
975
- * Remove the field and editor views
976
- */
977
- remove: function() {
978
- this.editor.remove();
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
- Backbone.View.prototype.remove.call(this);
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
- //EDITORS
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
- Form.editors = (function() {
811
+ //Add spaces
812
+ str = str.replace(/([A-Z])/g, ' $1');
993
813
 
994
- var helpers = Form.helpers;
814
+ //Uppercase first character
815
+ str = str.replace(/^./, function(str) { return str.toUpperCase(); });
995
816
 
996
- var editors = {};
817
+ return str;
818
+ },
997
819
 
998
820
  /**
999
- * Base editor (interface). To be extended, not used directly
821
+ * Returns the data to be passed to the template
1000
822
  *
1001
- * @param {Object} Options
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
- editors.Base = Backbone.View.extend({
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
- defaultValue: null,
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
- hasFocus: false,
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
- initialize: function(options) {
1015
- var options = options || {};
852
+ //Render field
853
+ var $field = $($.trim(this.template(_.result(this, 'templateData'))));
1016
854
 
1017
- if (options.model) {
1018
- if (!options.key) throw "Missing option: 'key'";
855
+ if (schema.fieldClass) $field.addClass(schema.fieldClass);
856
+ if (schema.fieldAttrs) $field.attr(schema.fieldAttrs);
1019
857
 
1020
- this.model = options.model;
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
- this.value = this.model.get(options.key);
1023
- }
1024
- else if (options.value) {
1025
- this.value = options.value;
1026
- }
863
+ if (_.isUndefined(selection)) return;
1027
864
 
1028
- if (this.value === undefined) this.value = this.defaultValue;
865
+ $container.append(editor.render().el);
866
+ });
1029
867
 
1030
- this.key = options.key;
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
- //Main attributes
1036
- this.$el.attr('name', this.getName());
870
+ return this;
871
+ },
1037
872
 
1038
- //Add custom CSS class names
1039
- if (this.schema.editorClass) this.$el.addClass(this.schema.editorClass);
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
- //Add custom attributes
1042
- if (this.schema.editorAttrs) this.$el.attr(this.schema.editorAttrs);
1043
- },
881
+ if (error) {
882
+ this.setError(error.message);
883
+ } else {
884
+ this.clearError();
885
+ }
1044
886
 
1045
- getValue: function() {
1046
- throw 'Not implemented. Extend and override this method.';
1047
- },
887
+ return error;
888
+ },
1048
889
 
1049
- setValue: function() {
1050
- throw 'Not implemented. Extend and override this method.';
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
- focus: function() {
1054
- throw 'Not implemented. Extend and override this method.';
1055
- },
899
+ //Add error CSS class
900
+ this.$el.addClass(this.errorClassName);
1056
901
 
1057
- blur: function() {
1058
- throw 'Not implemented. Extend and override this method.';
1059
- },
902
+ //Set error message
903
+ this.$('[data-error]').html(msg);
904
+ },
1060
905
 
1061
- /**
1062
- * Get the value for the form input 'name' attribute
1063
- *
1064
- * @return {String}
1065
- *
1066
- * @api private
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
- * Update the model with the current value
1077
- * NOTE: The method is defined on the editors so that they can be used independently of fields
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
- if (error) return error;
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
- * Check validity
1095
- * NOTE: The method is defined on the editors so that they can be used independently of fields
1096
- *
1097
- * @return {String}
1098
- */
1099
- validate: function() {
1100
- var $el = this.$el,
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
- return error;
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
- trigger: function(event) {
1121
- if (event === 'focus') {
1122
- this.hasFocus = true;
1123
- }
1124
- else if (event === 'blur') {
1125
- this.hasFocus = false;
1126
- }
951
+ /**
952
+ * Remove focus from the editor
953
+ */
954
+ blur: function() {
955
+ this.editor.blur();
956
+ },
1127
957
 
1128
- return Backbone.View.prototype.trigger.apply(this, arguments);
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
- initialize: function(options) {
1162
- editors.Base.prototype.initialize.call(this, options);
964
+ Backbone.View.prototype.remove.call(this);
965
+ }
1163
966
 
1164
- var schema = this.schema;
967
+ }, {
968
+ //STATICS
1165
969
 
1166
- //Allow customising text type (email, phone etc.) for HTML5 browsers
1167
- var type = 'text';
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
- if (schema && schema.editorAttrs && schema.editorAttrs.type) type = schema.editorAttrs.type;
1170
- if (schema && schema.dataType) type = schema.dataType;
981
+ /**
982
+ * CSS class name added to the field when there is a validation error
983
+ */
984
+ errorClassName: 'error'
1171
985
 
1172
- this.$el.attr('type', type);
1173
- },
986
+ });
1174
987
 
1175
- /**
1176
- * Adds the editor to the DOM
1177
- */
1178
- render: function() {
1179
- this.setValue(this.value);
1180
988
 
1181
- return this;
1182
- },
989
+ //==================================================================================================
990
+ //NESTEDFIELD
991
+ //==================================================================================================
1183
992
 
1184
- determineChange: function(event) {
1185
- var currentValue = this.$el.val();
1186
- var changed = (currentValue !== this.previousValue);
993
+ Form.NestedField = Form.Field.extend({
1187
994
 
1188
- if (changed) {
1189
- this.previousValue = currentValue;
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
- this.trigger('change', this);
1192
- }
1193
- },
1005
+ });
1194
1006
 
1195
- /**
1196
- * Returns the current editor value
1197
- * @return {String}
1198
- */
1199
- getValue: function() {
1200
- return this.$el.val();
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
- focus: function() {
1212
- if (this.hasFocus) return;
1023
+ hasFocus: false,
1213
1024
 
1214
- this.$el.focus();
1215
- },
1025
+ initialize: function(options) {
1026
+ var options = options || {};
1216
1027
 
1217
- blur: function() {
1218
- if (!this.hasFocus) return;
1028
+ //Set initial value
1029
+ if (options.model) {
1030
+ if (!options.key) throw "Missing option: 'key'";
1219
1031
 
1220
- this.$el.blur();
1221
- },
1032
+ this.model = options.model;
1222
1033
 
1223
- select: function() {
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
- * NUMBER
1232
- * Normal text input that only allows a number. Letters etc. are not entered
1057
+ * Get the value for the form input 'name' attribute
1058
+ *
1059
+ * @return {String}
1060
+ *
1061
+ * @api private
1233
1062
  */
1234
- editors.Number = editors.Text.extend({
1063
+ getName: function() {
1064
+ var key = this.key || '';
1235
1065
 
1236
- defaultValue: 0,
1066
+ //Replace periods with underscores (e.g. for when using paths)
1067
+ return key.replace(/\./g, '_');
1068
+ },
1237
1069
 
1238
- events: _.extend({}, editors.Text.prototype.events, {
1239
- 'keypress': 'onKeyPress'
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
- initialize: function(options) {
1243
- editors.Text.prototype.initialize.call(this, options);
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
- this.$el.attr('type', 'number');
1246
- this.$el.attr('step', 'any');
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
- * Check value is numeric
1251
- */
1252
- onKeyPress: function(event) {
1253
- var self = this,
1254
- delayedDetermineChange = function() {
1255
- setTimeout(function() {
1256
- self.determineChange();
1257
- }, 0);
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
- //Get the whole new value so that we can prevent things like double decimals points etc.
1267
- var newVal = this.$el.val() + String.fromCharCode(event.charCode);
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
- var numeric = /^[0-9]*\.?[0-9]*?$/.test(newVal);
1123
+ if (error) return error;
1124
+ },
1270
1125
 
1271
- if (numeric) {
1272
- delayedDetermineChange();
1273
- }
1274
- else {
1275
- event.preventDefault();
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
- getValue: function() {
1280
- var value = this.$el.val();
1148
+ return error;
1149
+ },
1281
1150
 
1282
- return value === "" ? null : parseFloat(value, 10);
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
- setValue: function(value) {
1286
- value = (function() {
1287
- if (_.isNumber(value)) return value;
1164
+ return Backbone.View.prototype.trigger.apply(this, arguments);
1165
+ },
1288
1166
 
1289
- if (_.isString(value) && value !== '') return parseFloat(value, 10);
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
- return null;
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
- if (_.isNaN(value)) value = null;
1188
+ //Functions can be used directly
1189
+ if (_.isFunction(validator)) return validator;
1295
1190
 
1296
- editors.Text.prototype.setValue.call(this, value);
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
- //PASSWORD
1303
- editors.Password = editors.Text.extend({
1212
+ defaultValue: '',
1304
1213
 
1305
- initialize: function(options) {
1306
- editors.Text.prototype.initialize.call(this, options);
1214
+ previousValue: '',
1307
1215
 
1308
- this.$el.attr('type', 'password');
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
- //TEXTAREA
1315
- editors.TextArea = editors.Text.extend({
1240
+ //Allow customising text type (email, phone etc.) for HTML5 browsers
1241
+ var type = 'text';
1316
1242
 
1317
- tagName: 'textarea'
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
- //CHECKBOX
1323
- editors.Checkbox = editors.Base.extend({
1255
+ return this;
1256
+ },
1324
1257
 
1325
- defaultValue: false,
1258
+ determineChange: function(event) {
1259
+ var currentValue = this.$el.val();
1260
+ var changed = (currentValue !== this.previousValue);
1326
1261
 
1327
- tagName: 'input',
1262
+ if (changed) {
1263
+ this.previousValue = currentValue;
1328
1264
 
1329
- events: {
1330
- 'click': function(event) {
1331
- this.trigger('change', this);
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
- initialize: function(options) {
1342
- editors.Base.prototype.initialize.call(this, options);
1269
+ /**
1270
+ * Returns the current editor value
1271
+ * @return {String}
1272
+ */
1273
+ getValue: function() {
1274
+ return this.$el.val();
1275
+ },
1343
1276
 
1344
- this.$el.attr('type', 'checkbox');
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
- * Adds the editor to the DOM
1349
- */
1350
- render: function() {
1351
- this.setValue(this.value);
1285
+ focus: function() {
1286
+ if (this.hasFocus) return;
1352
1287
 
1353
- return this;
1354
- },
1288
+ this.$el.focus();
1289
+ },
1355
1290
 
1356
- getValue: function() {
1357
- return this.$el.prop('checked');
1358
- },
1291
+ blur: function() {
1292
+ if (!this.hasFocus) return;
1359
1293
 
1360
- setValue: function(value) {
1361
- if (value) {
1362
- this.$el.prop('checked', true);
1363
- }
1364
- },
1294
+ this.$el.blur();
1295
+ },
1365
1296
 
1366
- focus: function() {
1367
- if (this.hasFocus) return;
1297
+ select: function() {
1298
+ this.$el.select();
1299
+ }
1368
1300
 
1369
- this.$el.focus();
1370
- },
1301
+ });
1371
1302
 
1372
- blur: function() {
1373
- if (!this.hasFocus) return;
1303
+ /**
1304
+ * TextArea editor
1305
+ */
1306
+ Form.editors.TextArea = Form.editors.Text.extend({
1374
1307
 
1375
- this.$el.blur();
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
- //HIDDEN
1382
- editors.Hidden = editors.Base.extend({
1324
+ initialize: function(options) {
1325
+ Form.editors.Text.prototype.initialize.call(this, options);
1383
1326
 
1384
- defaultValue: '',
1327
+ this.$el.attr('type', 'password');
1328
+ }
1385
1329
 
1386
- initialize: function(options) {
1387
- editors.Text.prototype.initialize.call(this, options);
1330
+ });
1388
1331
 
1389
- this.$el.attr('type', 'hidden');
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
- getValue: function() {
1393
- return this.value;
1394
- },
1339
+ defaultValue: 0,
1395
1340
 
1396
- setValue: function(value) {
1397
- this.value = value;
1398
- },
1341
+ events: _.extend({}, Form.editors.Text.prototype.events, {
1342
+ 'keypress': 'onKeyPress',
1343
+ 'change': 'onKeyPress'
1344
+ }),
1399
1345
 
1400
- focus: function() {
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
- blur: function() {
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
- * SELECT
1413
- *
1414
- * Renders a <select> with given options
1415
- *
1416
- * Requires an 'options' value on the schema.
1417
- * Can be an array of options, a function that calls back with the array of options, a string of HTML
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
- tagName: 'select',
1386
+ getValue: function() {
1387
+ var value = this.$el.val();
1423
1388
 
1424
- events: {
1425
- 'change': function(event) {
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
- initialize: function(options) {
1437
- editors.Base.prototype.initialize.call(this, options);
1392
+ setValue: function(value) {
1393
+ value = (function() {
1394
+ if (_.isNumber(value)) return value;
1438
1395
 
1439
- if (!this.schema || !this.schema.options) throw "Missing required 'schema.options'";
1440
- },
1396
+ if (_.isString(value) && value !== '') return parseFloat(value, 10);
1441
1397
 
1442
- render: function() {
1443
- this.setOptions(this.schema.options);
1398
+ return null;
1399
+ })();
1444
1400
 
1445
- return this;
1446
- },
1401
+ if (_.isNaN(value)) value = null;
1447
1402
 
1448
- /**
1449
- * Sets the options that populate the <select>
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
- //If a collection was passed, check if it needs fetching
1457
- if (options instanceof Backbone.Collection) {
1458
- var collection = options;
1406
+ });
1459
1407
 
1460
- //Don't do the fetch if it's already populated
1461
- if (collection.length > 0) {
1462
- this.renderOptions(options);
1463
- } else {
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
- //If a function was passed, run it to get the options
1473
- else if (_.isFunction(options)) {
1474
- options(function(result) {
1475
- self.renderOptions(result);
1476
- }, self);
1477
- }
1413
+ defaultValue: '',
1478
1414
 
1479
- //Otherwise, ready to go straight to renderOptions
1480
- else {
1481
- this.renderOptions(options);
1482
- }
1483
- },
1415
+ initialize: function(options) {
1416
+ Form.editors.Text.prototype.initialize.call(this, options);
1484
1417
 
1485
- /**
1486
- * Adds the <option> html to the DOM
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
- html = this._getOptionsHtml(options);
1421
+ focus: function() {
1496
1422
 
1497
- //Insert options
1498
- $select.html(html);
1423
+ },
1499
1424
 
1500
- //Select correct option
1501
- this.setValue(this.value);
1502
- },
1425
+ blur: function() {
1503
1426
 
1504
- _getOptionsHtml: function(options) {
1505
- var html;
1506
- //Accept string of HTML
1507
- if (_.isString(options)) {
1508
- html = options;
1509
- }
1427
+ }
1510
1428
 
1511
- //Or array
1512
- else if (_.isArray(options)) {
1513
- html = this._arrayToHtml(options);
1514
- }
1429
+ });
1515
1430
 
1516
- //Or Backbone collection
1517
- else if (options instanceof Backbone.Collection) {
1518
- html = this._collectionToHtml(options);
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
- else if (_.isFunction(options)) {
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
- return html;
1532
- },
1440
+ tagName: 'input',
1533
1441
 
1534
- getValue: function() {
1535
- return this.$el.val();
1442
+ events: {
1443
+ 'click': function(event) {
1444
+ this.trigger('change', this);
1536
1445
  },
1537
-
1538
- setValue: function(value) {
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
- focus: function() {
1543
- if (this.hasFocus) return;
1454
+ initialize: function(options) {
1455
+ Form.editors.Base.prototype.initialize.call(this, options);
1544
1456
 
1545
- this.$el.focus();
1546
- },
1457
+ this.$el.attr('type', 'checkbox');
1458
+ },
1547
1459
 
1548
- blur: function() {
1549
- if (!this.hasFocus) return;
1460
+ /**
1461
+ * Adds the editor to the DOM
1462
+ */
1463
+ render: function() {
1464
+ this.setValue(this.value);
1550
1465
 
1551
- this.$el.blur();
1552
- },
1466
+ return this;
1467
+ },
1553
1468
 
1554
- /**
1555
- * Transforms a collection into HTML ready to use in the renderOptions method
1556
- * @param {Backbone.Collection}
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
- //Now convert to HTML
1567
- var html = this._arrayToHtml(array);
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
- return html;
1570
- },
1481
+ focus: function() {
1482
+ if (this.hasFocus) return;
1571
1483
 
1572
- /**
1573
- * Create the <option> HTML
1574
- * @param {Array} Options as a simple array e.g. ['option1', 'option2']
1575
- * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
1576
- * @return {String} HTML
1577
- */
1578
- _arrayToHtml: function(array) {
1579
- var html = [];
1580
-
1581
- //Generate HTML
1582
- _.each(array, function(option) {
1583
- if (_.isObject(option)) {
1584
- if (option.group) {
1585
- html.push('<optgroup label="'+option.group+'">');
1586
- html.push(this._getOptionsHtml(option.options))
1587
- html.push('</optgroup>');
1588
- } else {
1589
- var val = (option.val || option.val === 0) ? option.val : '';
1590
- html.push('<option value="'+val+'">'+option.label+'</option>');
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
- return html.join('');
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
- * RADIO
1533
+ * Sets the options that populate the <select>
1607
1534
  *
1608
- * Renders a <ul> with given options represented as <li> objects containing radio buttons
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
- editors.Radio = editors.Select.extend({
1537
+ setOptions: function(options) {
1538
+ var self = this;
1615
1539
 
1616
- tagName: 'ul',
1617
- className: 'bbf-radio',
1540
+ //If a collection was passed, check if it needs fetching
1541
+ if (options instanceof Backbone.Collection) {
1542
+ var collection = options;
1618
1543
 
1619
- events: {
1620
- 'change input[type=radio]': function() {
1621
- this.trigger('change', this);
1622
- },
1623
- 'focus input[type=radio]': function() {
1624
- if (this.hasFocus) return;
1625
- this.trigger('focus', this);
1626
- },
1627
- 'blur input[type=radio]': function() {
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
- getValue: function() {
1638
- return this.$('input[type=radio]:checked').val();
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
- setValue: function(value) {
1642
- this.$('input[type=radio]').val([value]);
1643
- },
1563
+ //Otherwise, ready to go straight to renderOptions
1564
+ else {
1565
+ this.renderOptions(options);
1566
+ }
1567
+ },
1644
1568
 
1645
- focus: function() {
1646
- if (this.hasFocus) return;
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
- var checked = this.$('input[type=radio]:checked');
1649
- if (checked[0]) {
1650
- checked.focus();
1651
- return;
1652
- }
1579
+ html = this._getOptionsHtml(options);
1653
1580
 
1654
- this.$('input[type=radio]').first().focus();
1655
- },
1581
+ //Insert options
1582
+ $select.html(html);
1656
1583
 
1657
- blur: function() {
1658
- if (!this.hasFocus) return;
1584
+ //Select correct option
1585
+ this.setValue(this.value);
1586
+ },
1659
1587
 
1660
- this.$('input[type=radio]:focus').blur();
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
- * Create the radio list HTML
1665
- * @param {Array} Options as a simple array e.g. ['option1', 'option2']
1666
- * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
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
- _.each(array, function(option, index) {
1674
- var itemHtml = '<li>';
1675
- if (_.isObject(option)) {
1676
- var val = (option.val || option.val === 0) ? option.val : '';
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
- return html.join('');
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
- * CHECKBOXES
1697
- * Renders a <ul> with given options represented as <li> objects containing checkboxes
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
- editors.Checkboxes = editors.Select.extend({
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
- tagName: 'ul',
1706
- className: 'bbf-checkboxes',
1650
+ //Now convert to HTML
1651
+ var html = this._arrayToHtml(array);
1707
1652
 
1708
- events: {
1709
- 'click input[type=checkbox]': function() {
1710
- this.trigger('change', this);
1711
- },
1712
- 'focus input[type=checkbox]': function() {
1713
- if (this.hasFocus) return;
1714
- this.trigger('focus', this);
1715
- },
1716
- 'blur input[type=checkbox]': function() {
1717
- if (!this.hasFocus) return;
1718
- var self = this;
1719
- setTimeout(function() {
1720
- if (self.$('input[type=checkbox]:focus')[0]) return;
1721
- self.trigger('blur', self);
1722
- }, 0);
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
- getValue: function() {
1727
- var values = [];
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
- setValue: function(values) {
1735
- if (!_.isArray(values)) values = [values];
1736
- this.$('input[type=checkbox]').val(values);
1737
- },
1685
+ });
1738
1686
 
1739
- focus: function() {
1740
- if (this.hasFocus) return;
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
- this.$('input[type=checkbox]').first().focus();
1743
- },
1698
+ tagName: 'ul',
1744
1699
 
1745
- blur: function() {
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
- this.$('input[type=checkbox]:focus').blur();
1749
- },
1718
+ getValue: function() {
1719
+ return this.$('input[type=radio]:checked').val();
1720
+ },
1750
1721
 
1751
- /**
1752
- * Create the checkbox list HTML
1753
- * @param {Array} Options as a simple array e.g. ['option1', 'option2']
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
- _.each(array, function(option, index) {
1762
- var itemHtml = '<li>';
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
- return html.join('');
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
- * OBJECT
1785
- *
1786
- * Creates a child form. For editing Javascript objects
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
- editors.Object = editors.Base.extend({
1793
- //Prevent error classes being set on the main control; they are internally on the individual fields
1794
- hasNestedForm: true,
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
- className: 'bbf-object',
1772
+ });
1797
1773
 
1798
- initialize: function(options) {
1799
- //Set default value for the instance so it's not a shared object
1800
- this.value = {};
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
- //Init
1803
- editors.Base.prototype.initialize.call(this, options);
1785
+ tagName: 'ul',
1804
1786
 
1805
- //Check required options
1806
- if (!this.schema.subSchema) throw new Error("Missing required 'schema.subSchema' option for Object editor");
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
- render: function() {
1810
- //Create the nested form
1811
- this.form = new Form({
1812
- schema: this.schema.subSchema,
1813
- data: this.value,
1814
- idPrefix: this.id + '_',
1815
- fieldTemplate: 'nestedField'
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
- this._observeFormEvents();
1813
+ setValue: function(values) {
1814
+ if (!_.isArray(values)) values = [values];
1815
+ this.$('input[type=checkbox]').val(values);
1816
+ },
1819
1817
 
1820
- this.$el.html(this.form.render().el);
1818
+ focus: function() {
1819
+ if (this.hasFocus) return;
1821
1820
 
1822
- if (this.hasFocus) this.trigger('blur', this);
1821
+ this.$('input[type=checkbox]').first().focus();
1822
+ },
1823
1823
 
1824
- return this;
1825
- },
1824
+ blur: function() {
1825
+ if (!this.hasFocus) return;
1826
1826
 
1827
- getValue: function() {
1828
- if (this.form) return this.form.getValue();
1827
+ this.$('input[type=checkbox]:focus').blur();
1828
+ },
1829
1829
 
1830
- return this.value;
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
- setValue: function(value) {
1834
- this.value = value;
1855
+ return html.join('');
1856
+ }
1835
1857
 
1836
- this.render();
1837
- },
1858
+ });
1838
1859
 
1839
- focus: function() {
1840
- if (this.hasFocus) return;
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
- this.form.focus();
1843
- },
1898
+ this._observeFormEvents();
1844
1899
 
1845
- blur: function() {
1846
- if (!this.hasFocus) return;
1900
+ this.$el.html(this.nestedForm.render().el);
1847
1901
 
1848
- this.form.blur();
1849
- },
1902
+ if (this.hasFocus) this.trigger('blur', this);
1850
1903
 
1851
- remove: function() {
1852
- this.form.remove();
1904
+ return this;
1905
+ },
1853
1906
 
1854
- Backbone.View.prototype.remove.call(this);
1855
- },
1907
+ getValue: function() {
1908
+ if (this.nestedForm) return this.nestedForm.getValue();
1856
1909
 
1857
- validate: function() {
1858
- return this.form.validate();
1859
- },
1910
+ return this.value;
1911
+ },
1860
1912
 
1861
- _observeFormEvents: function() {
1862
- this.form.on('all', function() {
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
- this.trigger.apply(this, args);
1869
- }, this);
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
- * NESTED MODEL
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
- if (!options.schema.model)
1889
- throw 'Missing required "schema.model" option for NestedModel editor';
1890
- },
1931
+ remove: function() {
1932
+ this.nestedForm.remove();
1891
1933
 
1892
- render: function() {
1893
- var data = this.value || {},
1894
- key = this.key,
1895
- nestedModel = this.schema.model;
1934
+ Backbone.View.prototype.remove.call(this);
1935
+ },
1896
1936
 
1897
- //Wrap the data in a model if it isn't already a model instance
1898
- var modelInstance = (data.constructor === nestedModel) ? data : new nestedModel(data);
1937
+ validate: function() {
1938
+ return this.nestedForm.validate();
1939
+ },
1899
1940
 
1900
- this.form = new Form({
1901
- model: modelInstance,
1902
- idPrefix: this.id + '_',
1903
- fieldTemplate: 'nestedField'
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
- this._observeFormEvents();
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
- //Render form
1909
- this.$el.html(this.form.render().el);
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
- if (this.hasFocus) this.trigger('blur', this);
1976
+ var data = this.value || {},
1977
+ key = this.key,
1978
+ nestedModel = this.schema.model;
1912
1979
 
1913
- return this;
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
- * Update the embedded model, checking for nested validation errors and pass them up
1918
- * Then update the main model if all OK
1919
- *
1920
- * @return {Error|null} Validation error or null
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
- return editors.Object.prototype.commit.call(this);
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
- * DATE
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
- * Config options (if not set, defaults to options stored on the main Date class)
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
- editors.Date = editors.Base.extend({
2005
+ commit: function() {
2006
+ var error = this.nestedForm.commit();
2007
+ if (error) {
2008
+ this.$el.addClass('error');
2009
+ return error;
2010
+ }
1948
2011
 
1949
- events: {
1950
- 'change select': function() {
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
- initialize: function(options) {
1969
- options = options || {};
2015
+ });
1970
2016
 
1971
- editors.Base.prototype.initialize.call(this, options);
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
- var Self = editors.Date,
1974
- today = new Date();
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
- //Option defaults
1977
- this.options = _.extend({
1978
- monthNames: Self.monthNames,
1979
- showMonthNames: Self.showMonthNames
1980
- }, options);
2049
+ initialize: function(options) {
2050
+ options = options || {};
1981
2051
 
1982
- //Schema defaults
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
- //Cast to Date
1989
- if (this.value && !_.isDate(this.value)) {
1990
- this.value = new Date(this.value);
1991
- }
2054
+ var Self = Form.editors.Date,
2055
+ today = new Date();
1992
2056
 
1993
- //Set default date
1994
- if (!this.value) {
1995
- var date = new Date();
1996
- date.setSeconds(0);
1997
- date.setMilliseconds(0);
2057
+ //Option defaults
2058
+ this.options = _.extend({
2059
+ monthNames: Self.monthNames,
2060
+ showMonthNames: Self.showMonthNames
2061
+ }, options);
1998
2062
 
1999
- this.value = date;
2000
- }
2001
- },
2063
+ //Schema defaults
2064
+ this.schema = _.extend({
2065
+ yearStart: today.getFullYear() - 100,
2066
+ yearEnd: today.getFullYear()
2067
+ }, options.schema || {});
2002
2068
 
2003
- render: function() {
2004
- var options = this.options,
2005
- schema = this.schema;
2069
+ //Cast to Date
2070
+ if (this.value && !_.isDate(this.value)) {
2071
+ this.value = new Date(this.value);
2072
+ }
2006
2073
 
2007
- var datesOptions = _.map(_.range(1, 32), function(date) {
2008
- return '<option value="'+date+'">' + date + '</option>';
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
- var monthsOptions = _.map(_.range(0, 12), function(month) {
2012
- var value = options.showMonthNames ? options.monthNames[month] : (month + 1);
2013
- return '<option value="'+month+'">' + value + '</option>';
2014
- });
2080
+ this.value = date;
2081
+ }
2015
2082
 
2016
- var yearRange = schema.yearStart < schema.yearEnd ?
2017
- _.range(schema.yearStart, schema.yearEnd + 1) :
2018
- _.range(schema.yearStart, schema.yearEnd - 1, -1);
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
- //Render the selects
2024
- var $el = Form.helpers.parseHTML(Form.templates.date({
2025
- dates: datesOptions.join(''),
2026
- months: monthsOptions.join(''),
2027
- years: yearsOptions.join('')
2028
- }));
2087
+ render: function() {
2088
+ var options = this.options,
2089
+ schema = this.schema;
2029
2090
 
2030
- //Store references to selects
2031
- this.$date = $el.find('select[data-type="date"]');
2032
- this.$month = $el.find('select[data-type="month"]');
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
- //Create the hidden field to store values in case POSTed to server
2036
- this.$hidden = $('<input type="hidden" name="'+this.key+'" />');
2037
- $el.append(this.$hidden);
2095
+ var monthsOptions = _.map(_.range(0, 12), function(month) {
2096
+ var value = (options.showMonthNames)
2097
+ ? options.monthNames[month]
2098
+ : (month + 1);
2038
2099
 
2039
- //Set value on this and hidden field
2040
- this.setValue(this.value);
2100
+ return '<option value="'+month+'">' + value + '</option>';
2101
+ });
2041
2102
 
2042
- //Remove the wrapper tag
2043
- this.setElement($el);
2044
- this.$el.attr('id', this.id);
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
- if (this.hasFocus) this.trigger('blur', this);
2107
+ var yearsOptions = _.map(yearRange, function(year) {
2108
+ return '<option value="'+year+'">' + year + '</option>';
2109
+ });
2047
2110
 
2048
- return this;
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
- * @return {Date} Selected date
2053
- */
2054
- getValue: function() {
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
- if (!year || !month || !date) return null;
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
- return new Date(year, month, date);
2062
- },
2127
+ //Set value on this and hidden field
2128
+ this.setValue(this.value);
2063
2129
 
2064
- /**
2065
- * @param {Date} date
2066
- */
2067
- setValue: function(date) {
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
- this.updateHidden();
2073
- },
2135
+ if (this.hasFocus) this.trigger('blur', this);
2074
2136
 
2075
- focus: function() {
2076
- if (this.hasFocus) return;
2137
+ return this;
2138
+ },
2077
2139
 
2078
- this.$('select').first().focus();
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
- blur: function() {
2082
- if (!this.hasFocus) return;
2148
+ if (!year || !month || !date) return null;
2083
2149
 
2084
- this.$('select:focus').blur();
2085
- },
2150
+ return new Date(year, month, date);
2151
+ },
2086
2152
 
2087
- /**
2088
- * Update the hidden input which is maintained for when submitting a form
2089
- * via a normal browser POST
2090
- */
2091
- updateHidden: function() {
2092
- var val = this.getValue();
2093
- if (_.isDate(val)) val = val.toISOString();
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
- this.$hidden.val(val);
2096
- }
2161
+ this.updateHidden();
2162
+ },
2097
2163
 
2098
- }, {
2099
- //STATICS
2164
+ focus: function() {
2165
+ if (this.hasFocus) return;
2100
2166
 
2101
- //Whether to show month names instead of numbers
2102
- showMonthNames: true,
2167
+ this.$('select').first().focus();
2168
+ },
2103
2169
 
2104
- //Month names to use if showMonthNames is true
2105
- //Replace for localisation, e.g. Form.editors.Date.monthNames = ['Janvier', 'Fevrier'...]
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
- * DATETIME
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
- editors.DateTime = editors.Base.extend({
2180
+ updateHidden: function() {
2181
+ var val = this.getValue();
2117
2182
 
2118
- events: {
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
- initialize: function(options) {
2138
- options = options || {};
2185
+ this.$hidden.val(val);
2186
+ }
2139
2187
 
2140
- editors.Base.prototype.initialize.call(this, options);
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
- //Option defaults
2143
- this.options = _.extend({
2144
- DateEditor: editors.DateTime.DateEditor
2145
- }, options);
2198
+ //Whether to show month names instead of numbers
2199
+ showMonthNames: true,
2146
2200
 
2147
- //Schema defaults
2148
- this.schema = _.extend({
2149
- minsInterval: 15
2150
- }, options.schema || {});
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
- //Create embedded date editor
2153
- this.dateEditor = new this.options.DateEditor(options);
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
- this.value = this.dateEditor.value;
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
- render: function() {
2159
- function pad(n) {
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
- //Render time selects
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
- //Include the date editor
2182
- $el.find('.bbf-tmp').replaceWith(this.dateEditor.render().el);
2238
+ //Option defaults
2239
+ this.options = _.extend({
2240
+ DateEditor: Form.editors.DateTime.DateEditor
2241
+ }, options);
2183
2242
 
2184
- //Store references to selects
2185
- this.$hour = $el.find('select[data-type="hour"]');
2186
- this.$min = $el.find('select[data-type="min"]');
2243
+ //Schema defaults
2244
+ this.schema = _.extend({
2245
+ minsInterval: 15
2246
+ }, options.schema || {});
2187
2247
 
2188
- //Get the hidden date field to store values in case POSTed to server
2189
- this.$hidden = $el.find('input[type="hidden"]');
2248
+ //Create embedded date editor
2249
+ this.dateEditor = new this.options.DateEditor(options);
2190
2250
 
2191
- //Set time
2192
- this.setValue(this.value);
2251
+ this.value = this.dateEditor.value;
2193
2252
 
2194
- this.setElement($el);
2195
- this.$el.attr('id', this.id);
2253
+ //Template
2254
+ this.template = options.template || this.constructor.template;
2255
+ },
2196
2256
 
2197
- if (this.hasFocus) this.trigger('blur', this);
2257
+ render: function() {
2258
+ function pad(n) {
2259
+ return n < 10 ? '0' + n : n;
2260
+ }
2198
2261
 
2199
- return this;
2200
- },
2262
+ var schema = this.schema;
2201
2263
 
2202
- /**
2203
- * @return {Date} Selected datetime
2204
- */
2205
- getValue: function() {
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
- var hour = this.$hour.val(),
2209
- min = this.$min.val();
2269
+ var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) {
2270
+ return '<option value="'+min+'">' + pad(min) + '</option>';
2271
+ });
2210
2272
 
2211
- if (!date || !hour || !min) return null;
2273
+ //Render time selects
2274
+ var $el = $($.trim(this.template({
2275
+ hours: hoursOptions.join(),
2276
+ mins: minsOptions.join()
2277
+ })));
2212
2278
 
2213
- date.setHours(hour);
2214
- date.setMinutes(min);
2279
+ //Include the date editor
2280
+ $el.find('[data-date]').append(this.dateEditor.render().el);
2215
2281
 
2216
- return date;
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
- setValue: function(date) {
2220
- if (!_.isDate(date)) date = new Date(date);
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
- this.dateEditor.setValue(date);
2289
+ //Set time
2290
+ this.setValue(this.value);
2223
2291
 
2224
- this.$hour.val(date.getHours());
2225
- this.$min.val(date.getMinutes());
2292
+ this.setElement($el);
2293
+ this.$el.attr('id', this.id);
2294
+ this.$el.attr('name', this.getName());
2226
2295
 
2227
- this.updateHidden();
2228
- },
2296
+ if (this.hasFocus) this.trigger('blur', this);
2229
2297
 
2230
- focus: function() {
2231
- if (this.hasFocus) return;
2298
+ return this;
2299
+ },
2232
2300
 
2233
- this.$('select').first().focus();
2234
- },
2301
+ /**
2302
+ * @return {Date} Selected datetime
2303
+ */
2304
+ getValue: function() {
2305
+ var date = this.dateEditor.getValue();
2235
2306
 
2236
- blur: function() {
2237
- if (!this.hasFocus) return;
2307
+ var hour = this.$hour.val(),
2308
+ min = this.$min.val();
2238
2309
 
2239
- this.$('select:focus').blur();
2240
- },
2310
+ if (!date || !hour || !min) return null;
2241
2311
 
2242
- /**
2243
- * Update the hidden input which is maintained for when submitting a form
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
- this.$hidden.val(val);
2251
- },
2315
+ return date;
2316
+ },
2252
2317
 
2253
- /**
2254
- * Remove the Date editor before removing self
2255
- */
2256
- remove: function() {
2257
- this.dateEditor.remove();
2318
+ /**
2319
+ * @param {Date}
2320
+ */
2321
+ setValue: function(date) {
2322
+ if (!_.isDate(date)) date = new Date(date);
2258
2323
 
2259
- editors.Base.prototype.remove.call(this);
2260
- }
2324
+ this.dateEditor.setValue(date);
2261
2325
 
2262
- }, {
2263
- //STATICS
2326
+ this.$hour.val(date.getHours());
2327
+ this.$min.val(date.getMinutes());
2264
2328
 
2265
- //The date editor to use (constructor function, not instance)
2266
- DateEditor: editors.Date
2267
- });
2329
+ this.updateHidden();
2330
+ },
2268
2331
 
2269
- return editors;
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
- //SETUP
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
- Form.templates = {};
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
- //DEFAULT TEMPLATES
2284
- Form.setTemplates({
2285
-
2286
- //HTML
2287
- form: '\
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">&times;</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
- 'list.Modal': '\
2348
- <div class="bbf-list-modal">\
2349
- {{summary}}\
2350
- </div>\
2351
- '
2352
- }, {
2361
+ Form.editors.Base.prototype.remove.call(this);
2362
+ }
2353
2363
 
2354
- //CLASSNAMES
2355
- error: 'bbf-error'
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.11.0';
2382
+ Form.VERSION = '0.12.0';
2363
2383
 
2364
2384
 
2365
2385
  //Exports