Skip to content

Instantly share code, notes, and snippets.

@marfalkov
Last active August 29, 2015 13:59
Show Gist options
  • Save marfalkov/10478524 to your computer and use it in GitHub Desktop.
Save marfalkov/10478524 to your computer and use it in GitHub Desktop.
thorax integration test : http://jsfiddle.net/marfalkov/2gmjq/
/**
* Backbone Forms v0.14.0
*
* Copyright (c) 2014 Charles Davison, Pow Media Ltd
*
* License and more information at:
* http://github.com/powmedia/backbone-forms
*/
;(function(root) {
//DEPENDENCIES
//CommonJS
if (typeof exports !== 'undefined' && typeof require !== 'undefined') {
var _ = root._ || require('underscore'),
Backbone = root.Backbone || require('backbone');
}
//Browser
else {
var _ = root._,
Backbone = root.Backbone;
}
//SOURCE
//==================================================================================================
//FORM
//==================================================================================================
var Form = Thorax.View.extend({
events: {
'submit': function(event) {
this.trigger('submit', event);
}
},
/**
* Constructor
*
* @param {Object} [options.schema]
* @param {Backbone.Model} [options.model]
* @param {Object} [options.data]
* @param {String[]|Object[]} [options.fieldsets]
* @param {String[]} [options.fields]
* @param {String} [options.idPrefix]
* @param {Form.Field} [options.Field]
* @param {Form.Fieldset} [options.Fieldset]
* @param {Function} [options.template]
*/
initialize: function(options) {
var self = this;
options = options || {};
//Find the schema to use
var schema = this.schema = (function() {
//Prefer schema from options
if (options.schema) return _.result(options, 'schema');
//Then schema on model
var model = options.model;
if (model && model.schema) return _.result(model, 'schema');
//Then built-in schema
if (self.schema) return _.result(self, 'schema');
//Fallback to empty schema
return {};
})();
//Store important data
_.extend(this, _.pick(options, 'model', 'data', 'idPrefix', 'templateData'));
//Override defaults
var constructor = this.constructor;
this.template = options.template || this.template || constructor.template;
this.Fieldset = options.Fieldset || this.Fieldset || constructor.Fieldset;
this.Field = options.Field || this.Field || constructor.Field;
this.NestedField = options.NestedField || this.NestedField || constructor.NestedField;
//Check which fields will be included (defaults to all)
var selectedFields = this.selectedFields = options.fields || _.keys(schema);
//Create fields
var fields = this.fields = {};
_.each(selectedFields, function(key) {
var fieldSchema = schema[key];
fields[key] = this.createField(key, fieldSchema);
}, this);
//Create fieldsets
var fieldsetSchema = options.fieldsets || _.result(this, 'fieldsets') || [selectedFields],
fieldsets = this.fieldsets = [];
_.each(fieldsetSchema, function(itemSchema) {
this.fieldsets.push(this.createFieldset(itemSchema));
}, this);
},
/**
* Creates a Fieldset instance
*
* @param {String[]|Object[]} schema Fieldset schema
*
* @return {Form.Fieldset}
*/
createFieldset: function(schema) {
var options = {
schema: schema,
fields: this.fields
};
return new this.Fieldset(options);
},
/**
* Creates a Field instance
*
* @param {String} key
* @param {Object} schema Field schema
*
* @return {Form.Field}
*/
createField: function(key, schema) {
var options = {
form: this,
key: key,
schema: schema,
idPrefix: this.idPrefix
};
if (this.model) {
options.model = this.model;
} else if (this.data) {
options.value = this.data[key];
} else {
options.value = null;
}
var field = new this.Field(options);
this.listenTo(field.editor, 'all', this.handleEditorEvent);
return field;
},
/**
* Callback for when an editor event is fired.
* Re-triggers events on the form as key:event and triggers additional form-level events
*
* @param {String} event
* @param {Editor} editor
*/
handleEditorEvent: function(event, editor) {
//Re-trigger editor events on the form
var formEvent = editor.key+':'+event;
this.trigger.call(this, formEvent, this, editor, Array.prototype.slice.call(arguments, 2));
//Trigger additional events
switch (event) {
case 'change':
this.trigger('change', this);
break;
case 'focus':
if (!this.hasFocus) this.trigger('focus', this);
break;
case 'blur':
if (this.hasFocus) {
//TODO: Is the timeout etc needed?
var self = this;
setTimeout(function() {
var focusedField = _.find(self.fields, function(field) {
return field.editor.hasFocus;
});
if (!focusedField) self.trigger('blur', self);
}, 0);
}
break;
}
},
render: function() {
var self = this,
fields = this.fields,
$ = Backbone.$;
//Render form
var $form = $($.trim(this.template(_.result(this, 'templateData'))));
//Render standalone editors
$form.find('[data-editors]').add($form).each(function(i, el) {
var $container = $(el),
selection = $container.attr('data-editors');
if (_.isUndefined(selection)) return;
//Work out which fields to include
var keys = (selection == '*')
? self.selectedFields || _.keys(fields)
: selection.split(',');
//Add them
_.each(keys, function(key) {
var field = fields[key];
$container.append(field.editor.render().el);
});
});
//Render standalone fields
$form.find('[data-fields]').add($form).each(function(i, el) {
var $container = $(el),
selection = $container.attr('data-fields');
if (_.isUndefined(selection)) return;
//Work out which fields to include
var keys = (selection == '*')
? self.selectedFields || _.keys(fields)
: selection.split(',');
//Add them
_.each(keys, function(key) {
var field = fields[key];
$container.append(field.render().el);
});
});
//Render fieldsets
$form.find('[data-fieldsets]').add($form).each(function(i, el) {
var $container = $(el),
selection = $container.attr('data-fieldsets');
if (_.isUndefined(selection)) return;
_.each(self.fieldsets, function(fieldset) {
$container.append(fieldset.render().el);
});
});
//Set the main element
this.setElement($form);
//Set class
$form.addClass(this.className);
return this;
},
/**
* Validate the data
*
* @return {Object} Validation errors
*/
validate: function(options) {
var self = this,
fields = this.fields,
model = this.model,
errors = {};
options = options || {};
//Collect errors from schema validation
_.each(fields, function(field) {
var error = field.validate();
if (error) {
errors[field.key] = error;
}
});
//Get errors from default Backbone model validator
if (!options.skipModelValidate && model && model.validate) {
var modelErrors = model.validate(this.getValue());
if (modelErrors) {
var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
//If errors are not in object form then just store on the error object
if (!isDictionary) {
errors._others = errors._others || [];
errors._others.push(modelErrors);
}
//Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
if (isDictionary) {
_.each(modelErrors, function(val, key) {
//Set error on field if there isn't one already
if (fields[key] && !errors[key]) {
fields[key].setError(val);
errors[key] = val;
}
else {
//Otherwise add to '_others' key
errors._others = errors._others || [];
var tmpErr = {};
tmpErr[key] = val;
errors._others.push(tmpErr);
}
});
}
}
}
return _.isEmpty(errors) ? null : errors;
},
/**
* Update the model with all latest values.
*
* @param {Object} [options] Options to pass to Model#set (e.g. { silent: true })
*
* @return {Object} Validation errors
*/
commit: function(options) {
//Validate
options = options || {};
var validateOptions = {
skipModelValidate: !options.validate
};
var errors = this.validate(validateOptions);
if (errors) return errors;
//Commit
var modelError;
var setOptions = _.extend({
error: function(model, e) {
modelError = e;
}
}, options);
this.model.set(this.getValue(), setOptions);
if (modelError) return modelError;
},
/**
* Get all the field values as an object.
* Use this method when passing data instead of objects
*
* @param {String} [key] Specific field value to get
*/
getValue: function(key) {
//Return only given key if specified
if (key) return this.fields[key].getValue();
//Otherwise return entire form
var values = {};
_.each(this.fields, function(field) {
values[field.key] = field.getValue();
});
return values;
},
/**
* Update field values, referenced by key
*
* @param {Object|String} key New values to set, or property to set
* @param val Value to set
*/
setValue: function(prop, val) {
var data = {};
if (typeof prop === 'string') {
data[prop] = val;
} else {
data = prop;
}
var key;
for (key in this.schema) {
if (data[key] !== undefined) {
this.fields[key].setValue(data[key]);
}
}
},
/**
* Returns the editor for a given field key
*
* @param {String} key
*
* @return {Editor}
*/
getEditor: function(key) {
var field = this.fields[key];
if (!field) throw new Error('Field not found: '+key);
return field.editor;
},
/**
* Gives the first editor in the form focus
*/
focus: function() {
if (this.hasFocus) return;
//Get the first field
var fieldset = this.fieldsets[0],
field = fieldset.getFieldAt(0);
if (!field) return;
//Set focus
field.editor.focus();
},
/**
* Removes focus from the currently focused editor
*/
blur: function() {
if (!this.hasFocus) return;
var focusedField = _.find(this.fields, function(field) {
return field.editor.hasFocus;
});
if (focusedField) focusedField.editor.blur();
},
/**
* Manages the hasFocus property
*
* @param {String} event
*/
trigger: function(event) {
if (event === 'focus') {
this.hasFocus = true;
}
else if (event === 'blur') {
this.hasFocus = false;
}
return Thorax.View.prototype.trigger.apply(this, arguments);
},
/**
* Override default remove function in order to remove embedded views
*
* TODO: If editors are included directly with data-editors="x", they need to be removed
* May be best to use XView to manage adding/removing views
*/
remove: function() {
_.each(this.fieldsets, function(fieldset) {
fieldset.remove();
});
_.each(this.fields, function(field) {
field.remove();
});
return Thorax.View.prototype.remove.apply(this, arguments);
}
}, {
//STATICS
template: _.template('\
<form data-fieldsets></form>\
', null, this.templateSettings),
templateSettings: {
evaluate: /<%([\s\S]+?)%>/g,
interpolate: /<%=([\s\S]+?)%>/g,
escape: /<%-([\s\S]+?)%>/g
},
editors: {}
});
//==================================================================================================
//VALIDATORS
//==================================================================================================
Form.validators = (function() {
var validators = {};
validators.errMessages = {
required: 'Required',
regexp: 'Invalid',
number: 'Must be a number',
email: 'Invalid email address',
url: 'Invalid URL',
match: _.template('Must match field "<%= field %>"', null, Form.templateSettings)
};
validators.required = function(options) {
options = _.extend({
type: 'required',
message: this.errMessages.required
}, options);
return function required(value) {
options.value = value;
var err = {
type: options.type,
message: _.isFunction(options.message) ? options.message(options) : options.message
};
if (value === null || value === undefined || value === false || value === '') return err;
};
};
validators.regexp = function(options) {
if (!options.regexp) throw new Error('Missing required "regexp" option for "regexp" validator');
options = _.extend({
type: 'regexp',
match: true,
message: this.errMessages.regexp
}, options);
return function regexp(value) {
options.value = value;
var err = {
type: options.type,
message: _.isFunction(options.message) ? options.message(options) : options.message
};
//Don't check empty values (add a 'required' validator for this)
if (value === null || value === undefined || value === '') return;
//Create RegExp from string if it's valid
if ('string' === typeof options.regexp) options.regexp = new RegExp(options.regexp, options.flags);
if ((options.match) ? !options.regexp.test(value) : options.regexp.test(value)) return err;
};
};
validators.number = function(options) {
options = _.extend({
type: 'number',
message: this.errMessages.number,
regexp: /^[0-9]*\.?[0-9]*?$/
}, options);
return validators.regexp(options);
};
validators.email = function(options) {
options = _.extend({
type: 'email',
message: this.errMessages.email,
regexp: /^[\w\-]{1,}([\w\-\+.]{1,1}[\w\-]{1,}){0,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/
}, options);
return validators.regexp(options);
};
validators.url = function(options) {
options = _.extend({
type: 'url',
message: this.errMessages.url,
regexp: /^(http|https):\/\/(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
}, options);
return validators.regexp(options);
};
validators.match = function(options) {
if (!options.field) throw new Error('Missing required "field" options for "match" validator');
options = _.extend({
type: 'match',
message: this.errMessages.match
}, options);
return function match(value, attrs) {
options.value = value;
var err = {
type: options.type,
message: _.isFunction(options.message) ? options.message(options) : options.message
};
//Don't check empty values (add a 'required' validator for this)
if (value === null || value === undefined || value === '') return;
if (value !== attrs[options.field]) return err;
};
};
return validators;
})();
//==================================================================================================
//FIELDSET
//==================================================================================================
Form.Fieldset = Thorax.View.extend({
/**
* Constructor
*
* Valid fieldset schemas:
* ['field1', 'field2']
* { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
*
* @param {String[]|Object[]} options.schema Fieldset schema
* @param {Object} options.fields Form fields
*/
initialize: function(options) {
options = options || {};
//Create the full fieldset schema, merging defaults etc.
var schema = this.schema = this.createSchema(options.schema);
//Store the fields for this fieldset
this.fields = _.pick(options.fields, schema.fields);
//Override defaults
this.template = options.template || schema.template || this.template || this.constructor.template;
},
/**
* Creates the full fieldset schema, normalising, merging defaults etc.
*
* @param {String[]|Object[]} schema
*
* @return {Object}
*/
createSchema: function(schema) {
//Normalise to object
if (_.isArray(schema)) {
schema = { fields: schema };
}
//Add null legend to prevent template error
schema.legend = schema.legend || null;
return schema;
},
/**
* Returns the field for a given index
*
* @param {Number} index
*
* @return {Field}
*/
getFieldAt: function(index) {
var key = this.schema.fields[index];
return this.fields[key];
},
/**
* Returns data to pass to template
*
* @return {Object}
*/
templateData: function() {
return this.schema;
},
/**
* Renders the fieldset and fields
*
* @return {Fieldset} this
*/
render: function() {
var schema = this.schema,
fields = this.fields,
$ = Backbone.$;
//Render fieldset
var $fieldset = $($.trim(this.template(_.result(this, 'templateData'))));
//Render fields
$fieldset.find('[data-fields]').add($fieldset).each(function(i, el) {
var $container = $(el),
selection = $container.attr('data-fields');
if (_.isUndefined(selection)) return;
_.each(fields, function(field) {
$container.append(field.render().el);
});
});
this.setElement($fieldset);
return this;
},
/**
* Remove embedded views then self
*/
remove: function() {
_.each(this.fields, function(field) {
field.remove();
});
Thorax.View.prototype.remove.call(this);
}
}, {
//STATICS
template: _.template('\
<fieldset data-fields>\
<% if (legend) { %>\
<legend><%= legend %></legend>\
<% } %>\
</fieldset>\
', null, Form.templateSettings)
});
//==================================================================================================
//FIELD
//==================================================================================================
Form.Field = Thorax.View.extend({
/**
* Constructor
*
* @param {Object} options.key
* @param {Object} options.form
* @param {Object} [options.schema]
* @param {Function} [options.schema.template]
* @param {Backbone.Model} [options.model]
* @param {Object} [options.value]
* @param {String} [options.idPrefix]
* @param {Function} [options.template]
* @param {Function} [options.errorClassName]
*/
initialize: function(options) {
options = options || {};
//Store important data
_.extend(this, _.pick(options, 'form', 'key', 'model', 'value', 'idPrefix'));
//Create the full field schema, merging defaults etc.
var schema = this.schema = this.createSchema(options.schema);
//Override defaults
this.template = options.template || schema.template || this.template || this.constructor.template;
this.errorClassName = options.errorClassName || this.errorClassName || this.constructor.errorClassName;
//Create editor
this.editor = this.createEditor();
},
/**
* Creates the full field schema, merging defaults etc.
*
* @param {Object|String} schema
*
* @return {Object}
*/
createSchema: function(schema) {
if (_.isString(schema)) schema = { type: schema };
//Set defaults
schema = _.extend({
type: 'Text',
title: this.createTitle()
}, schema);
//Get the real constructor function i.e. if type is a string such as 'Text'
schema.type = (_.isString(schema.type)) ? Form.editors[schema.type] : schema.type;
return schema;
},
/**
* Creates the editor specified in the schema; either an editor string name or
* a constructor function
*
* @return {View}
*/
createEditor: function() {
var options = _.extend(
_.pick(this, 'schema', 'form', 'key', 'model', 'value'),
{ id: this.createEditorId() }
);
var constructorFn = this.schema.type;
return new constructorFn(options);
},
/**
* Creates the ID that will be assigned to the editor
*
* @return {String}
*/
createEditorId: function() {
var prefix = this.idPrefix,
id = this.key;
//Replace periods with underscores (e.g. for when using paths)
id = id.replace(/\./g, '_');
//If a specific ID prefix is set, use it
if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
if (_.isNull(prefix)) return id;
//Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
if (this.model) return this.model.cid + '_' + id;
return id;
},
/**
* Create the default field title (label text) from the key name.
* (Converts 'camelCase' to 'Camel Case')
*
* @return {String}
*/
createTitle: function() {
var str = this.key;
//Add spaces
str = str.replace(/([A-Z])/g, ' $1');
//Uppercase first character
str = str.replace(/^./, function(str) { return str.toUpperCase(); });
return str;
},
/**
* Returns the data to be passed to the template
*
* @return {Object}
*/
templateData: function() {
var schema = this.schema;
return {
help: schema.help || '',
title: schema.title,
fieldAttrs: schema.fieldAttrs,
editorAttrs: schema.editorAttrs,
key: this.key,
editorId: this.editor.id
};
},
/**
* Render the field and editor
*
* @return {Field} self
*/
render: function() {
var schema = this.schema,
editor = this.editor,
$ = Backbone.$;
//Only render the editor if Hidden
if (schema.type == Form.editors.Hidden) {
return this.setElement(editor.render().el);
}
//Render field
var $field = $($.trim(this.template(_.result(this, 'templateData'))));
if (schema.fieldClass) $field.addClass(schema.fieldClass);
if (schema.fieldAttrs) $field.attr(schema.fieldAttrs);
//Render editor
$field.find('[data-editor]').add($field).each(function(i, el) {
var $container = $(el),
selection = $container.attr('data-editor');
if (_.isUndefined(selection)) return;
$container.append(editor.render().el);
});
this.setElement($field);
return this;
},
/**
* Check the validity of the field
*
* @return {String}
*/
validate: function() {
var error = this.editor.validate();
if (error) {
this.setError(error.message);
} else {
this.clearError();
}
return error;
},
/**
* Set the field into an error state, adding the error class and setting the error message
*
* @param {String} msg Error message
*/
setError: function(msg) {
//Nested form editors (e.g. Object) set their errors internally
if (this.editor.hasNestedForm) return;
//Add error CSS class
this.$el.addClass(this.errorClassName);
//Set error message
this.$('[data-error]').html(msg);
},
/**
* Clear the error state and reset the help message
*/
clearError: function() {
//Remove error CSS class
this.$el.removeClass(this.errorClassName);
//Clear error message
this.$('[data-error]').empty();
},
/**
* Update the model with the new value from the editor
*
* @return {Mixed}
*/
commit: function() {
return this.editor.commit();
},
/**
* Get the value from the editor
*
* @return {Mixed}
*/
getValue: function() {
return this.editor.getValue();
},
/**
* Set/change the value of the editor
*
* @param {Mixed} value
*/
setValue: function(value) {
this.editor.setValue(value);
},
/**
* Give the editor focus
*/
focus: function() {
this.editor.focus();
},
/**
* Remove focus from the editor
*/
blur: function() {
this.editor.blur();
},
/**
* Remove the field and editor views
*/
remove: function() {
this.editor.remove();
Thorax.View.prototype.remove.call(this);
}
}, {
//STATICS
template: _.template('\
<div>\
<label for="<%= editorId %>"><%= title %></label>\
<div>\
<span data-editor></span>\
<div data-error></div>\
<div><%= help %></div>\
</div>\
</div>\
', null, Form.templateSettings),
/**
* CSS class name added to the field when there is a validation error
*/
errorClassName: 'error'
});
//==================================================================================================
//NESTEDFIELD
//==================================================================================================
Form.NestedField = Form.Field.extend({
template: _.template('\
<div>\
<span data-editor></span>\
<% if (help) { %>\
<div><%= help %></div>\
<% } %>\
<div data-error></div>\
</div>\
', null, Form.templateSettings)
});
/**
* Base editor (interface). To be extended, not used directly
*
* @param {Object} options
* @param {String} [options.id] Editor ID
* @param {Model} [options.model] Use instead of value, and use commit()
* @param {String} [options.key] The model attribute key. Required when using 'model'
* @param {Mixed} [options.value] When not using a model. If neither provided, defaultValue will be used
* @param {Object} [options.schema] Field schema; may be required by some editors
* @param {Object} [options.validators] Validators; falls back to those stored on schema
* @param {Object} [options.form] The form
*/
Form.Editor = Form.editors.Base = Thorax.View.extend({
defaultValue: null,
hasFocus: false,
initialize: function(options) {
var options = options || {};
//Set initial value
if (options.model) {
if (!options.key) throw new Error("Missing option: 'key'");
this.model = options.model;
this.value = this.model.get(options.key);
}
else if (options.value !== undefined) {
this.value = options.value;
}
if (this.value === undefined) this.value = this.defaultValue;
//Store important data
_.extend(this, _.pick(options, 'key', 'form'));
var schema = this.schema = options.schema || {};
this.validators = options.validators || schema.validators;
//Main attributes
this.$el.attr('id', this.id);
this.$el.attr('name', this.getName());
if (schema.editorClass) this.$el.addClass(schema.editorClass);
if (schema.editorAttrs) this.$el.attr(schema.editorAttrs);
},
/**
* Get the value for the form input 'name' attribute
*
* @return {String}
*
* @api private
*/
getName: function() {
var key = this.key || '';
//Replace periods with underscores (e.g. for when using paths)
return key.replace(/\./g, '_');
},
/**
* Get editor value
* Extend and override this method to reflect changes in the DOM
*
* @return {Mixed}
*/
getValue: function() {
return this.value;
},
/**
* Set editor value
* Extend and override this method to reflect changes in the DOM
*
* @param {Mixed} value
*/
setValue: function(value) {
this.value = value;
},
/**
* Give the editor focus
* Extend and override this method
*/
focus: function() {
throw new Error('Not implemented');
},
/**
* Remove focus from the editor
* Extend and override this method
*/
blur: function() {
throw new Error('Not implemented');
},
/**
* Update the model with the current value
*
* @param {Object} [options] Options to pass to model.set()
* @param {Boolean} [options.validate] Set to true to trigger built-in model validation
*
* @return {Mixed} error
*/
commit: function(options) {
var error = this.validate();
if (error) return error;
this.listenTo(this.model, 'invalid', function(model, e) {
error = e;
});
this.model.set(this.key, this.getValue(), options);
if (error) return error;
},
/**
* Check validity
*
* @return {Object|Undefined}
*/
validate: function() {
var $el = this.$el,
error = null,
value = this.getValue(),
formValues = this.form ? this.form.getValue() : {},
validators = this.validators,
getValidator = this.getValidator;
if (validators) {
//Run through validators until an error is found
_.every(validators, function(validator) {
error = getValidator(validator)(value, formValues);
return error ? false : true;
});
}
return error;
},
/**
* Set this.hasFocus, or call parent trigger()
*
* @param {String} event
*/
trigger: function(event) {
if (event === 'focus') {
this.hasFocus = true;
}
else if (event === 'blur') {
this.hasFocus = false;
}
return Thorax.View.prototype.trigger.apply(this, arguments);
},
/**
* Returns a validation function based on the type defined in the schema
*
* @param {RegExp|String|Function} validator
* @return {Function}
*/
getValidator: function(validator) {
var validators = Form.validators;
//Convert regular expressions to validators
if (_.isRegExp(validator)) {
return validators.regexp({ regexp: validator });
}
//Use a built-in validator if given a string
if (_.isString(validator)) {
if (!validators[validator]) throw new Error('Validator "'+validator+'" not found');
return validators[validator]();
}
//Functions can be used directly
if (_.isFunction(validator)) return validator;
//Use a customised built-in validator if given an object
if (_.isObject(validator) && validator.type) {
var config = validator;
return validators[config.type](config);
}
//Unkown validator type
throw new Error('Invalid validator: ' + validator);
}
});
/**
* Text
*
* Text input with focus, blur and change events
*/
Form.editors.Text = Form.Editor.extend({
tagName: 'input',
defaultValue: '',
previousValue: '',
events: {
'keyup': 'determineChange',
'keypress': function(event) {
var self = this;
setTimeout(function() {
self.determineChange();
}, 0);
},
'select': function(event) {
this.trigger('select', this);
},
'focus': function(event) {
this.trigger('focus', this);
},
'blur': function(event) {
this.trigger('blur', this);
}
},
initialize: function(options) {
Form.editors.Base.prototype.initialize.call(this, options);
var schema = this.schema;
//Allow customising text type (email, phone etc.) for HTML5 browsers
var type = 'text';
if (schema && schema.editorAttrs && schema.editorAttrs.type) type = schema.editorAttrs.type;
if (schema && schema.dataType) type = schema.dataType;
this.$el.attr('type', type);
},
/**
* Adds the editor to the DOM
*/
render: function() {
this.setValue(this.value);
return this;
},
determineChange: function(event) {
var currentValue = this.$el.val();
var changed = (currentValue !== this.previousValue);
if (changed) {
this.previousValue = currentValue;
this.trigger('change', this);
}
},
/**
* Returns the current editor value
* @return {String}
*/
getValue: function() {
return this.$el.val();
},
/**
* Sets the value of the form element
* @param {String}
*/
setValue: function(value) {
this.$el.val(value);
},
focus: function() {
if (this.hasFocus) return;
this.$el.focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$el.blur();
},
select: function() {
this.$el.select();
}
});
/**
* TextArea editor
*/
Form.editors.TextArea = Form.editors.Text.extend({
tagName: 'textarea',
/**
* Override Text constructor so type property isn't set (issue #261)
*/
initialize: function(options) {
Form.editors.Base.prototype.initialize.call(this, options);
}
});
/**
* Password editor
*/
Form.editors.Password = Form.editors.Text.extend({
initialize: function(options) {
Form.editors.Text.prototype.initialize.call(this, options);
this.$el.attr('type', 'password');
}
});
/**
* NUMBER
*
* Normal text input that only allows a number. Letters etc. are not entered.
*/
Form.editors.Number = Form.editors.Text.extend({
defaultValue: 0,
events: _.extend({}, Form.editors.Text.prototype.events, {
'keypress': 'onKeyPress',
'change': 'onKeyPress'
}),
initialize: function(options) {
Form.editors.Text.prototype.initialize.call(this, options);
var schema = this.schema;
this.$el.attr('type', 'number');
if (!schema || !schema.editorAttrs || !schema.editorAttrs.step) {
// provide a default for `step` attr,
// but don't overwrite if already specified
this.$el.attr('step', 'any');
}
},
/**
* Check value is numeric
*/
onKeyPress: function(event) {
var self = this,
delayedDetermineChange = function() {
setTimeout(function() {
self.determineChange();
}, 0);
};
//Allow backspace
if (event.charCode === 0) {
delayedDetermineChange();
return;
}
//Get the whole new value so that we can prevent things like double decimals points etc.
var newVal = this.$el.val()
if( event.charCode != undefined ) {
newVal = newVal + String.fromCharCode(event.charCode);
}
var numeric = /^[0-9]*\.?[0-9]*?$/.test(newVal);
if (numeric) {
delayedDetermineChange();
}
else {
event.preventDefault();
}
},
getValue: function() {
var value = this.$el.val();
return value === "" ? null : parseFloat(value, 10);
},
setValue: function(value) {
value = (function() {
if (_.isNumber(value)) return value;
if (_.isString(value) && value !== '') return parseFloat(value, 10);
return null;
})();
if (_.isNaN(value)) value = null;
Form.editors.Text.prototype.setValue.call(this, value);
}
});
/**
* Hidden editor
*/
Form.editors.Hidden = Form.editors.Text.extend({
defaultValue: '',
initialize: function(options) {
Form.editors.Text.prototype.initialize.call(this, options);
this.$el.attr('type', 'hidden');
},
focus: function() {
},
blur: function() {
}
});
/**
* Checkbox editor
*
* Creates a single checkbox, i.e. boolean value
*/
Form.editors.Checkbox = Form.editors.Base.extend({
defaultValue: false,
tagName: 'input',
events: {
'click': function(event) {
this.trigger('change', this);
},
'focus': function(event) {
this.trigger('focus', this);
},
'blur': function(event) {
this.trigger('blur', this);
}
},
initialize: function(options) {
Form.editors.Base.prototype.initialize.call(this, options);
this.$el.attr('type', 'checkbox');
},
/**
* Adds the editor to the DOM
*/
render: function() {
this.setValue(this.value);
return this;
},
getValue: function() {
return this.$el.prop('checked');
},
setValue: function(value) {
if (value) {
this.$el.prop('checked', true);
}else{
this.$el.prop('checked', false);
}
},
focus: function() {
if (this.hasFocus) return;
this.$el.focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$el.blur();
}
});
/**
* Select editor
*
* Renders a <select> with given options
*
* Requires an 'options' value on the schema.
* Can be an array of options, a function that calls back with the array of options, a string of HTML
* or a Backbone collection. If a collection, the models must implement a toString() method
*/
Form.editors.Select = Form.editors.Base.extend({
tagName: 'select',
events: {
'change': function(event) {
this.trigger('change', this);
},
'focus': function(event) {
this.trigger('focus', this);
},
'blur': function(event) {
this.trigger('blur', this);
}
},
initialize: function(options) {
Form.editors.Base.prototype.initialize.call(this, options);
if (!this.schema || !this.schema.options) throw new Error("Missing required 'schema.options'");
},
render: function() {
this.setOptions(this.schema.options);
return this;
},
/**
* Sets the options that populate the <select>
*
* @param {Mixed} options
*/
setOptions: function(options) {
var self = this;
//If a collection was passed, check if it needs fetching
if (options instanceof Thorax.Collection) {
var collection = options;
//Don't do the fetch if it's already populated
if (collection.length > 0) {
this.renderOptions(options);
} else {
collection.fetch({
success: function(collection) {
self.renderOptions(options);
}
});
}
}
//If a function was passed, run it to get the options
else if (_.isFunction(options)) {
options(function(result) {
self.renderOptions(result);
}, self);
}
//Otherwise, ready to go straight to renderOptions
else {
this.renderOptions(options);
}
},
/**
* Adds the <option> html to the DOM
* @param {Mixed} Options as a simple array e.g. ['option1', 'option2']
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
* or as a string of <option> HTML to insert into the <select>
* or any object
*/
renderOptions: function(options) {
var $select = this.$el,
html;
html = this._getOptionsHtml(options);
//Insert options
$select.html(html);
//Select correct option
this.setValue(this.value);
},
_getOptionsHtml: function(options) {
var html;
//Accept string of HTML
if (_.isString(options)) {
html = options;
}
//Or array
else if (_.isArray(options)) {
html = this._arrayToHtml(options);
}
//Or Backbone collection
else if (options instanceof Thorax.Collection) {
html = this._collectionToHtml(options);
}
else if (_.isFunction(options)) {
var newOptions;
options(function(opts) {
newOptions = opts;
}, this);
html = this._getOptionsHtml(newOptions);
//Or any object
}else{
html=this._objectToHtml(options);
}
return html;
},
getValue: function() {
return this.$el.val();
},
setValue: function(value) {
this.$el.val(value);
},
focus: function() {
if (this.hasFocus) return;
this.$el.focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$el.blur();
},
/**
* Transforms a collection into HTML ready to use in the renderOptions method
* @param {Backbone.Collection}
* @return {String}
*/
_collectionToHtml: function(collection) {
//Convert collection to array first
var array = [];
collection.each(function(model) {
array.push({ val: model.id, label: model.toString() });
});
//Now convert to HTML
var html = this._arrayToHtml(array);
return html;
},
/**
* Transforms an object into HTML ready to use in the renderOptions method
* @param {Object}
* @return {String}
*/
_objectToHtml: function(obj) {
//Convert object to array first
var array = [];
for(var key in obj){
if( obj.hasOwnProperty( key ) ) {
array.push({ val: key, label: obj[key] });
}
}
//Now convert to HTML
var html = this._arrayToHtml(array);
return html;
},
/**
* Create the <option> HTML
* @param {Array} Options as a simple array e.g. ['option1', 'option2']
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
* @return {String} HTML
*/
_arrayToHtml: function(array) {
var html = [];
//Generate HTML
_.each(array, function(option) {
if (_.isObject(option)) {
if (option.group) {
html.push('<optgroup label="'+option.group+'">');
html.push(this._getOptionsHtml(option.options))
html.push('</optgroup>');
} else {
var val = (option.val || option.val === 0) ? option.val : '';
html.push('<option value="'+val+'">'+option.label+'</option>');
}
}
else {
html.push('<option>'+option+'</option>');
}
}, this);
return html.join('');
}
});
/**
* Radio editor
*
* Renders a <ul> with given options represented as <li> objects containing radio buttons
*
* Requires an 'options' value on the schema.
* Can be an array of options, a function that calls back with the array of options, a string of HTML
* or a Backbone collection. If a collection, the models must implement a toString() method
*/
Form.editors.Radio = Form.editors.Select.extend({
tagName: 'ul',
events: {
'change input[type=radio]': function() {
this.trigger('change', this);
},
'focus input[type=radio]': function() {
if (this.hasFocus) return;
this.trigger('focus', this);
},
'blur input[type=radio]': function() {
if (!this.hasFocus) return;
var self = this;
setTimeout(function() {
if (self.$('input[type=radio]:focus')[0]) return;
self.trigger('blur', self);
}, 0);
}
},
/**
* Returns the template. Override for custom templates
*
* @return {Function} Compiled template
*/
getTemplate: function() {
return this.schema.template || this.constructor.template;
},
getValue: function() {
return this.$('input[type=radio]:checked').val();
},
setValue: function(value) {
this.$('input[type=radio]').val([value]);
},
focus: function() {
if (this.hasFocus) return;
var checked = this.$('input[type=radio]:checked');
if (checked[0]) {
checked.focus();
return;
}
this.$('input[type=radio]').first().focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$('input[type=radio]:focus').blur();
},
/**
* Create the radio list HTML
* @param {Array} Options as a simple array e.g. ['option1', 'option2']
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
* @return {String} HTML
*/
_arrayToHtml: function (array) {
var self = this;
var template = this.getTemplate(),
name = self.getName(),
id = self.id;
var items = _.map(array, function(option, index) {
var item = {
name: name,
id: id + '-' + index
}
if (_.isObject(option)) {
item.value = (option.val || option.val === 0) ? option.val : '';
item.label = option.label;
} else {
item.value = option;
item.label = option;
}
return item;
});
return template({ items: items });
}
}, {
//STATICS
template: _.template('\
<% _.each(items, function(item) { %>\
<li>\
<input type="radio" name="<%= item.name %>" value="<%= item.value %>" id="<%= item.id %>" />\
<label for="<%= item.id %>"><%= item.label %></label>\
</li>\
<% }); %>\
', null, Form.templateSettings)
});
/**
* Checkboxes editor
*
* Renders a <ul> with given options represented as <li> objects containing checkboxes
*
* Requires an 'options' value on the schema.
* Can be an array of options, a function that calls back with the array of options, a string of HTML
* or a Backbone collection. If a collection, the models must implement a toString() method
*/
Form.editors.Checkboxes = Form.editors.Select.extend({
tagName: 'ul',
groupNumber: 0,
events: {
'click input[type=checkbox]': function() {
this.trigger('change', this);
},
'focus input[type=checkbox]': function() {
if (this.hasFocus) return;
this.trigger('focus', this);
},
'blur input[type=checkbox]': function() {
if (!this.hasFocus) return;
var self = this;
setTimeout(function() {
if (self.$('input[type=checkbox]:focus')[0]) return;
self.trigger('blur', self);
}, 0);
}
},
getValue: function() {
var values = [];
this.$('input[type=checkbox]:checked').each(function() {
values.push($(this).val());
});
return values;
},
setValue: function(values) {
if (!_.isArray(values)) values = [values];
this.$('input[type=checkbox]').val(values);
},
focus: function() {
if (this.hasFocus) return;
this.$('input[type=checkbox]').first().focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$('input[type=checkbox]:focus').blur();
},
/**
* Create the checkbox list HTML
* @param {Array} Options as a simple array e.g. ['option1', 'option2']
* or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
* @return {String} HTML
*/
_arrayToHtml: function (array) {
var html = [];
var self = this;
_.each(array, function(option, index) {
var itemHtml = '<li>';
var close = true;
if (_.isObject(option)) {
if (option.group) {
var originalId = self.id;
self.id += "-" + self.groupNumber++;
itemHtml = ('<fieldset class="group"> <legend>'+option.group+'</legend>');
itemHtml += (self._arrayToHtml(option.options));
itemHtml += ('</fieldset>');
self.id = originalId;
close = false;
}else{
var val = (option.val || option.val === 0) ? option.val : '';
itemHtml += ('<input type="checkbox" name="'+self.getName()+'" value="'+val+'" id="'+self.id+'-'+index+'" />');
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option.label+'</label>');
}
}
else {
itemHtml += ('<input type="checkbox" name="'+self.getName()+'" value="'+option+'" id="'+self.id+'-'+index+'" />');
itemHtml += ('<label for="'+self.id+'-'+index+'">'+option+'</label>');
}
if(close){
itemHtml += '</li>';
}
html.push(itemHtml);
});
return html.join('');
}
});
/**
* Object editor
*
* Creates a child form. For editing Javascript objects
*
* @param {Object} options
* @param {Form} options.form The form this editor belongs to; used to determine the constructor for the nested form
* @param {Object} options.schema The schema for the object
* @param {Object} options.schema.subSchema The schema for the nested form
*/
Form.editors.Object = Form.editors.Base.extend({
//Prevent error classes being set on the main control; they are internally on the individual fields
hasNestedForm: true,
initialize: function(options) {
//Set default value for the instance so it's not a shared object
this.value = {};
//Init
Form.editors.Base.prototype.initialize.call(this, options);
//Check required options
if (!this.form) throw new Error('Missing required option "form"');
if (!this.schema.subSchema) throw new Error("Missing required 'schema.subSchema' option for Object editor");
},
render: function() {
//Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
var NestedForm = this.form.constructor;
//Create the nested form
this.nestedForm = new NestedForm({
schema: this.schema.subSchema,
data: this.value,
idPrefix: this.id + '_',
Field: NestedForm.NestedField
});
this._observeFormEvents();
this.$el.html(this.nestedForm.render().el);
if (this.hasFocus) this.trigger('blur', this);
return this;
},
getValue: function() {
if (this.nestedForm) return this.nestedForm.getValue();
return this.value;
},
setValue: function(value) {
this.value = value;
this.render();
},
focus: function() {
if (this.hasFocus) return;
this.nestedForm.focus();
},
blur: function() {
if (!this.hasFocus) return;
this.nestedForm.blur();
},
remove: function() {
this.nestedForm.remove();
Thorax.View.prototype.remove.call(this);
},
validate: function() {
return this.nestedForm.validate();
},
_observeFormEvents: function() {
if (!this.nestedForm) return;
this.nestedForm.on('all', function() {
// args = ["key:change", form, fieldEditor]
var args = _.toArray(arguments);
args[1] = this;
// args = ["key:change", this=objectEditor, fieldEditor]
this.trigger.apply(this, args);
}, this);
}
});
/**
* NestedModel editor
*
* Creates a child form. For editing nested Backbone models
*
* Special options:
* schema.model: Embedded model constructor
*/
Form.editors.NestedModel = Form.editors.Object.extend({
initialize: function(options) {
Form.editors.Base.prototype.initialize.call(this, options);
if (!this.form) throw new Error('Missing required option "form"');
if (!options.schema.model) throw new Error('Missing required "schema.model" option for NestedModel editor');
},
render: function() {
//Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
var NestedForm = this.form.constructor;
var data = this.value || {},
key = this.key,
nestedModel = this.schema.model;
//Wrap the data in a model if it isn't already a model instance
var modelInstance = (data.constructor === nestedModel) ? data : new nestedModel(data);
this.nestedForm = new NestedForm({
model: modelInstance,
idPrefix: this.id + '_',
fieldTemplate: 'nestedField'
});
this._observeFormEvents();
//Render form
this.$el.html(this.nestedForm.render().el);
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* Update the embedded model, checking for nested validation errors and pass them up
* Then update the main model if all OK
*
* @return {Error|null} Validation error or null
*/
commit: function() {
var error = this.nestedForm.commit();
if (error) {
this.$el.addClass('error');
return error;
}
return Form.editors.Object.prototype.commit.call(this);
}
});
/**
* Date editor
*
* Schema options
* @param {Number|String} [options.schema.yearStart] First year in list. Default: 100 years ago
* @param {Number|String} [options.schema.yearEnd] Last year in list. Default: current year
*
* Config options (if not set, defaults to options stored on the main Date class)
* @param {Boolean} [options.showMonthNames] Use month names instead of numbers. Default: true
* @param {String[]} [options.monthNames] Month names. Default: Full English names
*/
Form.editors.Date = Form.editors.Base.extend({
events: {
'change select': function() {
this.updateHidden();
this.trigger('change', this);
},
'focus select': function() {
if (this.hasFocus) return;
this.trigger('focus', this);
},
'blur select': function() {
if (!this.hasFocus) return;
var self = this;
setTimeout(function() {
if (self.$('select:focus')[0]) return;
self.trigger('blur', self);
}, 0);
}
},
initialize: function(options) {
options = options || {};
Form.editors.Base.prototype.initialize.call(this, options);
var Self = Form.editors.Date,
today = new Date();
//Option defaults
this.options = _.extend({
monthNames: Self.monthNames,
showMonthNames: Self.showMonthNames
}, options);
//Schema defaults
this.schema = _.extend({
yearStart: today.getFullYear() - 100,
yearEnd: today.getFullYear()
}, options.schema || {});
//Cast to Date
if (this.value && !_.isDate(this.value)) {
this.value = new Date(this.value);
}
//Set default date
if (!this.value) {
var date = new Date();
date.setSeconds(0);
date.setMilliseconds(0);
this.value = date;
}
//Template
this.template = options.template || this.constructor.template;
},
render: function() {
var options = this.options,
schema = this.schema,
$ = Backbone.$;
var datesOptions = _.map(_.range(1, 32), function(date) {
return '<option value="'+date+'">' + date + '</option>';
});
var monthsOptions = _.map(_.range(0, 12), function(month) {
var value = (options.showMonthNames)
? options.monthNames[month]
: (month + 1);
return '<option value="'+month+'">' + value + '</option>';
});
var yearRange = (schema.yearStart < schema.yearEnd)
? _.range(schema.yearStart, schema.yearEnd + 1)
: _.range(schema.yearStart, schema.yearEnd - 1, -1);
var yearsOptions = _.map(yearRange, function(year) {
return '<option value="'+year+'">' + year + '</option>';
});
//Render the selects
var $el = $($.trim(this.template({
dates: datesOptions.join(''),
months: monthsOptions.join(''),
years: yearsOptions.join('')
})));
//Store references to selects
this.$date = $el.find('[data-type="date"]');
this.$month = $el.find('[data-type="month"]');
this.$year = $el.find('[data-type="year"]');
//Create the hidden field to store values in case POSTed to server
this.$hidden = $('<input type="hidden" name="'+this.key+'" />');
$el.append(this.$hidden);
//Set value on this and hidden field
this.setValue(this.value);
//Remove the wrapper tag
this.setElement($el);
this.$el.attr('id', this.id);
this.$el.attr('name', this.getName());
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* @return {Date} Selected date
*/
getValue: function() {
var year = this.$year.val(),
month = this.$month.val(),
date = this.$date.val();
if (!year || !month || !date) return null;
return new Date(year, month, date);
},
/**
* @param {Date} date
*/
setValue: function(date) {
this.$date.val(date.getDate());
this.$month.val(date.getMonth());
this.$year.val(date.getFullYear());
this.updateHidden();
},
focus: function() {
if (this.hasFocus) return;
this.$('select').first().focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$('select:focus').blur();
},
/**
* Update the hidden input which is maintained for when submitting a form
* via a normal browser POST
*/
updateHidden: function() {
var val = this.getValue();
if (_.isDate(val)) val = val.toISOString();
this.$hidden.val(val);
}
}, {
//STATICS
template: _.template('\
<div>\
<select data-type="date"><%= dates %></select>\
<select data-type="month"><%= months %></select>\
<select data-type="year"><%= years %></select>\
</div>\
', null, Form.templateSettings),
//Whether to show month names instead of numbers
showMonthNames: true,
//Month names to use if showMonthNames is true
//Replace for localisation, e.g. Form.editors.Date.monthNames = ['Janvier', 'Fevrier'...]
monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
});
/**
* DateTime editor
*
* @param {Editor} [options.DateEditor] Date editor view to use (not definition)
* @param {Number} [options.schema.minsInterval] Interval between minutes. Default: 15
*/
Form.editors.DateTime = Form.editors.Base.extend({
events: {
'change select': function() {
this.updateHidden();
this.trigger('change', this);
},
'focus select': function() {
if (this.hasFocus) return;
this.trigger('focus', this);
},
'blur select': function() {
if (!this.hasFocus) return;
var self = this;
setTimeout(function() {
if (self.$('select:focus')[0]) return;
self.trigger('blur', self);
}, 0);
}
},
initialize: function(options) {
options = options || {};
Form.editors.Base.prototype.initialize.call(this, options);
//Option defaults
this.options = _.extend({
DateEditor: Form.editors.DateTime.DateEditor
}, options);
//Schema defaults
this.schema = _.extend({
minsInterval: 15
}, options.schema || {});
//Create embedded date editor
this.dateEditor = new this.options.DateEditor(options);
this.value = this.dateEditor.value;
//Template
this.template = options.template || this.constructor.template;
},
render: function() {
function pad(n) {
return n < 10 ? '0' + n : n;
}
var schema = this.schema,
$ = Backbone.$;
//Create options
var hoursOptions = _.map(_.range(0, 24), function(hour) {
return '<option value="'+hour+'">' + pad(hour) + '</option>';
});
var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) {
return '<option value="'+min+'">' + pad(min) + '</option>';
});
//Render time selects
var $el = $($.trim(this.template({
hours: hoursOptions.join(),
mins: minsOptions.join()
})));
//Include the date editor
$el.find('[data-date]').append(this.dateEditor.render().el);
//Store references to selects
this.$hour = $el.find('select[data-type="hour"]');
this.$min = $el.find('select[data-type="min"]');
//Get the hidden date field to store values in case POSTed to server
this.$hidden = $el.find('input[type="hidden"]');
//Set time
this.setValue(this.value);
this.setElement($el);
this.$el.attr('id', this.id);
this.$el.attr('name', this.getName());
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* @return {Date} Selected datetime
*/
getValue: function() {
var date = this.dateEditor.getValue();
var hour = this.$hour.val(),
min = this.$min.val();
if (!date || !hour || !min) return null;
date.setHours(hour);
date.setMinutes(min);
return date;
},
/**
* @param {Date}
*/
setValue: function(date) {
if (!_.isDate(date)) date = new Date(date);
this.dateEditor.setValue(date);
this.$hour.val(date.getHours());
this.$min.val(date.getMinutes());
this.updateHidden();
},
focus: function() {
if (this.hasFocus) return;
this.$('select').first().focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$('select:focus').blur();
},
/**
* Update the hidden input which is maintained for when submitting a form
* via a normal browser POST
*/
updateHidden: function() {
var val = this.getValue();
if (_.isDate(val)) val = val.toISOString();
this.$hidden.val(val);
},
/**
* Remove the Date editor before removing self
*/
remove: function() {
this.dateEditor.remove();
Form.editors.Base.prototype.remove.call(this);
}
}, {
//STATICS
template: _.template('\
<div class="bbf-datetime">\
<div class="bbf-date-container" data-date></div>\
<select data-type="hour"><%= hours %></select>\
:\
<select data-type="min"><%= mins %></select>\
</div>\
', null, Form.templateSettings),
//The date editor to use (constructor function, not instance)
DateEditor: Form.editors.Date
});
//Metadata
Form.VERSION = '0.14.0';
//Exports
Backbone.Form = Form;
if (typeof module !== 'undefined') module.exports = Form;
})(window || global || this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment