iugu-ux 0.8.8 → 0.8.9

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.
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);