/public/js/libs/plugins/backbone.forms.js
JavaScript | 2479 lines | 1358 code | 507 blank | 614 comment | 244 complexity | ae6ccc58c2ec628e11af2536900b5dd5 MD5 | raw file
- /**
- * Backbone Forms v0.14.0
- *
- * NOTE:
- * This version is for use with RequireJS
- * If using regular <script> tags to include your files, use backbone-forms.min.js
- *
- * Copyright (c) 2013 Charles Davison, Pow Media Ltd
- *
- * License and more information at:
- * http://github.com/powmedia/backbone-forms
- */
- define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
- //==================================================================================================
- //FORM
- //==================================================================================================
- var Form = Backbone.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 Backbone.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 Backbone.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 = Backbone.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();
- });
- Backbone.View.prototype.remove.call(this);
- }
- }, {
- //STATICS
- template: _.template('\
- <fieldset data-fields>\
- <% if (legend) { %>\
- <legend><%= legend %></legend>\
- <% } %>\
- </fieldset>\
- ', null, Form.templateSettings)
- });
- //==================================================================================================
- //FIELD
- //==================================================================================================
- Form.Field = Backbone.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();
- Backbone.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 = Backbone.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 Backbone.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 Backbone.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 Backbone.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('\
- <ul>\
- <% _.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>\
- <% }); %>\
- </ul>\
- ', 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();
- Backbone.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;
- return Form;
- });