/**
* Backbone Forms v0.10.1
*
* Copyright (c) 2012 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.jQuery || root.Zepto || root.ender || require('jquery'),
_ = root._ || require('underscore'),
Backbone = root.Backbone || require('backbone');
}
//Browser
else {
var $ = root.jQuery,
_ = root._,
Backbone = root.Backbone;
}
//SOURCE
//==================================================================================================
//FORM
//==================================================================================================
var Form = (function() {
return Backbone.View.extend({
hasFocus: false,
/**
* Creates a new form
*
* @param {Object} options
* @param {Model} [options.model] Model the form relates to. Required if options.data is not set
* @param {Object} [options.data] Date to populate the form. Required if options.model is not set
* @param {String[]} [options.fields] Fields to include in the form, in order
* @param {String[]|Object[]} [options.fieldsets] How to divide the fields up by section. E.g. [{ legend: 'Title', fields: ['field1', 'field2'] }]
* @param {String} [options.idPrefix] Prefix for editor IDs. By default, the model's CID is used.
* @param {String} [options.template] Form template key/name
* @param {String} [options.fieldsetTemplate] Fieldset template key/name
* @param {String} [options.fieldTemplate] Field template key/name
*
* @return {Form}
*/
initialize: function(options) {
//Check templates have been loaded
if (!Form.templates.form) throw new Error('Templates not loaded');
//Get the schema
this.schema = (function() {
if (options.schema) return options.schema;
var model = options.model;
if (!model) throw new Error('Could not find schema');
if (_.isFunction(model.schema)) return model.schema();
return model.schema;
})();
//Option defaults
options = _.extend({
template: 'form',
fieldsetTemplate: 'fieldset',
fieldTemplate: 'field'
}, options);
//Determine fieldsets
if (!options.fieldsets) {
var fields = options.fields || _.keys(this.schema);
options.fieldsets = [{ fields: fields }];
}
//Store main attributes
this.options = options;
this.model = options.model;
this.data = options.data;
this.fields = {};
},
/**
* Renders the form and all fields
*/
render: function() {
var self = this,
options = this.options,
template = Form.templates[options.template];
//Create el from template
var $form = $(template({
fieldsets: ''
}));
//Render fieldsets
var $fieldsetContainer = $('.bbf-tmp', $form);
_.each(options.fieldsets, function(fieldset) {
$fieldsetContainer.append(self.renderFieldset(fieldset));
});
$fieldsetContainer.children().unwrap();
//Set the template contents as the main element; removes the wrapper element
this.setElement($form);
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* Renders a fieldset and the fields within it
*
* Valid fieldset definitions:
* ['field1', 'field2']
* { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
*
* @param {Object|Array} fieldset A fieldset definition
*
* @return {jQuery} The fieldset DOM element
*/
renderFieldset: function(fieldset) {
var self = this,
template = Form.templates[this.options.fieldsetTemplate],
schema = this.schema,
getNested = Form.helpers.getNested;
//Normalise to object
if (_.isArray(fieldset)) {
fieldset = { fields: fieldset };
}
//Concatenating HTML as strings won't work so we need to insert field elements into a placeholder
var $fieldset = $(template(_.extend({}, fieldset, {
legend: '',
fields: ''
})));
//Set legend
if (fieldset.legend) {
$fieldset.find('.bbf-tmp-legend').replaceWith(fieldset.legend);
}
//or remove the containing tag if there isn't a legend
else {
$fieldset.find('.bbf-tmp-legend').parent().remove();
}
var $fieldsContainer = $('.bbf-tmp-fields', $fieldset);
//Render fields
_.each(fieldset.fields, function(key) {
//Get the field schema
var itemSchema = (function() {
//Return a normal key or path key
if (schema[key]) return schema[key];
//Return a nested schema, i.e. Object
var path = key.replace(/\./g, '.subSchema.');
return getNested(schema, path);
})();
if (!itemSchema) throw "Field '"+key+"' not found in schema";
//Create the field
var field = self.fields[key] = self.createField(key, itemSchema);
//Render the fields with editors, apart from Hidden fields
var fieldEl = field.render().el;
field.editor.on('all', function(event) {
// args = ["change", editor]
args = _.toArray(arguments);
args[0] = key + ':' + event;
args.splice(1, 0, this);
// args = ["key:change", this=form, editor]
this.trigger.apply(this, args)
}, self);
field.editor.on('change', function() {
this.trigger('change', self);
}, self);
field.editor.on('focus', function() {
if (this.hasFocus) return;
this.trigger('focus', this);
}, self);
field.editor.on('blur', function() {
if (!this.hasFocus) return;
var self = this;
setTimeout(function() {
if (_.find(self.fields, function(field) { return field.editor.hasFocus; })) return;
self.trigger('blur', self);
}, 0);
}, self);
if (itemSchema.type != 'Hidden') {
$fieldsContainer.append(fieldEl);
}
});
$fieldsContainer = $fieldsContainer.children().unwrap()
return $fieldset;
},
/**
* Renders a field and returns it
*
* @param {String} key The key for the field in the form schema
* @param {Object} schema Field schema
*
* @return {Field} The field view
*/
createField: function(key, schema) {
schema.template = schema.template || this.options.fieldTemplate;
var options = {
form: this,
key: key,
schema: schema,
idPrefix: this.options.idPrefix,
template: this.options.fieldTemplate
};
if (this.model) {
options.model = this.model;
} else if (this.data) {
options.value = this.data[key];
} else {
options.value = null;
}
return new Form.Field(options);
},
/**
* Validate the data
*
* @return {Object} Validation errors
*/
validate: function() {
var self = this,
fields = this.fields,
model = this.model,
errors = {};
//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 (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 (self.fields[key] && !errors[key]) {
self.fields[key].setError(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.
*
* @return {Object} Validation errors
*/
commit: function() {
//Validate
var errors = this.validate();
if (errors) return errors;
//Commit
var modelError;
this.model.set(this.getValue(), {
error: function(model, e) {
modelError = e;
}
});
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} data New values to set
*/
setValue: function(data) {
for (var key in data) {
if (_.has(this.fields, key)) {
this.fields[key].setValue(data[key]);
}
}
},
focus: function() {
if (this.hasFocus) return;
var fieldset = this.options.fieldsets[0];
if (fieldset) {
var field;
if (_.isArray(fieldset)) {
field = fieldset[0];
}
else {
field = fieldset.fields[0];
}
if (field) {
this.fields[field].editor.focus();
}
}
},
blur: function() {
if (!this.hasFocus) return;
focusedField = _.find(this.fields, function(field) { return field.editor.hasFocus; });
if (focusedField) focusedField.editor.blur();
},
/**
* Override default remove function in order to remove embedded views
*/
remove: function() {
var fields = this.fields;
for (var key in fields) {
fields[key].remove();
}
Backbone.View.prototype.remove.call(this);
},
trigger: function(event) {
if (event == 'focus') {
this.hasFocus = true;
}
else if (event == 'blur') {
this.hasFocus = false;
}
return Backbone.View.prototype.trigger.apply(this, arguments);
}
});
})();
//==================================================================================================
//HELPERS
//==================================================================================================
Form.helpers = (function() {
var helpers = {};
/**
* Gets a nested attribute using a path e.g. 'user.name'
*
* @param {Object} obj Object to fetch attribute from
* @param {String} path Attribute path e.g. 'user.name'
* @return {Mixed}
* @api private
*/
helpers.getNested = function(obj, path) {
var fields = path.split(".");
var result = obj;
for (var i = 0, n = fields.length; i < n; i++) {
result = result[fields[i]];
}
return result;
}
/**
* This function is used to transform the key from a schema into the title used in a label.
* (If a specific title is provided it will be used instead).
*
* By default this converts a camelCase string into words, i.e. Camel Case
* If you have a different naming convention for schema keys, replace this function.
*
* @param {String} Key
* @return {String} Title
*/
helpers.keyToTitle = function(str) {
//Add spaces
str = str.replace(/([A-Z])/g, ' $1');
//Uppercase first character
str = str.replace(/^./, function(str) { return str.toUpperCase(); });
return str;
};
/**
* Helper to compile a template with the {{mustache}} style tags. Template settings are reset
* to user's settings when done to avoid conflicts.
* @param {String} Template string
* @return {Template} Compiled template
*/
helpers.compileTemplate = function(str) {
//Store user's template options
var _interpolateBackup = _.templateSettings.interpolate;
//Set custom template settings
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
var template = _.template(str);
//Reset to users' template settings
_.templateSettings.interpolate = _interpolateBackup;
return template;
}
/**
* Helper to create a template with the {{mustache}} style tags.
* If context is passed in, the template will be evaluated.
* @param {String} Template string
* @param {Object} Optional; values to replace in template
* @return {Template|String} Compiled template or the evaluated string
*/
helpers.createTemplate = function(str, context) {
var template = helpers.compileTemplate(str);
if (!context) {
return template;
} else {
return template(context);
}
};
/**
* Sets the template compiler to the given function
* @param {Function} Template compiler function
*/
helpers.setTemplateCompiler = function(compiler) {
helpers.compileTemplate = compiler;
}
/**
* Sets the templates to be used.
*
* If the templates passed in are strings, they will be compiled, expecting Mustache style tags,
* i.e.
{{varName}}
*
* You can also pass in previously compiled Underscore templates, in which case you can use any style
* tags.
*
* @param {Object} templates
* @param {Object} classNames
*/
helpers.setTemplates = function(templates, classNames) {
var createTemplate = helpers.createTemplate;
Form.templates = Form.templates || {};
Form.classNames = Form.classNames || {};
//Set templates, compiling them if necessary
_.each(templates, function(template, key, index) {
if (_.isString(template)) template = createTemplate(template);
Form.templates[key] = template;
});
//Set class names
_.extend(Form.classNames, classNames);
};
/**
* Return the editor constructor for a given schema 'type'.
* Accepts strings for the default editors, or the reference to the constructor function
* for custom editors
*
* @param {String|Function} The schema type e.g. 'Text', 'Select', or the editor constructor e.g. editors.Date
* @param {Object} Options to pass to editor, including required 'key', 'schema'
* @return {Mixed} An instance of the mapped editor
*/
helpers.createEditor = function(schemaType, options) {
var constructorFn;
if (_.isString(schemaType)) {
constructorFn = Form.editors[schemaType];
} else {
constructorFn = schemaType;
}
return new constructorFn(options);
};
/**
* Triggers an event that can be cancelled. Requires the user to invoke a callback. If false
* is passed to the callback, the action does not run.
*
* NOTE: This helper uses private Backbone apis so can break when Backbone is upgraded
*
* @param {Mixed} Instance of Backbone model, view, collection to trigger event on
* @param {String} Event name
* @param {Array} Arguments to pass to the event handlers
* @param {Function} Callback to run after the event handler has run.
* If any of them passed false or error, this callback won't run
*/
helpers.triggerCancellableEvent = function(subject, event, args, callback) {
//Return if there are no event listeners
if (!subject._callbacks || !subject._callbacks[event]) return callback();
var next = subject._callbacks[event].next;
if (!next) return callback();
var fn = next.callback,
context = next.context || this;
//Add the callback that will be used when done
args.push(callback);
fn.apply(context, args);
}
/**
* Returns a validation function based on the type defined in the schema
*
* @param {RegExp|String|Function} validator
* @return {Function}
*/
helpers.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);
};
return helpers;
})();
//==================================================================================================
//VALIDATORS
//==================================================================================================
Form.validators = (function() {
var validators = {};
validators.errMessages = {
required: 'Required',
regexp: 'Invalid',
email: 'Invalid email address',
url: 'Invalid URL',
match: 'Must match field "{{field}}"'
}
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: Form.helpers.createTemplate(options.message, options)
};
if (value === null || value === undefined || value === '') return err;
};
};
validators.regexp = function(options) {
if (!options.regexp) throw new Error('Missing required "regexp" option for "regexp" validator');
options = _.extend({
type: 'regexp',
message: this.errMessages.regexp
}, options);
return function regexp(value) {
options.value = value;
var err = {
type: options.type,
message: Form.helpers.createTemplate(options.message, options)
};
//Don't check empty values (add a 'required' validator for this)
if (value === null || value === undefined || value === '') return;
if (!options.regexp.test(value)) return err;
};
};
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: Form.helpers.createTemplate(options.message, options)
};
//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;
})();
//==================================================================================================
//FIELD
//==================================================================================================
Form.Field = (function() {
var helpers = Form.helpers,
templates = Form.templates;
return Backbone.View.extend({
/**
* @param {Object} Options
* Required:
* key {String} : The model attribute key
* Optional:
* schema {Object} : Schema for the field
* value {Mixed} : Pass value when not using a model. Use getValue() to get out value
* model {Backbone.Model} : Use instead of value, and use commit().
* idPrefix {String} : Prefix to add to the editor DOM element's ID
*/
/**
* Creates a new field
*
* @param {Object} options
* @param {Object} [options.schema] Field schema. Defaults to { type: 'Text' }
* @param {Model} [options.model] Model the field relates to. Required if options.data is not set.
* @param {String} [options.key] Model key/attribute the field relates to.
* @param {Mixed} [options.value] Field value. Required if options.model is not set.
* @param {String} [options.idPrefix] Prefix for the editor ID. By default, the model's CID is used.
*
* @return {Field}
*/
initialize: function(options) {
options = options || {};
this.form = options.form;
this.key = options.key;
this.value = options.value;
this.model = options.model;
//Turn schema shorthand notation (e.g. 'Text') into schema object
if (_.isString(options.schema)) options.schema = { type: options.schema };
//Set schema defaults
this.schema = _.extend({
type: 'Text',
title: helpers.keyToTitle(this.key),
template: 'field'
}, options.schema);
},
/**
* Renders the field
*/
render: function() {
var schema = this.schema,
templates = Form.templates;
//Standard options that will go to all editors
var options = {
form: this.form,
key: this.key,
schema: schema,
idPrefix: this.options.idPrefix,
id: this.getId()
};
//Decide on data delivery type to pass to editors
if (this.model) {
options.model = this.model;
} else {
options.value = this.value;
}
//Decide on the editor to use
var editor = this.editor = helpers.createEditor(schema.type, options);
//Create the element
var $field = $(templates[schema.template]({
key: this.key,
title: schema.title,
id: editor.id,
type: schema.type,
editor: '',
help: ''
}));
//Render editor
$field.find('.bbf-tmp-editor').replaceWith(editor.render().el);
//Set help text
this.$help = $('.bbf-tmp-help', $field).parent();
this.$help.empty();
if (this.schema.help) this.$help.html(this.schema.help);
//Add custom CSS class names
if (this.schema.fieldClass) $field.addClass(this.schema.fieldClass);
//Add custom attributes
if (this.schema.fieldAttrs) $field.attr(this.schema.fieldAttrs);
//Replace the generated wrapper tag
this.setElement($field);
return this;
},
/**
* Creates the ID that will be assigned to the editor
*
* @return {String}
*/
getId: function() {
var prefix = this.options.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;
},
/**
* 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) {
//Object and NestedModel types set their own errors internally
if (this.editor.hasNestedForm) return;
var errClass = Form.classNames.error;
this.$el.addClass(errClass);
if (this.$help) this.$help.html(msg);
},
/**
* Clear the error state and reset the help message
*/
clearError: function() {
var errClass = Form.classNames.error;
this.$el.removeClass(errClass);
// some fields (e.g., Hidden), may not have a help el
if (this.$help) {
this.$help.empty();
//Reset help text if available
var helpMsg = this.schema.help;
if (helpMsg) this.$help.html(helpMsg);
}
},
/**
* Update the model with the new value from the editor
*/
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);
},
focus: function() {
this.editor.focus();
},
blur: function() {
this.editor.blur();
},
/**
* Remove the field and editor views
*/
remove: function() {
this.editor.remove();
Backbone.View.prototype.remove.call(this);
}
});
})();
//========================================================================
//EDITORS
//========================================================================
Form.editors = (function() {
var helpers = Form.helpers;
var editors = {};
/**
* Base editor (interface). To be extended, not used directly
*
* @param {Object} Options
* Optional:
* model {Backbone.Model} : Use instead of value, and use commit().
* key {String} : The model attribute key. Required when using 'model'
* value {String} : When not using a model. If neither provided, defaultValue will be used.
* schema {Object} : May be required by some editors
*/
editors.Base = Backbone.View.extend({
defaultValue: null,
hasFocus: false,
initialize: function(options) {
var options = options || {};
if (options.model) {
if (!options.key) throw "Missing option: 'key'";
this.model = options.model;
this.value = this.model.get(options.key);
}
else if (options.value) {
this.value = options.value;
}
if (this.value === undefined) this.value = this.defaultValue;
this.key = options.key;
this.form = options.form;
this.schema = options.schema || {};
this.validators = options.validators || this.schema.validators;
//Main attributes
this.$el.attr('name', this.getName());
//Add custom CSS class names
if (this.schema.editorClass) this.$el.addClass(this.schema.editorClass);
//Add custom attributes
if (this.schema.editorAttrs) this.$el.attr(this.schema.editorAttrs);
},
getValue: function() {
throw 'Not implemented. Extend and override this method.';
},
setValue: function() {
throw 'Not implemented. Extend and override this method.';
},
focus: function() {
throw 'Not implemented. Extend and override this method.';
},
blur: function() {
throw 'Not implemented. Extend and override this method.';
},
/**
* 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, '_')
},
/**
* Update the model with the current value
* NOTE: The method is defined on the editors so that they can be used independently of fields
*
* @return {Mixed} error
*/
commit: function() {
var error = this.validate();
if (error) return error;
this.model.set(this.key, this.getValue(), {
error: function(model, e) {
error = e;
}
});
if (error) return error;
},
/**
* Check validity
* NOTE: The method is defined on the editors so that they can be used independently of fields
*
* @return {String}
*/
validate: function() {
var $el = this.$el,
error = null,
value = this.getValue(),
formValues = this.form ? this.form.getValue() : {},
validators = this.validators,
getValidator = Form.helpers.getValidator;
if (validators) {
//Run through validators until an error is found
_.every(validators, function(validator) {
error = getValidator(validator)(value, formValues);
return continueLoop = error ? false : true;
});
}
return error;
},
trigger: function(event) {
if (event == 'focus') {
this.hasFocus = true;
}
else if (event == 'blur') {
this.hasFocus = false;
}
return Backbone.View.prototype.trigger.apply(this, arguments);
}
});
//TEXT
editors.Text = editors.Base.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) {
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();
}
});
/**
* NUMBER
* Normal text input that only allows a number. Letters etc. are not entered
*/
editors.Number = editors.Text.extend({
defaultValue: 0,
events: _.extend({}, editors.Text.prototype.events, {
'keypress': 'onKeyPress'
}),
initialize: function(options) {
editors.Text.prototype.initialize.call(this, options);
this.$el.attr('type', 'number');
},
/**
* 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() + 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;
editors.Text.prototype.setValue.call(this, value);
}
});
//PASSWORD
editors.Password = editors.Text.extend({
initialize: function(options) {
editors.Text.prototype.initialize.call(this, options);
this.$el.attr('type', 'password');
}
});
//TEXTAREA
editors.TextArea = editors.Text.extend({
tagName: 'textarea'
});
//CHECKBOX
editors.Checkbox = 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) {
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);
}
},
focus: function() {
if (this.hasFocus) return;
this.$el.focus();
},
blur: function() {
if (!this.hasFocus) return;
this.$el.blur();
}
});
//HIDDEN
editors.Hidden = editors.Base.extend({
defaultValue: '',
initialize: function(options) {
editors.Text.prototype.initialize.call(this, options);
this.$el.attr('type', 'hidden');
},
getValue: function() {
return this.value;
},
setValue: function(value) {
this.value = value;
},
focus: function() {
},
blur: function() {
}
});
/**
* SELECT
*
* Renders a