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