rails-backbone-forms 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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