rails-backbone-forms 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  module Rails
2
2
  module Backbone
3
3
  module Forms
4
- VERSION = '0.10.1'
4
+ VERSION = '0.11.0'
5
5
  end
6
6
  end
7
7
  end
@@ -30,7 +30,7 @@
30
30
  <h3>{{title}}</h3>\
31
31
  </div>\
32
32
  <% } %>\
33
- <div class="modal-body"><p>{{content}}</p></div>\
33
+ <div class="modal-body">{{content}}</div>\
34
34
  <div class="modal-footer">\
35
35
  <% if (allowCancel) { %>\
36
36
  <% if (cancelText) { %>\
@@ -54,19 +54,32 @@
54
54
  event.preventDefault();
55
55
 
56
56
  this.trigger('cancel');
57
- this.close();
57
+
58
+ if (this.options.content && this.options.content.trigger) {
59
+ this.options.content.trigger('cancel', this);
60
+ }
58
61
  },
59
62
  'click .cancel': function(event) {
60
63
  event.preventDefault();
61
64
 
62
65
  this.trigger('cancel');
63
- this.close();
66
+
67
+ if (this.options.content && this.options.content.trigger) {
68
+ this.options.content.trigger('cancel', this);
69
+ }
64
70
  },
65
71
  'click .ok': function(event) {
66
72
  event.preventDefault();
67
73
 
68
74
  this.trigger('ok');
69
- this.close();
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
+ }
70
83
  }
71
84
  },
72
85
 
@@ -89,6 +102,8 @@
89
102
  this.options = _.extend({
90
103
  title: null,
91
104
  okText: 'OK',
105
+ focusOk: true,
106
+ okCloses: true,
92
107
  cancelText: 'Cancel',
93
108
  allowCancel: true,
94
109
  escape: true,
@@ -110,11 +125,12 @@
110
125
  //Create the modal container
111
126
  $el.html(options.template(options));
112
127
 
113
- var $content = this.$content = $el.find('.modal-body p')
128
+ var $content = this.$content = $el.find('.modal-body')
114
129
 
115
130
  //Insert the main content if it's a view
116
131
  if (content.$el) {
117
- $el.find('.modal-body p').html(content.render().$el);
132
+ content.render();
133
+ $el.find('.modal-body').html(content.$el);
118
134
  }
119
135
 
120
136
  if (options.animate) $el.addClass('fade');
@@ -126,22 +142,30 @@
126
142
 
127
143
  /**
128
144
  * Renders and shows the modal
145
+ *
146
+ * @param {Function} [cb] Optional callback that runs only when OK is pressed.
129
147
  */
130
- open: function() {
148
+ open: function(cb) {
131
149
  if (!this.isRendered) this.render();
132
150
 
133
151
  var self = this,
134
152
  $el = this.$el;
135
153
 
136
154
  //Create it
137
- $el.modal({
155
+ $el.modal(_.extend({
138
156
  keyboard: this.options.allowCancel,
139
157
  backdrop: this.options.allowCancel ? true : 'static'
140
- });
158
+ }, this.options.modalOptions));
141
159
 
142
160
  //Focus OK button
143
161
  $el.one('shown', function() {
144
- $el.find('.btn.ok').focus();
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
+ }
145
169
 
146
170
  self.trigger('shown');
147
171
  });
@@ -149,13 +173,40 @@
149
173
  //Adjust the modal and backdrop z-index; for dealing with multiple modals
150
174
  var numModals = Modal.count,
151
175
  $backdrop = $('.modal-backdrop:eq('+numModals+')'),
152
- backdropIndex = $backdrop.css('z-index'),
153
- elIndex = $backdrop.css('z-index');
176
+ backdropIndex = parseInt($backdrop.css('z-index'),10),
177
+ elIndex = parseInt($backdrop.css('z-index'), 10);
154
178
 
155
179
  $backdrop.css('z-index', backdropIndex + numModals);
156
180
  this.$el.css('z-index', elIndex + numModals);
157
181
 
182
+ if (this.options.allowCancel) {
183
+ $backdrop.one('click', function() {
184
+ if (self.options.content && self.options.content.trigger) {
185
+ self.options.content.trigger('cancel', self);
186
+ }
187
+
188
+ self.trigger('cancel');
189
+ });
190
+
191
+ $(document).one('keyup.dismiss.modal', function (e) {
192
+ 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
+ });
198
+ }
199
+
200
+ this.on('cancel', function() {
201
+ self.close();
202
+ });
203
+
158
204
  Modal.count++;
205
+
206
+ //Run callback on OK if provided
207
+ if (cb) {
208
+ self.on('ok', cb);
209
+ }
159
210
 
160
211
  return this;
161
212
  },
@@ -173,14 +224,22 @@
173
224
  return;
174
225
  }
175
226
 
176
- $el.modal('hide');
177
-
178
- $el.one('hidden', function() {
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
+ }
179
232
  self.remove();
180
233
 
234
+ if (self.options.content && self.options.content.trigger) {
235
+ self.options.content.trigger('hidden', self);
236
+ }
237
+
181
238
  self.trigger('hidden');
182
239
  });
183
240
 
241
+ $el.modal('hide');
242
+
184
243
  Modal.count--;
185
244
  },
186
245
 
@@ -201,14 +260,14 @@
201
260
 
202
261
  //EXPORTS
203
262
  //CommonJS
