iugu-ux 0.8.8 → 0.8.9

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