rails-backbone-forms 0.10.1 → 0.11.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.
@@ -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