204
- if (typeof require == 'function' && module && exports) {
263
+ if (typeof require == 'function' && typeof module !== 'undefined' && exports) {
205
264
  module.exports = Modal;
206
265
  }
207
266
 
208
267
  //AMD / RequireJS
209
268
  if (typeof define === 'function' && define.amd) {
210
269
  return define(function() {
211
- return Modal;
270
+ Backbone.BootstrapModal = Modal;
212
271
  })
213
272
  }
214
273
 
@@ -0,0 +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,7 +1,7 @@
1
1
  /**
2
- * Backbone Forms v0.10.1
2
+ * Backbone Forms v0.11.0
3
3
  *
4
- * Copyright (c) 2012 Charles Davison, Pow Media Ltd
4
+ * Copyright (c) 2013 Charles Davison, Pow Media Ltd
5
5
  *
6
6
  * License and more information at:
7
7
  * http://github.com/powmedia/backbone-forms
@@ -29,11 +29,11 @@
29
29
  //==================================================================================================
30
30
  //FORM
31
31
  //==================================================================================================
32
-
32
+
33
33
  var Form = (function() {
34
34
 
35
35
  return Backbone.View.extend({
36
-
36
+
37
37
  hasFocus: false,
38
38
 
39
39
  /**
@@ -43,7 +43,7 @@ var Form = (function() {
43
43
  * @param {Model} [options.model] Model the form relates to. Required if options.data is not set
44
44
  * @param {Object} [options.data] Date to populate the form. Required if options.model is not set
45
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'] }]
46
+ * @param {String[]|Object[]} [options.fieldsets] How to divide the fields up by section. E.g. [{ legend: 'Title', fields: ['field1', 'field2'] }]
47
47
  * @param {String} [options.idPrefix] Prefix for editor IDs. By default, the model's CID is used.
48
48
  * @param {String} [options.template] Form template key/name
49
49
  * @param {String} [options.fieldsetTemplate] Fieldset template key/name
@@ -51,19 +51,19 @@ var Form = (function() {
51
51
  *
52
52
  * @return {Form}
53
53
  */
54
- initialize: function(options) {
54
+ initialize: function(options) {
55
55
  //Check templates have been loaded
56
56
  if (!Form.templates.form) throw new Error('Templates not loaded');
57
57
 
58
58
  //Get the schema
59
59
  this.schema = (function() {
60
60
  if (options.schema) return options.schema;
61
-
61
+
62
62
  var model = options.model;
63
63
  if (!model) throw new Error('Could not find schema');
64
-
64
+
65
65
  if (_.isFunction(model.schema)) return model.schema();
66
-
66
+
67
67
  return model.schema;
68
68
  })();
69
69
 
@@ -80,7 +80,7 @@ var Form = (function() {
80
80
 
81
81
  options.fieldsets = [{ fields: fields }];
82
82
  }
83
-
83
+
84
84
  //Store main attributes
85
85
  this.options = options;
86
86
  this.model = options.model;
@@ -95,9 +95,9 @@ var Form = (function() {
95
95
  var self = this,
96
96
  options = this.options,
97
97
  template = Form.templates[options.template];
98
-
98
+
99
99
  //Create el from template
100
- var $form = $(template({
100
+ var $form = Form.helpers.parseHTML(template({
101
101
  fieldsets: '<b class="bbf-tmp"></b>'
102
102
  }));
103
103
 
@@ -112,7 +112,7 @@ var Form = (function() {
112
112
 
113
113
  //Set the template contents as the main element; removes the wrapper element
114
114
  this.setElement($form);
115
-
115
+
116
116
  if (this.hasFocus) this.trigger('blur', this);
117
117
 
118
118
  return this;
@@ -126,7 +126,7 @@ var Form = (function() {
126
126
  * { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
127
127
  *
128
128
  * @param {Object|Array} fieldset A fieldset definition
129
- *
129
+ *
130
130
  * @return {jQuery} The fieldset DOM element
131
131
  */
132
132
  renderFieldset: function(fieldset) {
@@ -141,7 +141,7 @@ var Form = (function() {
141
141
  }
142
142
 
143
143
  //Concatenating HTML as strings won't work so we need to insert field elements into a placeholder
144
- var $fieldset = $(template(_.extend({}, fieldset, {
144
+ var $fieldset = Form.helpers.parseHTML(template(_.extend({}, fieldset, {
145
145
  legend: '<b class="bbf-tmp-legend"></b>',
146
146
  fields: '<b class="bbf-tmp-fields"></b>'
147
147
  })));
@@ -176,17 +176,17 @@ var Form = (function() {
176
176
 
177
177
  //Render the fields with editors, apart from Hidden fields
178
178
  var fieldEl = field.render().el;
179
-
179
+
180
180
  field.editor.on('all', function(event) {
181
181
  // args = ["change", editor]
182
- args = _.toArray(arguments);
182
+ var args = _.toArray(arguments);
183
183
  args[0] = key + ':' + event;
184
184
  args.splice(1, 0, this);
185
185
  // args = ["key:change", this=form, editor]
186
186
 
187
- this.trigger.apply(this, args)
187
+ this.trigger.apply(this, args);
188
188
  }, self);
189
-
189
+
190
190
  field.editor.on('change', function() {
191
191
  this.trigger('change', self);
192
192
  }, self);
@@ -203,13 +203,13 @@ var Form = (function() {
203
203
  self.trigger('blur', self);
204
204
  }, 0);
205
205
  }, self);
206
-
207
- if (itemSchema.type != 'Hidden') {
206
+
207
+ if (itemSchema.type !== 'Hidden') {
208
208
  $fieldsContainer.append(fieldEl);
209
209
  }
210
210
  });
211
211
 
212
- $fieldsContainer = $fieldsContainer.children().unwrap()
212
+ $fieldsContainer = $fieldsContainer.children().unwrap();
213
213
 
214
214
  return $fieldset;
215
215
  },
@@ -266,24 +266,25 @@ var Form = (function() {
266
266
  //Get errors from default Backbone model validator
267
267
  if (model && model.validate) {
268
268
  var modelErrors = model.validate(this.getValue());
269
-
269
+
270
270
  if (modelErrors) {
271
271
  var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
272
-
272
+
273
273
  //If errors are not in object form then just store on the error object
274
274
  if (!isDictionary) {
275
275
  errors._others = errors._others || [];
276
276
  errors._others.push(modelErrors);
277
277
  }
278
-
278
+
279
279
  //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
280
280
  if (isDictionary) {
281
281
  _.each(modelErrors, function(val, key) {
282
282
  //Set error on field if there isn't one already
283
283
  if (self.fields[key] && !errors[key]) {
284
284
  self.fields[key].setError(val);
285
+ errors[key] = val;
285
286
  }
286
-
287
+
287
288
  else {
288
289
  //Otherwise add to '_others' key
289
290
  errors._others = errors._others || [];
@@ -302,20 +303,25 @@ var Form = (function() {
302
303
  /**
303
304
  * Update the model with all latest values.
304
305
  *
306
+ * @param {Object} [options] Options to pass to Model#set (e.g. { silent: true })
307
+ *
305
308
  * @return {Object} Validation errors
306
309
  */
307
- commit: function() {
310
+ commit: function(options) {
308
311
  //Validate
309
312
  var errors = this.validate();
310
313
  if (errors) return errors;
311
314
 
312
315
  //Commit
313
316
  var modelError;
314
- this.model.set(this.getValue(), {
317
+
318
+ var setOptions = _.extend({
315
319
  error: function(model, e) {
316
320
  modelError = e;
317
321
  }
318
- });
322
+ }, options);
323
+
324
+ this.model.set(this.getValue(), setOptions);
319
325
 
320
326
  if (modelError) return modelError;
321
327
  },
@@ -323,14 +329,14 @@ var Form = (function() {
323
329
  /**
324
330
  * Get all the field values as an object.
325
331
  * Use this method when passing data instead of objects
326
- *
332
+ *
327
333
  * @param {String} [key] Specific field value to get
328
334
  */
329
335
  getValue: function(key) {
330
336
  //Return only given key if specified
331
337
  if (key) return this.fields[key].getValue();
332
-
333
- //Otherwise return entire form
338
+
339
+ //Otherwise return entire form
334
340
  var values = {};
335
341
  _.each(this.fields, function(field) {
336
342
  values[field.key] = field.getValue();
@@ -338,22 +344,31 @@ var Form = (function() {
338
344
 
339
345
  return values;
340
346
  },
341
-
347
+
342
348
  /**
343
349
  * Update field values, referenced by key
344
- * @param {Object} data New values to set
350
+ * @param {Object|String} key New values to set, or property to set
351
+ * @param val Value to set
345
352
  */
346
- setValue: function(data) {
347
- for (var key in data) {
348
- if (_.has(this.fields, key)) {
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) {
349
364
  this.fields[key].setValue(data[key]);
350
365
  }
351
366
  }
352
367
  },
353
-
368
+
354
369
  focus: function() {
355
370
  if (this.hasFocus) return;
356
-
371
+
357
372
  var fieldset = this.options.fieldsets[0];
358
373
  if (fieldset) {
359
374
  var field;
@@ -368,12 +383,12 @@ var Form = (function() {
368
383
  }
369
384
  }
370
385
  },
371
-
386
+
372
387
  blur: function() {
373
388
  if (!this.hasFocus) return;
374
-
375
- focusedField = _.find(this.fields, function(field) { return field.editor.hasFocus; });
376
-
389
+
390
+ var focusedField = _.find(this.fields, function(field) { return field.editor.hasFocus; });
391
+
377
392
  if (focusedField) focusedField.editor.blur();
378
393
  },
379
394
 
@@ -382,23 +397,22 @@ var Form = (function() {
382
397
  */
383
398
  remove: function() {
384
399
  var fields = this.fields;
385
-
400
+
386
401
  for (var key in fields) {
387
402
  fields[key].remove();
388
403
  }
389
404
 
390
405
  Backbone.View.prototype.remove.call(this);
391
406
  },
392
-
393
-
407
+
394
408
  trigger: function(event) {
395
- if (event == 'focus') {
409
+ if (event === 'focus') {
396
410
  this.hasFocus = true;
397
411
  }
398
- else if (event == 'blur') {
412
+ else if (event === 'blur') {
399
413
  this.hasFocus = false;
400
414
  }
401
-
415
+
402
416
  return Backbone.View.prototype.trigger.apply(this, arguments);
403
417
  }
404
418
  });
@@ -429,7 +443,7 @@ Form.helpers = (function() {
429
443
  result = result[fields[i]];
430
444
  }
431
445
  return result;
432
- }
446
+ };
433
447
 
434
448
  /**
435
449
  * This function is used to transform the key from a schema into the title used in a label.
@@ -470,7 +484,7 @@ Form.helpers = (function() {
470
484
  _.templateSettings.interpolate = _interpolateBackup;
471
485
 
472
486
  return template;
473
- }
487
+ };
474
488
 
475
489
  /**
476
490
  * Helper to create a template with the {{mustache}} style tags.
@@ -480,7 +494,7 @@ Form.helpers = (function() {
480
494
  * @return {Template|String} Compiled template or the evaluated string
481
495
  */
482
496
  helpers.createTemplate = function(str, context) {
483
- var template = helpers.compileTemplate(str);
497
+ var template = helpers.compileTemplate($.trim(str));
484
498
 
485
499
  if (!context) {
486
500
  return template;
@@ -496,7 +510,7 @@ Form.helpers = (function() {
496
510
  */
497
511
  helpers.setTemplateCompiler = function(compiler) {
498
512
  helpers.compileTemplate = compiler;
499
- }
513
+ };
500
514
 
501
515
 
502
516
  /**
@@ -550,33 +564,6 @@ Form.helpers = (function() {
550
564
  return new constructorFn(options);
551
565
  };
552
566
 
553
- /**
554
- * Triggers an event that can be cancelled. Requires the user to invoke a callback. If false
555
- * is passed to the callback, the action does not run.
556
- *
557
- * NOTE: This helper uses private Backbone apis so can break when Backbone is upgraded
558
- *
559
- * @param {Mixed} Instance of Backbone model, view, collection to trigger event on
560
- * @param {String} Event name
561
- * @param {Array} Arguments to pass to the event handlers
562
- * @param {Function} Callback to run after the event handler has run.
563
- * If any of them passed false or error, this callback won't run
564
- */
565
- helpers.triggerCancellableEvent = function(subject, event, args, callback) {
566
- //Return if there are no event listeners
567
- if (!subject._callbacks || !subject._callbacks[event]) return callback();
568
-
569
- var next = subject._callbacks[event].next;
570
- if (!next) return callback();
571
-
572
- var fn = next.callback,
573
- context = next.context || this;
574
-
575
- //Add the callback that will be used when done
576
- args.push(callback);
577
-
578
- fn.apply(context, args);
579
- }
580
567
 
581
568
  /**
582
569
  * Returns a validation function based on the type defined in the schema
@@ -614,6 +601,20 @@ Form.helpers = (function() {
614
601
  };
615
602
 
616
603
 
604
+ /**
605
+ * Given an HTML string, return a jQuery-wrapped array of DOM nodes.
606
+ *
607
+ * @param {String} html
608
+ * @return {Object}
609
+ */
610
+ helpers.parseHTML = function(html) {
611
+ if ($.parseHTML !== undefined) {
612
+ return $($.parseHTML(html));
613
+ }
614
+ return $(html);
615
+ };
616
+
617
+
617
618
  return helpers;
618
619
 
619
620
  })();
@@ -633,7 +634,7 @@ Form.validators = (function() {
633
634
  email: 'Invalid email address',
634
635
  url: 'Invalid URL',
635
636
  match: 'Must match field "{{field}}"'
636
- }
637
+ };
637
638
 
638
639
  validators.required = function(options) {
639
640
  options = _.extend({
@@ -649,7 +650,7 @@ Form.validators = (function() {
649
650
  message: Form.helpers.createTemplate(options.message, options)
650
651
  };
651
652
 
652
- if (value === null || value === undefined || value === '') return err;
653
+ if (value === null || value === undefined || value === false || value === '') return err;
653
654
  };
654
655
  };
655
656
 
@@ -690,7 +691,7 @@ Form.validators = (function() {
690
691
  options = _.extend({
691
692
  type: 'url',
692
693
  message: this.errMessages.url,
693
- regexp: /^(http|https):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i
694
+ regexp: /^(http|https):\/\/(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
694
695
  }, options);
695
696
 
696
697
  return validators.regexp(options);
@@ -715,8 +716,8 @@ Form.validators = (function() {
715
716
  //Don't check empty values (add a 'required' validator for this)
716
717
  if (value === null || value === undefined || value === '') return;
717
718
 
718
- if (value != attrs[options.field]) return err;
719
- }
719
+ if (value !== attrs[options.field]) return err;
720
+ };
720
721
  };
721
722
 
722
723
 
@@ -748,7 +749,7 @@ Form.Field = (function() {
748
749
  */
749
750
  /**
750
751
  * Creates a new field
751
- *
752
+ *
752
753
  * @param {Object} options
753
754
  * @param {Object} [options.schema] Field schema. Defaults to { type: 'Text' }
754
755
  * @param {Model} [options.model] Model the field relates to. Required if options.data is not set.
@@ -768,7 +769,7 @@ Form.Field = (function() {
768
769
 
769
770
  //Turn schema shorthand notation (e.g. 'Text') into schema object
770
771
  if (_.isString(options.schema)) options.schema = { type: options.schema };
771
-
772
+
772
773
  //Set schema defaults
773
774
  this.schema = _.extend({
774
775
  type: 'Text',
@@ -777,6 +778,29 @@ Form.Field = (function() {
777
778
  }, options.schema);
778
779
  },
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
+ },
802
+
803
+
780
804
  /**
781
805
  * Renders the field
782
806
  */
@@ -802,17 +826,15 @@ Form.Field = (function() {
802
826
 
803
827
  //Decide on the editor to use
804
828
  var editor = this.editor = helpers.createEditor(schema.type, options);
805
-
829
+
806
830
  //Create the element
807
- var $field = $(templates[schema.template]({
808
- key: this.key,
809
- title: schema.title,
810
- id: editor.id,
811
- type: schema.type,
812
- editor: '<b class="bbf-tmp-editor"></b>',
813
- help: '<b class="bbf-tmp-help"></b>'
814
- }));
815
-
831
+ var $field = Form.helpers.parseHTML(templates[schema.template](this.renderingContext(schema, editor)));
832
+
833
+ //Remove <label> if it's not wanted
834
+ if (schema.title === false) {
835
+ $field.find('label[for="'+editor.id+'"]').first().remove();
836
+ }
837
+
816
838
  //Render editor
817
839
  $field.find('.bbf-tmp-editor').replaceWith(editor.render().el);
818
840
 
@@ -820,13 +842,17 @@ Form.Field = (function() {
820
842
  this.$help = $('.bbf-tmp-help', $field).parent();
821
843
  this.$help.empty();
822
844
  if (this.schema.help) this.$help.html(this.schema.help);
823
-
845
+
846
+ //Create error container
847
+ this.$error = $($('.bbf-tmp-error', $field).parent()[0]);
848
+ if (this.$error) this.$error.empty();
849
+
824
850
  //Add custom CSS class names
825
851
  if (this.schema.fieldClass) $field.addClass(this.schema.fieldClass);
826
-
852
+
827
853
  //Add custom attributes
828
854
  if (this.schema.fieldAttrs) $field.attr(this.schema.fieldAttrs);
829
-
855
+
830
856
  //Replace the generated wrapper tag
831
857
  this.setElement($field);
832
858
 
@@ -854,7 +880,7 @@ Form.Field = (function() {
854
880
 
855
881
  return id;
856
882
  },
857
-
883
+
858
884
  /**
859
885
  * Check the validity of the field
860
886
  *
@@ -871,7 +897,7 @@ Form.Field = (function() {
871
897
 
872
898
  return error;
873
899
  },
874
-
900
+
875
901
  /**
876
902
  * Set the field into an error state, adding the error class and setting the error message
877
903
  *
@@ -880,26 +906,32 @@ Form.Field = (function() {
880
906
  setError: function(msg) {
881
907
  //Object and NestedModel types set their own errors internally
882
908
  if (this.editor.hasNestedForm) return;
883
-
909
+
884
910
  var errClass = Form.classNames.error;
885
911
 
886
912
  this.$el.addClass(errClass);
887
-
888
- if (this.$help) this.$help.html(msg);
913
+
914
+ if (this.$error) {
915
+ this.$error.html(msg);
916
+ } else if (this.$help) {
917
+ this.$help.html(msg);
918
+ }
889
919
  },
890
-
920
+
891
921
  /**
892
922
  * Clear the error state and reset the help message
893
923
  */
894
924
  clearError: function() {
895
925
  var errClass = Form.classNames.error;
896
-
926
+
897
927
  this.$el.removeClass(errClass);
898
-
928
+
899
929
  // some fields (e.g., Hidden), may not have a help el
900
- if (this.$help) {
930
+ if (this.$error) {
931
+ this.$error.empty();
932
+ } else if (this.$help) {
901
933
  this.$help.empty();
902
-
934
+
903
935
  //Reset help text if available
904
936
  var helpMsg = this.schema.help;
905
937
  if (helpMsg) this.$help.html(helpMsg);
@@ -921,7 +953,7 @@ Form.Field = (function() {
921
953
  getValue: function() {
922
954
  return this.editor.getValue();
923
955
  },
924
-
956
+
925
957
  /**
926
958
  * Set/change the value of the editor
927
959
  *
@@ -930,11 +962,11 @@ Form.Field = (function() {
930
962
  setValue: function(value) {
931
963
  this.editor.setValue(value);
932
964
  },
933
-
965
+
934
966
  focus: function() {
935
967
  this.editor.focus();
936
968
  },
937
-
969
+
938
970
  blur: function() {
939
971
  this.editor.blur();
940
972
  },
@@ -952,6 +984,7 @@ Form.Field = (function() {
952
984
 
953
985
  })();
954
986
 
987
+
955
988
  //========================================================================
956
989
  //EDITORS
957
990
  //========================================================================
@@ -975,7 +1008,7 @@ Form.editors = (function() {
975
1008
  editors.Base = Backbone.View.extend({
976
1009
 
977
1010
  defaultValue: null,
978
-
1011
+
979
1012
  hasFocus: false,
980
1013
 
981
1014
  initialize: function(options) {
@@ -991,20 +1024,20 @@ Form.editors = (function() {
991
1024
  else if (options.value) {
992
1025
  this.value = options.value;
993
1026
  }
994
-
1027
+
995
1028
  if (this.value === undefined) this.value = this.defaultValue;
996
1029
 
997
1030
  this.key = options.key;
998
1031
  this.form = options.form;
999
1032
  this.schema = options.schema || {};
1000
1033
  this.validators = options.validators || this.schema.validators;
1001
-
1034
+
1002
1035
  //Main attributes
1003
1036
  this.$el.attr('name', this.getName());
1004
-
1037
+
1005
1038
  //Add custom CSS class names
1006
1039
  if (this.schema.editorClass) this.$el.addClass(this.schema.editorClass);
1007
-
1040
+
1008
1041
  //Add custom attributes
1009
1042
  if (this.schema.editorAttrs) this.$el.attr(this.schema.editorAttrs);
1010
1043
  },
@@ -1012,15 +1045,15 @@ Form.editors = (function() {
1012
1045
  getValue: function() {
1013
1046
  throw 'Not implemented. Extend and override this method.';
1014
1047
  },
1015
-
1048
+
1016
1049
  setValue: function() {
1017
1050
  throw 'Not implemented. Extend and override this method.';
1018
1051
  },
1019
-
1052
+
1020
1053
  focus: function() {
1021
1054
  throw 'Not implemented. Extend and override this method.';
1022
1055
  },
1023
-
1056
+
1024
1057
  blur: function() {
1025
1058
  throw 'Not implemented. Extend and override this method.';
1026
1059
  },
@@ -1029,39 +1062,38 @@ Form.editors = (function() {
1029
1062
  * Get the value for the form input 'name' attribute
1030
1063
  *
1031
1064
  * @return {String}
1032
- *
1065
+ *
1033
1066
  * @api private
1034
1067
  */
1035
1068
  getName: function() {
1036
1069
  var key = this.key || '';
1037
1070
 
1038
1071
  //Replace periods with underscores (e.g. for when using paths)
1039
- return key.replace(/\./g, '_')
1072
+ return key.replace(/\./g, '_');
1040
1073
  },
1041
-
1074
+
1042
1075
  /**
1043
1076
  * Update the model with the current value
1044
1077
  * NOTE: The method is defined on the editors so that they can be used independently of fields
1045
1078
  *
1046
1079
  * @return {Mixed} error
1047
1080
  */
1048
- commit: function() {
1081
+ commit: function(options) {
1049
1082
  var error = this.validate();
1050
1083
  if (error) return error;
1051
-
1052
- this.model.set(this.key, this.getValue(), {
1053
- error: function(model, e) {
1054
- error = e;
1055
- }
1084
+
1085
+ this.listenTo(this.model, 'invalid', function(model, e) {
1086
+ error = e;
1056
1087
  });
1057
-
1088
+ this.model.set(this.key, this.getValue(), options);
1089
+
1058
1090
  if (error) return error;
1059
1091
  },
1060
-
1092
+
1061
1093
  /**
1062
1094
  * Check validity
1063
1095
  * NOTE: The method is defined on the editors so that they can be used independently of fields
1064
- *
1096
+ *
1065
1097
  * @return {String}
1066
1098
  */
1067
1099
  validate: function() {
@@ -1077,22 +1109,22 @@ Form.editors = (function() {
1077
1109
  _.every(validators, function(validator) {
1078
1110
  error = getValidator(validator)(value, formValues);
1079
1111
 
1080
- return continueLoop = error ? false : true;
1112
+ return error ? false : true;
1081
1113
  });
1082
1114
  }
1083
1115
 
1084
1116
  return error;
1085
1117
  },
1086
-
1087
-
1118
+
1119
+
1088
1120
  trigger: function(event) {
1089
- if (event == 'focus') {
1121
+ if (event === 'focus') {
1090
1122
  this.hasFocus = true;
1091
1123
  }
1092
- else if (event == 'blur') {
1124
+ else if (event === 'blur') {
1093
1125
  this.hasFocus = false;
1094
1126
  }
1095
-
1127
+
1096
1128
  return Backbone.View.prototype.trigger.apply(this, arguments);
1097
1129
  }
1098
1130
  });
@@ -1102,11 +1134,11 @@ Form.editors = (function() {
1102
1134
  editors.Text = editors.Base.extend({
1103
1135
 
1104
1136
  tagName: 'input',
1105
-
1137
+
1106
1138
  defaultValue: '',
1107
-
1139
+
1108
1140
  previousValue: '',
1109
-
1141
+
1110
1142
  events: {
1111
1143
  'keyup': 'determineChange',
1112
1144
  'keypress': function(event) {
@@ -1125,15 +1157,15 @@ Form.editors = (function() {
1125
1157
  this.trigger('blur', this);
1126
1158
  }
1127
1159
  },
1128
-
1160
+
1129
1161
  initialize: function(options) {
1130
1162
  editors.Base.prototype.initialize.call(this, options);
1131
-
1163
+
1132
1164
  var schema = this.schema;
1133
-
1165
+
1134
1166
  //Allow customising text type (email, phone etc.) for HTML5 browsers
1135
1167
  var type = 'text';
1136
-
1168
+
1137
1169
  if (schema && schema.editorAttrs && schema.editorAttrs.type) type = schema.editorAttrs.type;
1138
1170
  if (schema && schema.dataType) type = schema.dataType;
1139
1171
 
@@ -1148,14 +1180,14 @@ Form.editors = (function() {
1148
1180
 
1149
1181
  return this;
1150
1182
  },
1151
-
1183
+
1152
1184
  determineChange: function(event) {
1153
1185
  var currentValue = this.$el.val();
1154
- var changed = (currentValue != this.previousValue);
1155
-
1186
+ var changed = (currentValue !== this.previousValue);
1187
+
1156
1188
  if (changed) {
1157
1189
  this.previousValue = currentValue;
1158
-
1190
+
1159
1191
  this.trigger('change', this);
1160
1192
  }
1161
1193
  },
@@ -1167,27 +1199,27 @@ Form.editors = (function() {
1167
1199
  getValue: function() {
1168
1200
  return this.$el.val();
1169
1201
  },
1170
-
1202
+
1171
1203
  /**
1172
1204
  * Sets the value of the form element
1173
1205
  * @param {String}
1174
1206
  */
1175
- setValue: function(value) {
1207
+ setValue: function(value) {
1176
1208
  this.$el.val(value);
1177
1209
  },
1178
-
1210
+
1179
1211
  focus: function() {
1180
1212
  if (this.hasFocus) return;
1181
1213
 
1182
1214
  this.$el.focus();
1183
1215
  },
1184
-
1216
+
1185
1217
  blur: function() {
1186
1218
  if (!this.hasFocus) return;
1187
1219
 
1188
1220
  this.$el.blur();
1189
1221
  },
1190
-
1222
+
1191
1223
  select: function() {
1192
1224
  this.$el.select();
1193
1225
  }
@@ -1211,6 +1243,7 @@ Form.editors = (function() {
1211
1243
  editors.Text.prototype.initialize.call(this, options);
1212
1244
 
1213
1245
  this.$el.attr('type', 'number');
1246
+ this.$el.attr('step', 'any');
1214
1247
  },
1215
1248
 
1216
1249
  /**
@@ -1222,14 +1255,14 @@ Form.editors = (function() {
1222
1255
  setTimeout(function() {
1223
1256
  self.determineChange();
1224
1257
  }, 0);
1225
- }
1226
-
1258
+ };
1259
+
1227
1260
  //Allow backspace
1228
- if (event.charCode == 0) {
1261
+ if (event.charCode === 0) {
1229
1262
  delayedDetermineChange();
1230
1263
  return;
1231
1264
  }
1232
-
1265
+
1233
1266
  //Get the whole new value so that we can prevent things like double decimals points etc.
1234
1267
  var newVal = this.$el.val() + String.fromCharCode(event.charCode);
1235
1268
 
@@ -1243,12 +1276,12 @@ Form.editors = (function() {
1243
1276
  }
1244
1277
  },
1245
1278
 
1246
- getValue: function() {
1279
+ getValue: function() {
1247
1280
  var value = this.$el.val();
1248
-
1281
+
1249
1282
  return value === "" ? null : parseFloat(value, 10);
1250
1283
  },
1251
-
1284
+
1252
1285
  setValue: function(value) {
1253
1286
  value = (function() {
1254
1287
  if (_.isNumber(value)) return value;
@@ -1259,7 +1292,7 @@ Form.editors = (function() {
1259
1292
  })();
1260
1293
 
1261
1294
  if (_.isNaN(value)) value = null;
1262
-
1295
+
1263
1296
  editors.Text.prototype.setValue.call(this, value);
1264
1297
  }
1265
1298
 
@@ -1284,15 +1317,15 @@ Form.editors = (function() {
1284
1317
  tagName: 'textarea'
1285
1318
 
1286
1319
  });
1287
-
1288
-
1320
+
1321
+
1289
1322
  //CHECKBOX
1290
1323
  editors.Checkbox = editors.Base.extend({
1291
-
1324
+
1292
1325
  defaultValue: false,
1293
-
1326
+
1294
1327
  tagName: 'input',
1295
-
1328
+
1296
1329
  events: {
1297
1330
  'click': function(event) {
1298
1331
  this.trigger('change', this);
@@ -1304,10 +1337,10 @@ Form.editors = (function() {
1304
1337
  this.trigger('blur', this);
1305
1338
  }
1306
1339
  },
1307
-
1340
+
1308
1341
  initialize: function(options) {
1309
1342
  editors.Base.prototype.initialize.call(this, options);
1310
-
1343
+
1311
1344
  this.$el.attr('type', 'checkbox');
1312
1345
  },
1313
1346
 
@@ -1319,35 +1352,35 @@ Form.editors = (function() {
1319
1352
 
1320
1353
  return this;
1321
1354
  },
1322
-
1355
+
1323
1356
  getValue: function() {
1324
1357
  return this.$el.prop('checked');
1325
1358
  },
1326
-
1359
+
1327
1360
  setValue: function(value) {
1328
1361
  if (value) {
1329
1362
  this.$el.prop('checked', true);
1330
1363
  }
1331
1364
  },
1332
-
1365
+
1333
1366
  focus: function() {
1334
1367
  if (this.hasFocus) return;
1335
1368
 
1336
1369
  this.$el.focus();
1337
1370
  },
1338
-
1371
+
1339
1372
  blur: function() {
1340
1373
  if (!this.hasFocus) return;
1341
1374
 
1342
1375
  this.$el.blur();
1343
1376
  }
1344
-
1377
+
1345
1378
  });
1346
-
1347
-
1379
+
1380
+
1348
1381
  //HIDDEN
1349
1382
  editors.Hidden = editors.Base.extend({
1350
-
1383
+
1351
1384
  defaultValue: '',
1352
1385
 
1353
1386
  initialize: function(options) {
@@ -1355,21 +1388,21 @@ Form.editors = (function() {
1355
1388
 
1356
1389
  this.$el.attr('type', 'hidden');
1357
1390
  },
1358
-
1391
+
1359
1392
  getValue: function() {
1360
1393
  return this.value;
1361
1394
  },
1362
-
1395
+
1363
1396
  setValue: function(value) {
1364
1397
  this.value = value;
1365
1398
  },
1366
-
1399
+
1367
1400
  focus: function() {
1368
-
1401
+
1369
1402
  },
1370
-
1403
+
1371
1404
  blur: function() {
1372
-
1405
+
1373
1406
  }
1374
1407
 
1375
1408
  });
@@ -1377,7 +1410,7 @@ Form.editors = (function() {
1377
1410
 
1378
1411
  /**
1379
1412
  * SELECT
1380
- *
1413
+ *
1381
1414
  * Renders a <select> with given options
1382
1415
  *
1383
1416
  * Requires an 'options' value on the schema.
@@ -1387,7 +1420,7 @@ Form.editors = (function() {
1387
1420
  editors.Select = editors.Base.extend({
1388
1421
 
1389
1422
  tagName: 'select',
1390
-
1423
+
1391
1424
  events: {
1392
1425
  'change': function(event) {
1393
1426
  this.trigger('change', this);
@@ -1440,7 +1473,7 @@ Form.editors = (function() {
1440
1473
  else if (_.isFunction(options)) {
1441
1474
  options(function(result) {
1442
1475
  self.renderOptions(result);
1443
- });
1476
+ }, self);
1444
1477
  }
1445
1478
 
1446
1479
  //Otherwise, ready to go straight to renderOptions
@@ -1459,6 +1492,17 @@ Form.editors = (function() {
1459
1492
  var $select = this.$el,
1460
1493
  html;
1461
1494
 
1495
+ html = this._getOptionsHtml(options);
1496
+
1497
+ //Insert options
1498
+ $select.html(html);
1499
+
1500
+ //Select correct option
1501
+ this.setValue(this.value);
1502
+ },
1503
+
1504
+ _getOptionsHtml: function(options) {
1505
+ var html;
1462
1506
  //Accept string of HTML
1463
1507
  if (_.isString(options)) {
1464
1508
  html = options;
@@ -1471,30 +1515,36 @@ Form.editors = (function() {
1471
1515
 
1472
1516
  //Or Backbone collection
1473
1517
  else if (options instanceof Backbone.Collection) {
1474
- html = this._collectionToHtml(options)
1518
+ html = this._collectionToHtml(options);
1475
1519
  }
1476
1520
 
1477
- //Insert options
1478
- $select.html(html);
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
+ }
1479
1530
 
1480
- //Select correct option
1481
- this.setValue(this.value);
1531
+ return html;
1482
1532
  },
1483
1533
 
1484
1534
  getValue: function() {
1485
1535
  return this.$el.val();
1486
1536
  },
1487
-
1537
+
1488
1538
  setValue: function(value) {
1489
1539
  this.$el.val(value);
1490
1540
  },
1491
-
1541
+
1492
1542
  focus: function() {
1493
1543
  if (this.hasFocus) return;
1494
1544
 
1495
1545
  this.$el.focus();
1496
1546
  },
1497
-
1547
+
1498
1548
  blur: function() {
1499
1549
  if (!this.hasFocus) return;
1500
1550
 
@@ -1503,7 +1553,7 @@ Form.editors = (function() {
1503
1553
 
1504
1554
  /**
1505
1555
  * Transforms a collection into HTML ready to use in the renderOptions method
1506
- * @param {Backbone.Collection}
1556
+ * @param {Backbone.Collection}
1507
1557
  * @return {String}
1508
1558
  */
1509
1559
  _collectionToHtml: function(collection) {
@@ -1531,13 +1581,19 @@ Form.editors = (function() {
1531
1581
  //Generate HTML
1532
1582
  _.each(array, function(option) {
1533
1583
  if (_.isObject(option)) {
1534
- var val = option.val ? option.val : '';
1535
- html.push('<option value="'+val+'">'+option.label+'</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
+ }
1536
1592
  }
1537
1593
  else {
1538
1594
  html.push('<option>'+option+'</option>');
1539
1595
  }
1540
- });
1596
+ }, this);
1541
1597
 
1542
1598
  return html.join('');
1543
1599
  }
@@ -1548,7 +1604,7 @@ Form.editors = (function() {
1548
1604
 
1549
1605
  /**
1550
1606
  * RADIO
1551
- *
1607
+ *
1552
1608
  * Renders a <ul> with given options represented as <li> objects containing radio buttons
1553
1609
  *
1554
1610
  * Requires an 'options' value on the schema.
@@ -1559,10 +1615,10 @@ Form.editors = (function() {
1559
1615
 
1560
1616
  tagName: 'ul',
1561
1617
  className: 'bbf-radio',
1562
-
1618
+
1563
1619
  events: {
1564
- 'click input[type=radio]:not(:checked)': function() {
1565
- this.trigger('change', this)
1620
+ 'change input[type=radio]': function() {
1621
+ this.trigger('change', this);
1566
1622
  },
1567
1623
  'focus input[type=radio]': function() {
1568
1624
  if (this.hasFocus) return;
@@ -1585,22 +1641,22 @@ Form.editors = (function() {
1585
1641
  setValue: function(value) {
1586
1642
  this.$('input[type=radio]').val([value]);
1587
1643
  },
1588
-
1644
+
1589
1645
  focus: function() {
1590
1646
  if (this.hasFocus) return;
1591
-
1647
+
1592
1648
  var checked = this.$('input[type=radio]:checked');
1593
1649
  if (checked[0]) {
1594
1650
  checked.focus();
1595
1651
  return;
1596
1652
  }
1597
-
1653
+
1598
1654
  this.$('input[type=radio]').first().focus();
1599
1655
  },
1600
-
1656
+
1601
1657
  blur: function() {
1602
1658
  if (!this.hasFocus) return;
1603
-
1659
+
1604
1660
  this.$('input[type=radio]:focus').blur();
1605
1661
  },
1606
1662
 
@@ -1617,13 +1673,13 @@ Form.editors = (function() {
1617
1673
  _.each(array, function(option, index) {
1618
1674
  var itemHtml = '<li>';
1619
1675
  if (_.isObject(option)) {
1620
- var val = option.val ? option.val : '';
1621
- itemHtml += ('<input type="radio" name="'+self.id+'" value="'+val+'" id="'+self.id+'-'+index+'" />')
1622
- itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>')
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>');
1623
1679
  }
1624
1680
  else {
1625
- itemHtml += ('<input type="radio" name="'+self.id+'" value="'+option+'" id="'+self.id+'-'+index+'" />')
1626
- itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>')
1681
+ itemHtml += ('<input type="radio" name="'+self.id+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
1682
+ itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
1627
1683
  }
1628
1684
  itemHtml += '</li>';
1629
1685
  html.push(itemHtml);
@@ -1648,10 +1704,10 @@ Form.editors = (function() {
1648
1704
 
1649
1705
  tagName: 'ul',
1650
1706
  className: 'bbf-checkboxes',
1651
-
1707
+
1652
1708
  events: {
1653
1709
  'click input[type=checkbox]': function() {
1654
- this.trigger('change', this)
1710
+ this.trigger('change', this);
1655
1711
  },
1656
1712
  'focus input[type=checkbox]': function() {
1657
1713
  if (this.hasFocus) return;
@@ -1679,16 +1735,16 @@ Form.editors = (function() {
1679
1735
  if (!_.isArray(values)) values = [values];
1680
1736
  this.$('input[type=checkbox]').val(values);
1681
1737
  },
1682
-
1738
+
1683
1739
  focus: function() {
1684
1740
  if (this.hasFocus) return;
1685
-
1741
+
1686
1742
  this.$('input[type=checkbox]').first().focus();
1687
1743
  },
1688
-
1744
+
1689
1745
  blur: function() {
1690
1746
  if (!this.hasFocus) return;
1691
-
1747
+
1692
1748
  this.$('input[type=checkbox]:focus').blur();
1693
1749
  },
1694
1750
 
@@ -1705,13 +1761,13 @@ Form.editors = (function() {
1705
1761
  _.each(array, function(option, index) {
1706
1762
  var itemHtml = '<li>';
1707
1763
  if (_.isObject(option)) {
1708
- var val = option.val ? option.val : '';
1709
- itemHtml += ('<input type="checkbox" name="'+self.id+'" value="'+val+'" id="'+self.id+'-'+index+'" />')
1710
- itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>')
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>');
1711
1767
  }
1712
1768
  else {
1713
- itemHtml += ('<input type="checkbox" name="'+self.id+'" value="'+option+'" id="'+self.id+'-'+index+'" />')
1714
- itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>')
1769
+ itemHtml += ('<input type="checkbox" name="'+self.id+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
1770
+ itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
1715
1771
  }
1716
1772
  itemHtml += '</li>';
1717
1773
  html.push(itemHtml);
@@ -1726,9 +1782,9 @@ Form.editors = (function() {
1726
1782
 
1727
1783
  /**
1728
1784
  * OBJECT
1729
- *
1785
+ *
1730
1786
  * Creates a child form. For editing Javascript objects
1731
- *
1787
+ *
1732
1788
  * @param {Object} options
1733
1789
  * @param {Object} options.schema The schema for the object
1734
1790
  * @param {Object} options.schema.subSchema The schema for the nested form
@@ -1750,7 +1806,7 @@ Form.editors = (function() {
1750
1806
  if (!this.schema.subSchema) throw new Error("Missing required 'schema.subSchema' option for Object editor");
1751
1807
  },
1752
1808
 
1753
- render: function() {
1809
+ render: function() {
1754
1810
  //Create the nested form
1755
1811
  this.form = new Form({
1756
1812
  schema: this.schema.subSchema,
@@ -1762,9 +1818,9 @@ Form.editors = (function() {
1762
1818
  this._observeFormEvents();
1763
1819
 
1764
1820
  this.$el.html(this.form.render().el);
1765
-
1821
+
1766
1822
  if (this.hasFocus) this.trigger('blur', this);
1767
-
1823
+
1768
1824
  return this;
1769
1825
  },
1770
1826
 
@@ -1773,22 +1829,22 @@ Form.editors = (function() {
1773
1829
 
1774
1830
  return this.value;
1775
1831
  },
1776
-
1832
+
1777
1833
  setValue: function(value) {
1778
1834
  this.value = value;
1779
-
1835
+
1780
1836
  this.render();
1781
1837
  },
1782
-
1838
+
1783
1839
  focus: function() {
1784
1840
  if (this.hasFocus) return;
1785
-
1841
+
1786
1842
  this.form.focus();
1787
1843
  },
1788
-
1844
+
1789
1845
  blur: function() {
1790
1846
  if (!this.hasFocus) return;
1791
-
1847
+
1792
1848
  this.form.blur();
1793
1849
  },
1794
1850
 
@@ -1797,19 +1853,19 @@ Form.editors = (function() {
1797
1853
 
1798
1854
  Backbone.View.prototype.remove.call(this);
1799
1855
  },
1800
-
1856
+
1801
1857
  validate: function() {
1802
1858
  return this.form.validate();
1803
1859
  },
1804
-
1860
+
1805
1861
  _observeFormEvents: function() {
1806
1862
  this.form.on('all', function() {
1807
1863
  // args = ["key:change", form, fieldEditor]
1808
- args = _.toArray(arguments);
1864
+ var args = _.toArray(arguments);
1809
1865
  args[1] = this;
1810
1866
  // args = ["key:change", this=objectEditor, fieldEditor]
1811
-
1812
- this.trigger.apply(this, args)
1867
+
1868
+ this.trigger.apply(this, args);
1813
1869
  }, this);
1814
1870
  }
1815
1871
 
@@ -1819,9 +1875,9 @@ Form.editors = (function() {
1819
1875
 
1820
1876
  /**
1821
1877
  * NESTED MODEL
1822
- *
1878
+ *
1823
1879
  * Creates a child form. For editing nested Backbone models
1824
- *
1880
+ *
1825
1881
  * Special options:
1826
1882
  * schema.model: Embedded model constructor
1827
1883
  */
@@ -1839,9 +1895,7 @@ Form.editors = (function() {
1839
1895
  nestedModel = this.schema.model;
1840
1896
 
1841
1897
  //Wrap the data in a model if it isn't already a model instance
1842
- var modelInstance = (data.constructor == nestedModel)
1843
- ? data
1844
- : new nestedModel(data);
1898
+ var modelInstance = (data.constructor === nestedModel) ? data : new nestedModel(data);
1845
1899
 
1846
1900
  this.form = new Form({
1847
1901
  model: modelInstance,
@@ -1853,7 +1907,7 @@ Form.editors = (function() {
1853
1907
 
1854
1908
  //Render form
1855
1909
  this.$el.html(this.form.render().el);
1856
-
1910
+
1857
1911
  if (this.hasFocus) this.trigger('blur', this);
1858
1912
 
1859
1913
  return this;
@@ -1912,12 +1966,12 @@ Form.editors = (function() {
1912
1966
  },
1913
1967
 
1914
1968
  initialize: function(options) {
1915
- options = options || {}
1969
+ options = options || {};
1916
1970
 
1917
1971
  editors.Base.prototype.initialize.call(this, options);
1918
1972
 
1919
1973
  var Self = editors.Date,
1920
- today = new Date;
1974
+ today = new Date();
1921
1975
 
1922
1976
  //Option defaults
1923
1977
  this.options = _.extend({
@@ -1930,18 +1984,18 @@ Form.editors = (function() {
1930
1984
  yearStart: today.getFullYear() - 100,
1931
1985
  yearEnd: today.getFullYear()
1932
1986
  }, options.schema || {});
1933
-
1987
+
1934
1988
  //Cast to Date
1935
1989
  if (this.value && !_.isDate(this.value)) {
1936
1990
  this.value = new Date(this.value);
1937
1991
  }
1938
-
1992
+
1939
1993
  //Set default date
1940
1994
  if (!this.value) {
1941
1995
  var date = new Date();
1942
1996
  date.setSeconds(0);
1943
1997
  date.setMilliseconds(0);
1944
-
1998
+
1945
1999
  this.value = date;
1946
2000
  }
1947
2001
  },
@@ -1959,12 +2013,15 @@ Form.editors = (function() {
1959
2013
  return '<option value="'+month+'">' + value + '</option>';
1960
2014
  });
1961
2015
 
1962
- var yearsOptions = _.map(_.range(schema.yearStart, schema.yearEnd + 1), function(year) {
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) {
1963
2020
  return '<option value="'+year+'">' + year + '</option>';
1964
2021
  });
1965
2022
 
1966
2023
  //Render the selects
1967
- var $el = $(Form.templates.date({
2024
+ var $el = Form.helpers.parseHTML(Form.templates.date({
1968
2025
  dates: datesOptions.join(''),
1969
2026
  months: monthsOptions.join(''),
1970
2027
  years: yearsOptions.join('')
@@ -1985,7 +2042,7 @@ Form.editors = (function() {
1985
2042
  //Remove the wrapper tag
1986
2043
  this.setElement($el);
1987
2044
  this.$el.attr('id', this.id);
1988
-
2045
+
1989
2046
  if (this.hasFocus) this.trigger('blur', this);
1990
2047
 
1991
2048
  return this;
@@ -2003,7 +2060,7 @@ Form.editors = (function() {
2003
2060
 
2004
2061
  return new Date(year, month, date);
2005
2062
  },
2006
-
2063
+
2007
2064
  /**
2008
2065
  * @param {Date} date
2009
2066
  */
@@ -2014,16 +2071,16 @@ Form.editors = (function() {
2014
2071
 
2015
2072
  this.updateHidden();
2016
2073
  },
2017
-
2074
+
2018
2075
  focus: function() {
2019
2076
  if (this.hasFocus) return;
2020
-
2077
+
2021
2078
  this.$('select').first().focus();
2022
2079
  },
2023
-
2080
+
2024
2081
  blur: function() {
2025
2082
  if (!this.hasFocus) return;
2026
-
2083
+
2027
2084
  this.$('select:focus').blur();
2028
2085
  },
2029
2086
 
@@ -2052,7 +2109,7 @@ Form.editors = (function() {
2052
2109
 
2053
2110
  /**
2054
2111
  * DATETIME
2055
- *
2112
+ *
2056
2113
  * @param {Editor} [options.DateEditor] Date editor view to use (not definition)
2057
2114
  * @param {Number} [options.schema.minsInterval] Interval between minutes. Default: 15
2058
2115
  */
@@ -2100,7 +2157,7 @@ Form.editors = (function() {
2100
2157
 
2101
2158
  render: function() {
2102
2159
  function pad(n) {
2103
- return n < 10 ? '0' + n : n
2160
+ return n < 10 ? '0' + n : n;
2104
2161
  }
2105
2162
 
2106
2163
  var schema = this.schema;
@@ -2115,7 +2172,7 @@ Form.editors = (function() {
2115
2172
  });
2116
2173
 
2117
2174
  //Render time selects
2118
- var $el = $(Form.templates.dateTime({
2175
+ var $el = Form.helpers.parseHTML(Form.templates.dateTime({
2119
2176
  date: '<b class="bbf-tmp"></b>',
2120
2177
  hours: hoursOptions.join(),
2121
2178
  mins: minsOptions.join()
@@ -2130,13 +2187,13 @@ Form.editors = (function() {
2130
2187
 
2131
2188
  //Get the hidden date field to store values in case POSTed to server
2132
2189
  this.$hidden = $el.find('input[type="hidden"]');
2133
-
2190
+
2134
2191
  //Set time
2135
2192
  this.setValue(this.value);
2136
2193
 
2137
2194
  this.setElement($el);
2138
2195
  this.$el.attr('id', this.id);
2139
-
2196
+
2140
2197
  if (this.hasFocus) this.trigger('blur', this);
2141
2198
 
2142
2199
  return this;
@@ -2158,27 +2215,27 @@ Form.editors = (function() {
2158
2215
 
2159
2216
  return date;
2160
2217
  },
2161
-
2218
+
2162
2219
  setValue: function(date) {
2163
2220
  if (!_.isDate(date)) date = new Date(date);
2164
-
2221
+
2165
2222
  this.dateEditor.setValue(date);
2166
-
2223
+
2167
2224
  this.$hour.val(date.getHours());
2168
2225
  this.$min.val(date.getMinutes());
2169
2226
 
2170
2227
  this.updateHidden();
2171
2228
  },
2172
-
2229
+
2173
2230
  focus: function() {
2174
2231
  if (this.hasFocus) return;
2175
-
2232
+
2176
2233
  this.$('select').first().focus();
2177
2234
  },
2178
-
2235
+
2179
2236
  blur: function() {
2180
2237
  if (!this.hasFocus) return;
2181
-
2238
+
2182
2239
  this.$('select:focus').blur();
2183
2240
  },
2184
2241
 
@@ -2243,6 +2300,7 @@ Form.editors = (function() {
2243
2300
  <label for="{{id}}">{{title}}</label>\
2244
2301
  <div class="bbf-editor">{{editor}}</div>\
2245
2302
  <div class="bbf-help">{{help}}</div>\
2303
+ <div class="bbf-error">{{error}}</div>\
2246
2304
  </li>\
2247
2305
  ',
2248
2306
 
@@ -2251,6 +2309,7 @@ Form.editors = (function() {
2251
2309
  <label for="{{id}}">{{title}}</label>\
2252
2310
  <div class="bbf-editor">{{editor}}</div>\
2253
2311
  <div class="bbf-help">{{help}}</div>\
2312
+ <div class="bbf-error">{{error}}</div>\
2254
2313
  </li>\
2255
2314
  ',
2256
2315
 
@@ -2300,7 +2359,7 @@ Form.editors = (function() {
2300
2359
 
2301
2360
 
2302
2361
  //Metadata
2303
- Form.VERSION = '0.10.1';
2362
+ Form.VERSION = '0.11.0';
2304
2363
 
2305
2364
 
2306
2365
  //Exports