PageRenderTime 66ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/public/js/libs/plugins/backbone.forms.js

https://github.com/Reflen/spinel
JavaScript | 2479 lines | 1358 code | 507 blank | 614 comment | 244 complexity | ae6ccc58c2ec628e11af2536900b5dd5 MD5 | raw file
  1. /**
  2. * Backbone Forms v0.14.0
  3. *
  4. * NOTE:
  5. * This version is for use with RequireJS
  6. * If using regular <script> tags to include your files, use backbone-forms.min.js
  7. *
  8. * Copyright (c) 2013 Charles Davison, Pow Media Ltd
  9. *
  10. * License and more information at:
  11. * http://github.com/powmedia/backbone-forms
  12. */
  13. define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
  14. //==================================================================================================
  15. //FORM
  16. //==================================================================================================
  17. var Form = Backbone.View.extend({
  18. events: {
  19. 'submit': function(event) {
  20. this.trigger('submit', event);
  21. }
  22. },
  23. /**
  24. * Constructor
  25. *
  26. * @param {Object} [options.schema]
  27. * @param {Backbone.Model} [options.model]
  28. * @param {Object} [options.data]
  29. * @param {String[]|Object[]} [options.fieldsets]
  30. * @param {String[]} [options.fields]
  31. * @param {String} [options.idPrefix]
  32. * @param {Form.Field} [options.Field]
  33. * @param {Form.Fieldset} [options.Fieldset]
  34. * @param {Function} [options.template]
  35. */
  36. initialize: function(options) {
  37. var self = this;
  38. options = options || {};
  39. //Find the schema to use
  40. var schema = this.schema = (function() {
  41. //Prefer schema from options
  42. if (options.schema) return _.result(options, 'schema');
  43. //Then schema on model
  44. var model = options.model;
  45. if (model && model.schema) return _.result(model, 'schema');
  46. //Then built-in schema
  47. if (self.schema) return _.result(self, 'schema');
  48. //Fallback to empty schema
  49. return {};
  50. })();
  51. //Store important data
  52. _.extend(this, _.pick(options, 'model', 'data', 'idPrefix', 'templateData'));
  53. //Override defaults
  54. var constructor = this.constructor;
  55. this.template = options.template || this.template || constructor.template;
  56. this.Fieldset = options.Fieldset || this.Fieldset || constructor.Fieldset;
  57. this.Field = options.Field || this.Field || constructor.Field;
  58. this.NestedField = options.NestedField || this.NestedField || constructor.NestedField;
  59. //Check which fields will be included (defaults to all)
  60. var selectedFields = this.selectedFields = options.fields || _.keys(schema);
  61. //Create fields
  62. var fields = this.fields = {};
  63. _.each(selectedFields, function(key) {
  64. var fieldSchema = schema[key];
  65. fields[key] = this.createField(key, fieldSchema);
  66. }, this);
  67. //Create fieldsets
  68. var fieldsetSchema = options.fieldsets || _.result(this, 'fieldsets') || [selectedFields],
  69. fieldsets = this.fieldsets = [];
  70. _.each(fieldsetSchema, function(itemSchema) {
  71. this.fieldsets.push(this.createFieldset(itemSchema));
  72. }, this);
  73. },
  74. /**
  75. * Creates a Fieldset instance
  76. *
  77. * @param {String[]|Object[]} schema Fieldset schema
  78. *
  79. * @return {Form.Fieldset}
  80. */
  81. createFieldset: function(schema) {
  82. var options = {
  83. schema: schema,
  84. fields: this.fields
  85. };
  86. return new this.Fieldset(options);
  87. },
  88. /**
  89. * Creates a Field instance
  90. *
  91. * @param {String} key
  92. * @param {Object} schema Field schema
  93. *
  94. * @return {Form.Field}
  95. */
  96. createField: function(key, schema) {
  97. var options = {
  98. form: this,
  99. key: key,
  100. schema: schema,
  101. idPrefix: this.idPrefix
  102. };
  103. if (this.model) {
  104. options.model = this.model;
  105. } else if (this.data) {
  106. options.value = this.data[key];
  107. } else {
  108. options.value = null;
  109. }
  110. var field = new this.Field(options);
  111. this.listenTo(field.editor, 'all', this.handleEditorEvent);
  112. return field;
  113. },
  114. /**
  115. * Callback for when an editor event is fired.
  116. * Re-triggers events on the form as key:event and triggers additional form-level events
  117. *
  118. * @param {String} event
  119. * @param {Editor} editor
  120. */
  121. handleEditorEvent: function(event, editor) {
  122. //Re-trigger editor events on the form
  123. var formEvent = editor.key + ':' + event;
  124. this.trigger.call(this, formEvent, this, editor, Array.prototype.slice.call(arguments, 2));
  125. //Trigger additional events
  126. switch (event) {
  127. case 'change':
  128. this.trigger('change', this);
  129. break;
  130. case 'focus':
  131. if (!this.hasFocus) this.trigger('focus', this);
  132. break;
  133. case 'blur':
  134. if (this.hasFocus) {
  135. //TODO: Is the timeout etc needed?
  136. var self = this;
  137. setTimeout(function() {
  138. var focusedField = _.find(self.fields, function(field) {
  139. return field.editor.hasFocus;
  140. });
  141. if (!focusedField) self.trigger('blur', self);
  142. }, 0);
  143. }
  144. break;
  145. }
  146. },
  147. render: function() {
  148. var self = this,
  149. fields = this.fields,
  150. $ = Backbone.$;
  151. //Render form
  152. var $form = $($.trim(this.template(_.result(this, 'templateData'))));
  153. //Render standalone editors
  154. $form.find('[data-editors]').add($form).each(function(i, el) {
  155. var $container = $(el),
  156. selection = $container.attr('data-editors');
  157. if (_.isUndefined(selection)) return;
  158. //Work out which fields to include
  159. var keys = (selection == '*') ? self.selectedFields || _.keys(fields) : selection.split(',');
  160. //Add them
  161. _.each(keys, function(key) {
  162. var field = fields[key];
  163. $container.append(field.editor.render().el);
  164. });
  165. });
  166. //Render standalone fields
  167. $form.find('[data-fields]').add($form).each(function(i, el) {
  168. var $container = $(el),
  169. selection = $container.attr('data-fields');
  170. if (_.isUndefined(selection)) return;
  171. //Work out which fields to include
  172. var keys = (selection == '*') ? self.selectedFields || _.keys(fields) : selection.split(',');
  173. //Add them
  174. _.each(keys, function(key) {
  175. var field = fields[key];
  176. $container.append(field.render().el);
  177. });
  178. });
  179. //Render fieldsets
  180. $form.find('[data-fieldsets]').add($form).each(function(i, el) {
  181. var $container = $(el),
  182. selection = $container.attr('data-fieldsets');
  183. if (_.isUndefined(selection)) return;
  184. _.each(self.fieldsets, function(fieldset) {
  185. $container.append(fieldset.render().el);
  186. });
  187. });
  188. //Set the main element
  189. this.setElement($form);
  190. //Set class
  191. $form.addClass(this.className);
  192. return this;
  193. },
  194. /**
  195. * Validate the data
  196. *
  197. * @return {Object} Validation errors
  198. */
  199. validate: function(options) {
  200. var self = this,
  201. fields = this.fields,
  202. model = this.model,
  203. errors = {};
  204. options = options || {};
  205. //Collect errors from schema validation
  206. _.each(fields, function(field) {
  207. var error = field.validate();
  208. if (error) {
  209. errors[field.key] = error;
  210. }
  211. });
  212. //Get errors from default Backbone model validator
  213. if (!options.skipModelValidate && model && model.validate) {
  214. var modelErrors = model.validate(this.getValue());
  215. if (modelErrors) {
  216. var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
  217. //If errors are not in object form then just store on the error object
  218. if (!isDictionary) {
  219. errors._others = errors._others || [];
  220. errors._others.push(modelErrors);
  221. }
  222. //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
  223. if (isDictionary) {
  224. _.each(modelErrors, function(val, key) {
  225. //Set error on field if there isn't one already
  226. if (fields[key] && !errors[key]) {
  227. fields[key].setError(val);
  228. errors[key] = val;
  229. } else {
  230. //Otherwise add to '_others' key
  231. errors._others = errors._others || [];
  232. var tmpErr = {};
  233. tmpErr[key] = val;
  234. errors._others.push(tmpErr);
  235. }
  236. });
  237. }
  238. }
  239. }
  240. return _.isEmpty(errors) ? null : errors;
  241. },
  242. /**
  243. * Update the model with all latest values.
  244. *
  245. * @param {Object} [options] Options to pass to Model#set (e.g. { silent: true })
  246. *
  247. * @return {Object} Validation errors
  248. */
  249. commit: function(options) {
  250. //Validate
  251. options = options || {};
  252. var validateOptions = {
  253. skipModelValidate: !options.validate
  254. };
  255. var errors = this.validate(validateOptions);
  256. if (errors) return errors;
  257. //Commit
  258. var modelError;
  259. var setOptions = _.extend({
  260. error: function(model, e) {
  261. modelError = e;
  262. }
  263. }, options);
  264. this.model.set(this.getValue(), setOptions);
  265. if (modelError) return modelError;
  266. },
  267. /**
  268. * Get all the field values as an object.
  269. * Use this method when passing data instead of objects
  270. *
  271. * @param {String} [key] Specific field value to get
  272. */
  273. getValue: function(key) {
  274. //Return only given key if specified
  275. if (key) return this.fields[key].getValue();
  276. //Otherwise return entire form
  277. var values = {};
  278. _.each(this.fields, function(field) {
  279. values[field.key] = field.getValue();
  280. });
  281. return values;
  282. },
  283. /**
  284. * Update field values, referenced by key
  285. *
  286. * @param {Object|String} key New values to set, or property to set
  287. * @param val Value to set
  288. */
  289. setValue: function(prop, val) {
  290. var data = {};
  291. if (typeof prop === 'string') {
  292. data[prop] = val;
  293. } else {
  294. data = prop;
  295. }
  296. var key;
  297. for (key in this.schema) {
  298. if (data[key] !== undefined) {
  299. this.fields[key].setValue(data[key]);
  300. }
  301. }
  302. },
  303. /**
  304. * Returns the editor for a given field key
  305. *
  306. * @param {String} key
  307. *
  308. * @return {Editor}
  309. */
  310. getEditor: function(key) {
  311. var field = this.fields[key];
  312. if (!field) throw new Error('Field not found: ' + key);
  313. return field.editor;
  314. },
  315. /**
  316. * Gives the first editor in the form focus
  317. */
  318. focus: function() {
  319. if (this.hasFocus) return;
  320. //Get the first field
  321. var fieldset = this.fieldsets[0],
  322. field = fieldset.getFieldAt(0);
  323. if (!field) return;
  324. //Set focus
  325. field.editor.focus();
  326. },
  327. /**
  328. * Removes focus from the currently focused editor
  329. */
  330. blur: function() {
  331. if (!this.hasFocus) return;
  332. var focusedField = _.find(this.fields, function(field) {
  333. return field.editor.hasFocus;
  334. });
  335. if (focusedField) focusedField.editor.blur();
  336. },
  337. /**
  338. * Manages the hasFocus property
  339. *
  340. * @param {String} event
  341. */
  342. trigger: function(event) {
  343. if (event === 'focus') {
  344. this.hasFocus = true;
  345. } else if (event === 'blur') {
  346. this.hasFocus = false;
  347. }
  348. return Backbone.View.prototype.trigger.apply(this, arguments);
  349. },
  350. /**
  351. * Override default remove function in order to remove embedded views
  352. *
  353. * TODO: If editors are included directly with data-editors="x", they need to be removed
  354. * May be best to use XView to manage adding/removing views
  355. */
  356. remove: function() {
  357. _.each(this.fieldsets, function(fieldset) {
  358. fieldset.remove();
  359. });
  360. _.each(this.fields, function(field) {
  361. field.remove();
  362. });
  363. return Backbone.View.prototype.remove.apply(this, arguments);
  364. }
  365. }, {
  366. //STATICS
  367. template: _.template('\
  368. <form data-fieldsets></form>\
  369. ', null, this.templateSettings),
  370. templateSettings: {
  371. evaluate: /<%([\s\S]+?)%>/g,
  372. interpolate: /<%=([\s\S]+?)%>/g,
  373. escape: /<%-([\s\S]+?)%>/g
  374. },
  375. editors: {}
  376. });
  377. //==================================================================================================
  378. //VALIDATORS
  379. //==================================================================================================
  380. Form.validators = (function() {
  381. var validators = {};
  382. validators.errMessages = {
  383. required: 'Required',
  384. regexp: 'Invalid',
  385. number: 'Must be a number',
  386. email: 'Invalid email address',
  387. url: 'Invalid URL',
  388. match: _.template('Must match field "<%= field %>"', null, Form.templateSettings)
  389. };
  390. validators.required = function(options) {
  391. options = _.extend({
  392. type: 'required',
  393. message: this.errMessages.required
  394. }, options);
  395. return function required(value) {
  396. options.value = value;
  397. var err = {
  398. type: options.type,
  399. message: _.isFunction(options.message) ? options.message(options) : options.message
  400. };
  401. if (value === null || value === undefined || value === false || value === '') return err;
  402. };
  403. };
  404. validators.regexp = function(options) {
  405. if (!options.regexp) throw new Error('Missing required "regexp" option for "regexp" validator');
  406. options = _.extend({
  407. type: 'regexp',
  408. match: true,
  409. message: this.errMessages.regexp
  410. }, options);
  411. return function regexp(value) {
  412. options.value = value;
  413. var err = {
  414. type: options.type,
  415. message: _.isFunction(options.message) ? options.message(options) : options.message
  416. };
  417. //Don't check empty values (add a 'required' validator for this)
  418. if (value === null || value === undefined || value === '') return;
  419. //Create RegExp from string if it's valid
  420. if ('string' === typeof options.regexp) options.regexp = new RegExp(options.regexp, options.flags);
  421. if ((options.match) ? !options.regexp.test(value) : options.regexp.test(value)) return err;
  422. };
  423. };
  424. validators.number = function(options) {
  425. options = _.extend({
  426. type: 'number',
  427. message: this.errMessages.number,
  428. regexp: /^[0-9]*\.?[0-9]*?$/
  429. }, options);
  430. return validators.regexp(options);
  431. };
  432. validators.email = function(options) {
  433. options = _.extend({
  434. type: 'email',
  435. message: this.errMessages.email,
  436. regexp: /^[\w\-]{1,}([\w\-\+.]{1,1}[\w\-]{1,}){0,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/
  437. }, options);
  438. return validators.regexp(options);
  439. };
  440. validators.url = function(options) {
  441. options = _.extend({
  442. type: 'url',
  443. message: this.errMessages.url,
  444. regexp: /^(http|https):\/\/(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
  445. }, options);
  446. return validators.regexp(options);
  447. };
  448. validators.match = function(options) {
  449. if (!options.field) throw new Error('Missing required "field" options for "match" validator');
  450. options = _.extend({
  451. type: 'match',
  452. message: this.errMessages.match
  453. }, options);
  454. return function match(value, attrs) {
  455. options.value = value;
  456. var err = {
  457. type: options.type,
  458. message: _.isFunction(options.message) ? options.message(options) : options.message
  459. };
  460. //Don't check empty values (add a 'required' validator for this)
  461. if (value === null || value === undefined || value === '') return;
  462. if (value !== attrs[options.field]) return err;
  463. };
  464. };
  465. return validators;
  466. })();
  467. //==================================================================================================
  468. //FIELDSET
  469. //==================================================================================================
  470. Form.Fieldset = Backbone.View.extend({
  471. /**
  472. * Constructor
  473. *
  474. * Valid fieldset schemas:
  475. * ['field1', 'field2']
  476. * { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
  477. *
  478. * @param {String[]|Object[]} options.schema Fieldset schema
  479. * @param {Object} options.fields Form fields
  480. */
  481. initialize: function(options) {
  482. options = options || {};
  483. //Create the full fieldset schema, merging defaults etc.
  484. var schema = this.schema = this.createSchema(options.schema);
  485. //Store the fields for this fieldset
  486. this.fields = _.pick(options.fields, schema.fields);
  487. //Override defaults
  488. this.template = options.template || schema.template || this.template || this.constructor.template;
  489. },
  490. /**
  491. * Creates the full fieldset schema, normalising, merging defaults etc.
  492. *
  493. * @param {String[]|Object[]} schema
  494. *
  495. * @return {Object}
  496. */
  497. createSchema: function(schema) {
  498. //Normalise to object
  499. if (_.isArray(schema)) {
  500. schema = {
  501. fields: schema
  502. };
  503. }
  504. //Add null legend to prevent template error
  505. schema.legend = schema.legend || null;
  506. return schema;
  507. },
  508. /**
  509. * Returns the field for a given index
  510. *
  511. * @param {Number} index
  512. *
  513. * @return {Field}
  514. */
  515. getFieldAt: function(index) {
  516. var key = this.schema.fields[index];
  517. return this.fields[key];
  518. },
  519. /**
  520. * Returns data to pass to template
  521. *
  522. * @return {Object}
  523. */
  524. templateData: function() {
  525. return this.schema;
  526. },
  527. /**
  528. * Renders the fieldset and fields
  529. *
  530. * @return {Fieldset} this
  531. */
  532. render: function() {
  533. var schema = this.schema,
  534. fields = this.fields,
  535. $ = Backbone.$;
  536. //Render fieldset
  537. var $fieldset = $($.trim(this.template(_.result(this, 'templateData'))));
  538. //Render fields
  539. $fieldset.find('[data-fields]').add($fieldset).each(function(i, el) {
  540. var $container = $(el),
  541. selection = $container.attr('data-fields');
  542. if (_.isUndefined(selection)) return;
  543. _.each(fields, function(field) {
  544. $container.append(field.render().el);
  545. });
  546. });
  547. this.setElement($fieldset);
  548. return this;
  549. },
  550. /**
  551. * Remove embedded views then self
  552. */
  553. remove: function() {
  554. _.each(this.fields, function(field) {
  555. field.remove();
  556. });
  557. Backbone.View.prototype.remove.call(this);
  558. }
  559. }, {
  560. //STATICS
  561. template: _.template('\
  562. <fieldset data-fields>\
  563. <% if (legend) { %>\
  564. <legend><%= legend %></legend>\
  565. <% } %>\
  566. </fieldset>\
  567. ', null, Form.templateSettings)
  568. });
  569. //==================================================================================================
  570. //FIELD
  571. //==================================================================================================
  572. Form.Field = Backbone.View.extend({
  573. /**
  574. * Constructor
  575. *
  576. * @param {Object} options.key
  577. * @param {Object} options.form
  578. * @param {Object} [options.schema]
  579. * @param {Function} [options.schema.template]
  580. * @param {Backbone.Model} [options.model]
  581. * @param {Object} [options.value]
  582. * @param {String} [options.idPrefix]
  583. * @param {Function} [options.template]
  584. * @param {Function} [options.errorClassName]
  585. */
  586. initialize: function(options) {
  587. options = options || {};
  588. //Store important data
  589. _.extend(this, _.pick(options, 'form', 'key', 'model', 'value', 'idPrefix'));
  590. //Create the full field schema, merging defaults etc.
  591. var schema = this.schema = this.createSchema(options.schema);
  592. //Override defaults
  593. this.template = options.template || schema.template || this.template || this.constructor.template;
  594. this.errorClassName = options.errorClassName || this.errorClassName || this.constructor.errorClassName;
  595. //Create editor
  596. this.editor = this.createEditor();
  597. },
  598. /**
  599. * Creates the full field schema, merging defaults etc.
  600. *
  601. * @param {Object|String} schema
  602. *
  603. * @return {Object}
  604. */
  605. createSchema: function(schema) {
  606. if (_.isString(schema)) schema = {
  607. type: schema
  608. };
  609. //Set defaults
  610. schema = _.extend({
  611. type: 'Text',
  612. title: this.createTitle()
  613. }, schema);
  614. //Get the real constructor function i.e. if type is a string such as 'Text'
  615. schema.type = (_.isString(schema.type)) ? Form.editors[schema.type] : schema.type;
  616. return schema;
  617. },
  618. /**
  619. * Creates the editor specified in the schema; either an editor string name or
  620. * a constructor function
  621. *
  622. * @return {View}
  623. */
  624. createEditor: function() {
  625. var options = _.extend(
  626. _.pick(this, 'schema', 'form', 'key', 'model', 'value'), {
  627. id: this.createEditorId()
  628. }
  629. );
  630. var constructorFn = this.schema.type;
  631. return new constructorFn(options);
  632. },
  633. /**
  634. * Creates the ID that will be assigned to the editor
  635. *
  636. * @return {String}
  637. */
  638. createEditorId: function() {
  639. var prefix = this.idPrefix,
  640. id = this.key;
  641. //Replace periods with underscores (e.g. for when using paths)
  642. id = id.replace(/\./g, '_');
  643. //If a specific ID prefix is set, use it
  644. if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
  645. if (_.isNull(prefix)) return id;
  646. //Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
  647. if (this.model) return this.model.cid + '_' + id;
  648. return id;
  649. },
  650. /**
  651. * Create the default field title (label text) from the key name.
  652. * (Converts 'camelCase' to 'Camel Case')
  653. *
  654. * @return {String}
  655. */
  656. createTitle: function() {
  657. var str = this.key;
  658. //Add spaces
  659. str = str.replace(/([A-Z])/g, ' $1');
  660. //Uppercase first character
  661. str = str.replace(/^./, function(str) {
  662. return str.toUpperCase();
  663. });
  664. return str;
  665. },
  666. /**
  667. * Returns the data to be passed to the template
  668. *
  669. * @return {Object}
  670. */
  671. templateData: function() {
  672. var schema = this.schema;
  673. return {
  674. help: schema.help || '',
  675. title: schema.title,
  676. fieldAttrs: schema.fieldAttrs,
  677. editorAttrs: schema.editorAttrs,
  678. key: this.key,
  679. editorId: this.editor.id
  680. };
  681. },
  682. /**
  683. * Render the field and editor
  684. *
  685. * @return {Field} self
  686. */
  687. render: function() {
  688. var schema = this.schema,
  689. editor = this.editor,
  690. $ = Backbone.$;
  691. //Only render the editor if Hidden
  692. if (schema.type == Form.editors.Hidden) {
  693. return this.setElement(editor.render().el);
  694. }
  695. //Render field
  696. var $field = $($.trim(this.template(_.result(this, 'templateData'))));
  697. if (schema.fieldClass) $field.addClass(schema.fieldClass);
  698. if (schema.fieldAttrs) $field.attr(schema.fieldAttrs);
  699. //Render editor
  700. $field.find('[data-editor]').add($field).each(function(i, el) {
  701. var $container = $(el),
  702. selection = $container.attr('data-editor');
  703. if (_.isUndefined(selection)) return;
  704. $container.append(editor.render().el);
  705. });
  706. this.setElement($field);
  707. return this;
  708. },
  709. /**
  710. * Check the validity of the field
  711. *
  712. * @return {String}
  713. */
  714. validate: function() {
  715. var error = this.editor.validate();
  716. if (error) {
  717. this.setError(error.message);
  718. } else {
  719. this.clearError();
  720. }
  721. return error;
  722. },
  723. /**
  724. * Set the field into an error state, adding the error class and setting the error message
  725. *
  726. * @param {String} msg Error message
  727. */
  728. setError: function(msg) {
  729. //Nested form editors (e.g. Object) set their errors internally
  730. if (this.editor.hasNestedForm) return;
  731. //Add error CSS class
  732. this.$el.addClass(this.errorClassName);
  733. //Set error message
  734. this.$('[data-error]').html(msg);
  735. },
  736. /**
  737. * Clear the error state and reset the help message
  738. */
  739. clearError: function() {
  740. //Remove error CSS class
  741. this.$el.removeClass(this.errorClassName);
  742. //Clear error message
  743. this.$('[data-error]').empty();
  744. },
  745. /**
  746. * Update the model with the new value from the editor
  747. *
  748. * @return {Mixed}
  749. */
  750. commit: function() {
  751. return this.editor.commit();
  752. },
  753. /**
  754. * Get the value from the editor
  755. *
  756. * @return {Mixed}
  757. */
  758. getValue: function() {
  759. return this.editor.getValue();
  760. },
  761. /**
  762. * Set/change the value of the editor
  763. *
  764. * @param {Mixed} value
  765. */
  766. setValue: function(value) {
  767. this.editor.setValue(value);
  768. },
  769. /**
  770. * Give the editor focus
  771. */
  772. focus: function() {
  773. this.editor.focus();
  774. },
  775. /**
  776. * Remove focus from the editor
  777. */
  778. blur: function() {
  779. this.editor.blur();
  780. },
  781. /**
  782. * Remove the field and editor views
  783. */
  784. remove: function() {
  785. this.editor.remove();
  786. Backbone.View.prototype.remove.call(this);
  787. }
  788. }, {
  789. //STATICS
  790. template: _.template('\
  791. <div>\
  792. <label for="<%= editorId %>"><%= title %></label>\
  793. <div>\
  794. <span data-editor></span>\
  795. <div data-error></div>\
  796. <div><%= help %></div>\
  797. </div>\
  798. </div>\
  799. ', null, Form.templateSettings),
  800. /**
  801. * CSS class name added to the field when there is a validation error
  802. */
  803. errorClassName: 'error'
  804. });
  805. //==================================================================================================
  806. //NESTEDFIELD
  807. //==================================================================================================
  808. Form.NestedField = Form.Field.extend({
  809. template: _.template('\
  810. <div>\
  811. <span data-editor></span>\
  812. <% if (help) { %>\
  813. <div><%= help %></div>\
  814. <% } %>\
  815. <div data-error></div>\
  816. </div>\
  817. ', null, Form.templateSettings)
  818. });
  819. /**
  820. * Base editor (interface). To be extended, not used directly
  821. *
  822. * @param {Object} options
  823. * @param {String} [options.id] Editor ID
  824. * @param {Model} [options.model] Use instead of value, and use commit()
  825. * @param {String} [options.key] The model attribute key. Required when using 'model'
  826. * @param {Mixed} [options.value] When not using a model. If neither provided, defaultValue will be used
  827. * @param {Object} [options.schema] Field schema; may be required by some editors
  828. * @param {Object} [options.validators] Validators; falls back to those stored on schema
  829. * @param {Object} [options.form] The form
  830. */
  831. Form.Editor = Form.editors.Base = Backbone.View.extend({
  832. defaultValue: null,
  833. hasFocus: false,
  834. initialize: function(options) {
  835. var options = options || {};
  836. //Set initial value
  837. if (options.model) {
  838. if (!options.key) throw new Error("Missing option: 'key'");
  839. this.model = options.model;
  840. this.value = this.model.get(options.key);
  841. } else if (options.value !== undefined) {
  842. this.value = options.value;
  843. }
  844. if (this.value === undefined) this.value = this.defaultValue;
  845. //Store important data
  846. _.extend(this, _.pick(options, 'key', 'form'));
  847. var schema = this.schema = options.schema || {};
  848. this.validators = options.validators || schema.validators;
  849. //Main attributes
  850. this.$el.attr('id', this.id);
  851. this.$el.attr('name', this.getName());
  852. if (schema.editorClass) this.$el.addClass(schema.editorClass);
  853. if (schema.editorAttrs) this.$el.attr(schema.editorAttrs);
  854. },
  855. /**
  856. * Get the value for the form input 'name' attribute
  857. *
  858. * @return {String}
  859. *
  860. * @api private
  861. */
  862. getName: function() {
  863. var key = this.key || '';
  864. //Replace periods with underscores (e.g. for when using paths)
  865. return key.replace(/\./g, '_');
  866. },
  867. /**
  868. * Get editor value
  869. * Extend and override this method to reflect changes in the DOM
  870. *
  871. * @return {Mixed}
  872. */
  873. getValue: function() {
  874. return this.value;
  875. },
  876. /**
  877. * Set editor value
  878. * Extend and override this method to reflect changes in the DOM
  879. *
  880. * @param {Mixed} value
  881. */
  882. setValue: function(value) {
  883. this.value = value;
  884. },
  885. /**
  886. * Give the editor focus
  887. * Extend and override this method
  888. */
  889. focus: function() {
  890. throw new Error('Not implemented');
  891. },
  892. /**
  893. * Remove focus from the editor
  894. * Extend and override this method
  895. */
  896. blur: function() {
  897. throw new Error('Not implemented');
  898. },
  899. /**
  900. * Update the model with the current value
  901. *
  902. * @param {Object} [options] Options to pass to model.set()
  903. * @param {Boolean} [options.validate] Set to true to trigger built-in model validation
  904. *
  905. * @return {Mixed} error
  906. */
  907. commit: function(options) {
  908. var error = this.validate();
  909. if (error) return error;
  910. this.listenTo(this.model, 'invalid', function(model, e) {
  911. error = e;
  912. });
  913. this.model.set(this.key, this.getValue(), options);
  914. if (error) return error;
  915. },
  916. /**
  917. * Check validity
  918. *
  919. * @return {Object|Undefined}
  920. */
  921. validate: function() {
  922. var $el = this.$el,
  923. error = null,
  924. value = this.getValue(),
  925. formValues = this.form ? this.form.getValue() : {},
  926. validators = this.validators,
  927. getValidator = this.getValidator;
  928. if (validators) {
  929. //Run through validators until an error is found
  930. _.every(validators, function(validator) {
  931. error = getValidator(validator)(value, formValues);
  932. return error ? false : true;
  933. });
  934. }
  935. return error;
  936. },
  937. /**
  938. * Set this.hasFocus, or call parent trigger()
  939. *
  940. * @param {String} event
  941. */
  942. trigger: function(event) {
  943. if (event === 'focus') {
  944. this.hasFocus = true;
  945. } else if (event === 'blur') {
  946. this.hasFocus = false;
  947. }
  948. return Backbone.View.prototype.trigger.apply(this, arguments);
  949. },
  950. /**
  951. * Returns a validation function based on the type defined in the schema
  952. *
  953. * @param {RegExp|String|Function} validator
  954. * @return {Function}
  955. */
  956. getValidator: function(validator) {
  957. var validators = Form.validators;
  958. //Convert regular expressions to validators
  959. if (_.isRegExp(validator)) {
  960. return validators.regexp({
  961. regexp: validator
  962. });
  963. }
  964. //Use a built-in validator if given a string
  965. if (_.isString(validator)) {
  966. if (!validators[validator]) throw new Error('Validator "' + validator + '" not found');
  967. return validators[validator]();
  968. }
  969. //Functions can be used directly
  970. if (_.isFunction(validator)) return validator;
  971. //Use a customised built-in validator if given an object
  972. if (_.isObject(validator) && validator.type) {
  973. var config = validator;
  974. return validators[config.type](config);
  975. }
  976. //Unkown validator type
  977. throw new Error('Invalid validator: ' + validator);
  978. }
  979. });
  980. /**
  981. * Text
  982. *
  983. * Text input with focus, blur and change events
  984. */
  985. Form.editors.Text = Form.Editor.extend({
  986. tagName: 'input',
  987. defaultValue: '',
  988. previousValue: '',
  989. events: {
  990. 'keyup': 'determineChange',
  991. 'keypress': function(event) {
  992. var self = this;
  993. setTimeout(function() {
  994. self.determineChange();
  995. }, 0);
  996. },
  997. 'select': function(event) {
  998. this.trigger('select', this);
  999. },
  1000. 'focus': function(event) {
  1001. this.trigger('focus', this);
  1002. },
  1003. 'blur': function(event) {
  1004. this.trigger('blur', this);
  1005. }
  1006. },
  1007. initialize: function(options) {
  1008. Form.editors.Base.prototype.initialize.call(this, options);
  1009. var schema = this.schema;
  1010. //Allow customising text type (email, phone etc.) for HTML5 browsers
  1011. var type = 'text';
  1012. if (schema && schema.editorAttrs && schema.editorAttrs.type) type = schema.editorAttrs.type;
  1013. if (schema && schema.dataType) type = schema.dataType;
  1014. this.$el.attr('type', type);
  1015. },
  1016. /**
  1017. * Adds the editor to the DOM
  1018. */
  1019. render: function() {
  1020. this.setValue(this.value);
  1021. return this;
  1022. },
  1023. determineChange: function(event) {
  1024. var currentValue = this.$el.val();
  1025. var changed = (currentValue !== this.previousValue);
  1026. if (changed) {
  1027. this.previousValue = currentValue;
  1028. this.trigger('change', this);
  1029. }
  1030. },
  1031. /**
  1032. * Returns the current editor value
  1033. * @return {String}
  1034. */
  1035. getValue: function() {
  1036. return this.$el.val();
  1037. },
  1038. /**
  1039. * Sets the value of the form element
  1040. * @param {String}
  1041. */
  1042. setValue: function(value) {
  1043. this.$el.val(value);
  1044. },
  1045. focus: function() {
  1046. if (this.hasFocus) return;
  1047. this.$el.focus();
  1048. },
  1049. blur: function() {
  1050. if (!this.hasFocus) return;
  1051. this.$el.blur();
  1052. },
  1053. select: function() {
  1054. this.$el.select();
  1055. }
  1056. });
  1057. /**
  1058. * TextArea editor
  1059. */
  1060. Form.editors.TextArea = Form.editors.Text.extend({
  1061. tagName: 'textarea',
  1062. /**
  1063. * Override Text constructor so type property isn't set (issue #261)
  1064. */
  1065. initialize: function(options) {
  1066. Form.editors.Base.prototype.initialize.call(this, options);
  1067. }
  1068. });
  1069. /**
  1070. * Password editor
  1071. */
  1072. Form.editors.Password = Form.editors.Text.extend({
  1073. initialize: function(options) {
  1074. Form.editors.Text.prototype.initialize.call(this, options);
  1075. this.$el.attr('type', 'password');
  1076. }
  1077. });
  1078. /**
  1079. * NUMBER
  1080. *
  1081. * Normal text input that only allows a number. Letters etc. are not entered.
  1082. */
  1083. Form.editors.Number = Form.editors.Text.extend({
  1084. defaultValue: 0,
  1085. events: _.extend({}, Form.editors.Text.prototype.events, {
  1086. 'keypress': 'onKeyPress',
  1087. 'change': 'onKeyPress'
  1088. }),
  1089. initialize: function(options) {
  1090. Form.editors.Text.prototype.initialize.call(this, options);
  1091. var schema = this.schema;
  1092. this.$el.attr('type', 'number');
  1093. if (!schema || !schema.editorAttrs || !schema.editorAttrs.step) {
  1094. // provide a default for `step` attr,
  1095. // but don't overwrite if already specified
  1096. this.$el.attr('step', 'any');
  1097. }
  1098. },
  1099. /**
  1100. * Check value is numeric
  1101. */
  1102. onKeyPress: function(event) {
  1103. var self = this,
  1104. delayedDetermineChange = function() {
  1105. setTimeout(function() {
  1106. self.determineChange();
  1107. }, 0);
  1108. };
  1109. //Allow backspace
  1110. if (event.charCode === 0) {
  1111. delayedDetermineChange();
  1112. return;
  1113. }
  1114. //Get the whole new value so that we can prevent things like double decimals points etc.
  1115. var newVal = this.$el.val()
  1116. if (event.charCode != undefined) {
  1117. newVal = newVal + String.fromCharCode(event.charCode);
  1118. }
  1119. var numeric = /^[0-9]*\.?[0-9]*?$/.test(newVal);
  1120. if (numeric) {
  1121. delayedDetermineChange();
  1122. } else {
  1123. event.preventDefault();
  1124. }
  1125. },
  1126. getValue: function() {
  1127. var value = this.$el.val();
  1128. return value === "" ? null : parseFloat(value, 10);
  1129. },
  1130. setValue: function(value) {
  1131. value = (function() {
  1132. if (_.isNumber(value)) return value;
  1133. if (_.isString(value) && value !== '') return parseFloat(value, 10);
  1134. return null;
  1135. })();
  1136. if (_.isNaN(value)) value = null;
  1137. Form.editors.Text.prototype.setValue.call(this, value);
  1138. }
  1139. });
  1140. /**
  1141. * Hidden editor
  1142. */
  1143. Form.editors.Hidden = Form.editors.Text.extend({
  1144. defaultValue: '',
  1145. initialize: function(options) {
  1146. Form.editors.Text.prototype.initialize.call(this, options);
  1147. this.$el.attr('type', 'hidden');
  1148. },
  1149. focus: function() {
  1150. },
  1151. blur: function() {
  1152. }
  1153. });
  1154. /**
  1155. * Checkbox editor
  1156. *
  1157. * Creates a single checkbox, i.e. boolean value
  1158. */
  1159. Form.editors.Checkbox = Form.editors.Base.extend({
  1160. defaultValue: false,
  1161. tagName: 'input',
  1162. events: {
  1163. 'click': function(event) {
  1164. this.trigger('change', this);
  1165. },
  1166. 'focus': function(event) {
  1167. this.trigger('focus', this);
  1168. },
  1169. 'blur': function(event) {
  1170. this.trigger('blur', this);
  1171. }
  1172. },
  1173. initialize: function(options) {
  1174. Form.editors.Base.prototype.initialize.call(this, options);
  1175. this.$el.attr('type', 'checkbox');
  1176. },
  1177. /**
  1178. * Adds the editor to the DOM
  1179. */
  1180. render: function() {
  1181. this.setValue(this.value);
  1182. return this;
  1183. },
  1184. getValue: function() {
  1185. return this.$el.prop('checked');
  1186. },
  1187. setValue: function(value) {
  1188. if (value) {
  1189. this.$el.prop('checked', true);
  1190. } else {
  1191. this.$el.prop('checked', false);
  1192. }
  1193. },
  1194. focus: function() {
  1195. if (this.hasFocus) return;
  1196. this.$el.focus();
  1197. },
  1198. blur: function() {
  1199. if (!this.hasFocus) return;
  1200. this.$el.blur();
  1201. }
  1202. });
  1203. /**
  1204. * Select editor
  1205. *
  1206. * Renders a <select> with given options
  1207. *
  1208. * Requires an 'options' value on the schema.
  1209. * Can be an array of options, a function that calls back with the array of options, a string of HTML
  1210. * or a Backbone collection. If a collection, the models must implement a toString() method
  1211. */
  1212. Form.editors.Select = Form.editors.Base.extend({
  1213. tagName: 'select',
  1214. events: {
  1215. 'change': function(event) {
  1216. this.trigger('change', this);
  1217. },
  1218. 'focus': function(event) {
  1219. this.trigger('focus', this);
  1220. },
  1221. 'blur': function(event) {
  1222. this.trigger('blur', this);
  1223. }
  1224. },
  1225. initialize: function(options) {
  1226. Form.editors.Base.prototype.initialize.call(this, options);
  1227. if (!this.schema || !this.schema.options) throw new Error("Missing required 'schema.options'");
  1228. },
  1229. render: function() {
  1230. this.setOptions(this.schema.options);
  1231. return this;
  1232. },
  1233. /**
  1234. * Sets the options that populate the <select>
  1235. *
  1236. * @param {Mixed} options
  1237. */
  1238. setOptions: function(options) {
  1239. var self = this;
  1240. //If a collection was passed, check if it needs fetching
  1241. if (options instanceof Backbone.Collection) {
  1242. var collection = options;
  1243. //Don't do the fetch if it's already populated
  1244. if (collection.length > 0) {
  1245. this.renderOptions(options);
  1246. } else {
  1247. collection.fetch({
  1248. success: function(collection) {
  1249. self.renderOptions(options);
  1250. }
  1251. });
  1252. }
  1253. }
  1254. //If a function was passed, run it to get the options
  1255. else if (_.isFunction(options)) {
  1256. options(function(result) {
  1257. self.renderOptions(result);
  1258. }, self);
  1259. }
  1260. //Otherwise, ready to go straight to renderOptions
  1261. else {
  1262. this.renderOptions(options);
  1263. }
  1264. },
  1265. /**
  1266. * Adds the <option> html to the DOM
  1267. * @param {Mixed} Options as a simple array e.g. ['option1', 'option2']
  1268. * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
  1269. * or as a string of <option> HTML to insert into the <select>
  1270. * or any object
  1271. */
  1272. renderOptions: function(options) {
  1273. var $select = this.$el,
  1274. html;
  1275. html = this._getOptionsHtml(options);
  1276. //Insert options
  1277. $select.html(html);
  1278. //Select correct option
  1279. this.setValue(this.value);
  1280. },
  1281. _getOptionsHtml: function(options) {
  1282. var html;
  1283. //Accept string of HTML
  1284. if (_.isString(options)) {
  1285. html = options;
  1286. }
  1287. //Or array
  1288. else if (_.isArray(options)) {
  1289. html = this._arrayToHtml(options);
  1290. }
  1291. //Or Backbone collection
  1292. else if (options instanceof Backbone.Collection) {
  1293. html = this._collectionToHtml(options);
  1294. } else if (_.isFunction(options)) {
  1295. var newOptions;
  1296. options(function(opts) {
  1297. newOptions = opts;
  1298. }, this);
  1299. html = this._getOptionsHtml(newOptions);
  1300. //Or any object
  1301. } else {
  1302. html = this._objectToHtml(options);
  1303. }
  1304. return html;
  1305. },
  1306. getValue: function() {
  1307. return this.$el.val();
  1308. },
  1309. setValue: function(value) {
  1310. this.$el.val(value);
  1311. },
  1312. focus: function() {
  1313. if (this.hasFocus) return;
  1314. this.$el.focus();
  1315. },
  1316. blur: function() {
  1317. if (!this.hasFocus) return;
  1318. this.$el.blur();
  1319. },
  1320. /**
  1321. * Transforms a collection into HTML ready to use in the renderOptions method
  1322. * @param {Backbone.Collection}
  1323. * @return {String}
  1324. */
  1325. _collectionToHtml: function(collection) {
  1326. //Convert collection to array first
  1327. var array = [];
  1328. collection.each(function(model) {
  1329. array.push({
  1330. val: model.id,
  1331. label: model.toString()
  1332. });
  1333. });
  1334. //Now convert to HTML
  1335. var html = this._arrayToHtml(array);
  1336. return html;
  1337. },
  1338. /**
  1339. * Transforms an object into HTML ready to use in the renderOptions method
  1340. * @param {Object}
  1341. * @return {String}
  1342. */
  1343. _objectToHtml: function(obj) {
  1344. //Convert object to array first
  1345. var array = [];
  1346. for (var key in obj) {
  1347. if (obj.hasOwnProperty(key)) {
  1348. array.push({
  1349. val: key,
  1350. label: obj[key]
  1351. });
  1352. }
  1353. }
  1354. //Now convert to HTML
  1355. var html = this._arrayToHtml(array);
  1356. return html;
  1357. },
  1358. /**
  1359. * Create the <option> HTML
  1360. * @param {Array} Options as a simple array e.g. ['option1', 'option2']
  1361. * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
  1362. * @return {String} HTML
  1363. */
  1364. _arrayToHtml: function(array) {
  1365. var html = [];
  1366. //Generate HTML
  1367. _.each(array, function(option) {
  1368. if (_.isObject(option)) {
  1369. if (option.group) {
  1370. html.push('<optgroup label="' + option.group + '">');
  1371. html.push(this._getOptionsHtml(option.options))
  1372. html.push('</optgroup>');
  1373. } else {
  1374. var val = (option.val || option.val === 0) ? option.val : '';
  1375. html.push('<option value="' + val + '">' + option.label + '</option>');
  1376. }
  1377. } else {
  1378. html.push('<option>' + option + '</option>');
  1379. }
  1380. }, this);
  1381. return html.join('');
  1382. }
  1383. });
  1384. /**
  1385. * Radio editor
  1386. *
  1387. * Renders a <ul> with given options represented as <li> objects containing radio buttons
  1388. *
  1389. * Requires an 'options' value on the schema.
  1390. * Can be an array of options, a function that calls back with the array of options, a string of HTML
  1391. * or a Backbone collection. If a collection, the models must implement a toString() method
  1392. */
  1393. Form.editors.Radio = Form.editors.Select.extend({
  1394. tagName: 'ul',
  1395. events: {
  1396. 'change input[type=radio]': function() {
  1397. this.trigger('change', this);
  1398. },
  1399. 'focus input[type=radio]': function() {
  1400. if (this.hasFocus) return;
  1401. this.trigger('focus', this);
  1402. },
  1403. 'blur input[type=radio]': function() {
  1404. if (!this.hasFocus) return;
  1405. var self = this;
  1406. setTimeout(function() {
  1407. if (self.$('input[type=radio]:focus')[0]) return;
  1408. self.trigger('blur', self);
  1409. }, 0);
  1410. }
  1411. },
  1412. /**
  1413. * Returns the template. Override for custom templates
  1414. *
  1415. * @return {Function} Compiled template
  1416. */
  1417. getTemplate: function() {
  1418. return this.schema.template || this.constructor.template;
  1419. },
  1420. getValue: function() {
  1421. return this.$('input[type=radio]:checked').val();
  1422. },
  1423. setValue: function(value) {
  1424. this.$('input[type=radio]').val([value]);
  1425. },
  1426. focus: function() {
  1427. if (this.hasFocus) return;
  1428. var checked = this.$('input[type=radio]:checked');
  1429. if (checked[0]) {
  1430. checked.focus();
  1431. return;
  1432. }
  1433. this.$('input[type=radio]').first().focus();
  1434. },
  1435. blur: function() {
  1436. if (!this.hasFocus) return;
  1437. this.$('input[type=radio]:focus').blur();
  1438. },
  1439. /**
  1440. * Create the radio list HTML
  1441. * @param {Array} Options as a simple array e.g. ['option1', 'option2']
  1442. * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
  1443. * @return {String} HTML
  1444. */
  1445. _arrayToHtml: function(array) {
  1446. var self = this;
  1447. var template = this.getTemplate(),
  1448. name = self.getName(),
  1449. id = self.id;
  1450. var items = _.map(array, function(option, index) {
  1451. var item = {
  1452. name: name,
  1453. id: id + '-' + index
  1454. }
  1455. if (_.isObject(option)) {
  1456. item.value = (option.val || option.val === 0) ? option.val : '';
  1457. item.label = option.label;
  1458. } else {
  1459. item.value = option;
  1460. item.label = option;
  1461. }
  1462. return item;
  1463. });
  1464. return template({
  1465. items: items
  1466. });
  1467. }
  1468. }, {
  1469. //STATICS
  1470. template: _.template('\
  1471. <ul>\
  1472. <% _.each(items, function(item) { %>\
  1473. <li>\
  1474. <input type="radio" name="<%= item.name %>" value="<%= item.value %>" id="<%= item.id %>" />\
  1475. <label for="<%= item.id %>"><%= item.label %></label>\
  1476. </li>\
  1477. <% }); %>\
  1478. </ul>\
  1479. ', null, Form.templateSettings)
  1480. });
  1481. /**
  1482. * Checkboxes editor
  1483. *
  1484. * Renders a <ul> with given options represented as <li> objects containing checkboxes
  1485. *
  1486. * Requires an 'options' value on the schema.
  1487. * Can be an array of options, a function that calls back with the array of options, a string of HTML
  1488. * or a Backbone collection. If a collection, the models must implement a toString() method
  1489. */
  1490. Form.editors.Checkboxes = Form.editors.Select.extend({
  1491. tagName: 'ul',
  1492. groupNumber: 0,
  1493. events: {
  1494. 'click input[type=checkbox]': function() {
  1495. this.trigger('change', this);
  1496. },
  1497. 'focus input[type=checkbox]': function() {
  1498. if (this.hasFocus) return;
  1499. this.trigger('focus', this);
  1500. },
  1501. 'blur input[type=checkbox]': function() {
  1502. if (!this.hasFocus) return;
  1503. var self = this;
  1504. setTimeout(function() {
  1505. if (self.$('input[type=checkbox]:focus')[0]) return;
  1506. self.trigger('blur', self);
  1507. }, 0);
  1508. }
  1509. },
  1510. getValue: function() {
  1511. var values = [];
  1512. this.$('input[type=checkbox]:checked').each(function() {
  1513. values.push($(this).val());
  1514. });
  1515. return values;
  1516. },
  1517. setValue: function(values) {
  1518. if (!_.isArray(values)) values = [values];
  1519. this.$('input[type=checkbox]').val(values);
  1520. },
  1521. focus: function() {
  1522. if (this.hasFocus) return;
  1523. this.$('input[type=checkbox]').first().focus();
  1524. },
  1525. blur: function() {
  1526. if (!this.hasFocus) return;
  1527. this.$('input[type=checkbox]:focus').blur();
  1528. },
  1529. /**
  1530. * Create the checkbox list HTML
  1531. * @param {Array} Options as a simple array e.g. ['option1', 'option2']
  1532. * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
  1533. * @return {String} HTML
  1534. */
  1535. _arrayToHtml: function(array) {
  1536. var html = [];
  1537. var self = this;
  1538. _.each(array, function(option, index) {
  1539. var itemHtml = '<li>';
  1540. var close = true;
  1541. if (_.isObject(option)) {
  1542. if (option.group) {
  1543. var originalId = self.id;
  1544. self.id += "-" + self.groupNumber++;
  1545. itemHtml = ('<fieldset class="group"> <legend>' + option.group + '</legend>');
  1546. itemHtml += (self._arrayToHtml(option.options));
  1547. itemHtml += ('</fieldset>');
  1548. self.id = originalId;
  1549. close = false;
  1550. } else {
  1551. var val = (option.val || option.val === 0) ? option.val : '';
  1552. itemHtml += ('<input type="checkbox" name="' + self.getName() + '" value="' + val + '" id="' + self.id + '-' + index + '" />');
  1553. itemHtml += ('<label for="' + self.id + '-' + index + '">' + option.label + '</label>');
  1554. }
  1555. } else {
  1556. itemHtml += ('<input type="checkbox" name="' + self.getName() + '" value="' + option + '" id="' + self.id + '-' + index + '" />');
  1557. itemHtml += ('<label for="' + self.id + '-' + index + '">' + option + '</label>');
  1558. }
  1559. if (close) {
  1560. itemHtml += '</li>';
  1561. }
  1562. html.push(itemHtml);
  1563. });
  1564. return html.join('');
  1565. }
  1566. });
  1567. /**
  1568. * Object editor
  1569. *
  1570. * Creates a child form. For editing Javascript objects
  1571. *
  1572. * @param {Object} options
  1573. * @param {Form} options.form The form this editor belongs to; used to determine the constructor for the nested form
  1574. * @param {Object} options.schema The schema for the object
  1575. * @param {Object} options.schema.subSchema The schema for the nested form
  1576. */
  1577. Form.editors.Object = Form.editors.Base.extend({
  1578. //Prevent error classes being set on the main control; they are internally on the individual fields
  1579. hasNestedForm: true,
  1580. initialize: function(options) {
  1581. //Set default value for the instance so it's not a shared object
  1582. this.value = {};
  1583. //Init
  1584. Form.editors.Base.prototype.initialize.call(this, options);
  1585. //Check required options
  1586. if (!this.form) throw new Error('Missing required option "form"');
  1587. if (!this.schema.subSchema) throw new Error("Missing required 'schema.subSchema' option for Object editor");
  1588. },
  1589. render: function() {
  1590. //Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
  1591. var NestedForm = this.form.constructor;
  1592. //Create the nested form
  1593. this.nestedForm = new NestedForm({
  1594. schema: this.schema.subSchema,
  1595. data: this.value,
  1596. idPrefix: this.id + '_',
  1597. Field: NestedForm.NestedField
  1598. });
  1599. this._observeFormEvents();
  1600. this.$el.html(this.nestedForm.render().el);
  1601. if (this.hasFocus) this.trigger('blur', this);
  1602. return this;
  1603. },
  1604. getValue: function() {
  1605. if (this.nestedForm) return this.nestedForm.getValue();
  1606. return this.value;
  1607. },
  1608. setValue: function(value) {
  1609. this.value = value;
  1610. this.render();
  1611. },
  1612. focus: function() {
  1613. if (this.hasFocus) return;
  1614. this.nestedForm.focus();
  1615. },
  1616. blur: function() {
  1617. if (!this.hasFocus) return;
  1618. this.nestedForm.blur();
  1619. },
  1620. remove: function() {
  1621. this.nestedForm.remove();
  1622. Backbone.View.prototype.remove.call(this);
  1623. },
  1624. validate: function() {
  1625. return this.nestedForm.validate();
  1626. },
  1627. _observeFormEvents: function() {
  1628. if (!this.nestedForm) return;
  1629. this.nestedForm.on('all', function() {
  1630. // args = ["key:change", form, fieldEditor]
  1631. var args = _.toArray(arguments);
  1632. args[1] = this;
  1633. // args = ["key:change", this=objectEditor, fieldEditor]
  1634. this.trigger.apply(this, args);
  1635. }, this);
  1636. }
  1637. });
  1638. /**
  1639. * NestedModel editor
  1640. *
  1641. * Creates a child form. For editing nested Backbone models
  1642. *
  1643. * Special options:
  1644. * schema.model: Embedded model constructor
  1645. */
  1646. Form.editors.NestedModel = Form.editors.Object.extend({
  1647. initialize: function(options) {
  1648. Form.editors.Base.prototype.initialize.call(this, options);
  1649. if (!this.form) throw new Error('Missing required option "form"');
  1650. if (!options.schema.model) throw new Error('Missing required "schema.model" option for NestedModel editor');
  1651. },
  1652. render: function() {
  1653. //Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
  1654. var NestedForm = this.form.constructor;
  1655. var data = this.value || {},
  1656. key = this.key,
  1657. nestedModel = this.schema.model;
  1658. //Wrap the data in a model if it isn't already a model instance
  1659. var modelInstance = (data.constructor === nestedModel) ? data : new nestedModel(data);
  1660. this.nestedForm = new NestedForm({
  1661. model: modelInstance,
  1662. idPrefix: this.id + '_',
  1663. fieldTemplate: 'nestedField'
  1664. });
  1665. this._observeFormEvents();
  1666. //Render form
  1667. this.$el.html(this.nestedForm.render().el);
  1668. if (this.hasFocus) this.trigger('blur', this);
  1669. return this;
  1670. },
  1671. /**
  1672. * Update the embedded model, checking for nested validation errors and pass them up
  1673. * Then update the main model if all OK
  1674. *
  1675. * @return {Error|null} Validation error or null
  1676. */
  1677. commit: function() {
  1678. var error = this.nestedForm.commit();
  1679. if (error) {
  1680. this.$el.addClass('error');
  1681. return error;
  1682. }
  1683. return Form.editors.Object.prototype.commit.call(this);
  1684. }
  1685. });
  1686. /**
  1687. * Date editor
  1688. *
  1689. * Schema options
  1690. * @param {Number|String} [options.schema.yearStart] First year in list. Default: 100 years ago
  1691. * @param {Number|String} [options.schema.yearEnd] Last year in list. Default: current year
  1692. *
  1693. * Config options (if not set, defaults to options stored on the main Date class)
  1694. * @param {Boolean} [options.showMonthNames] Use month names instead of numbers. Default: true
  1695. * @param {String[]} [options.monthNames] Month names. Default: Full English names
  1696. */
  1697. Form.editors.Date = Form.editors.Base.extend({
  1698. events: {
  1699. 'change select': function() {
  1700. this.updateHidden();
  1701. this.trigger('change', this);
  1702. },
  1703. 'focus select': function() {
  1704. if (this.hasFocus) return;
  1705. this.trigger('focus', this);
  1706. },
  1707. 'blur select': function() {
  1708. if (!this.hasFocus) return;
  1709. var self = this;
  1710. setTimeout(function() {
  1711. if (self.$('select:focus')[0]) return;
  1712. self.trigger('blur', self);
  1713. }, 0);
  1714. }
  1715. },
  1716. initialize: function(options) {
  1717. options = options || {};
  1718. Form.editors.Base.prototype.initialize.call(this, options);
  1719. var Self = Form.editors.Date,
  1720. today = new Date();
  1721. //Option defaults
  1722. this.options = _.extend({
  1723. monthNames: Self.monthNames,
  1724. showMonthNames: Self.showMonthNames
  1725. }, options);
  1726. //Schema defaults
  1727. this.schema = _.extend({
  1728. yearStart: today.getFullYear() - 100,
  1729. yearEnd: today.getFullYear()
  1730. }, options.schema || {});
  1731. //Cast to Date
  1732. if (this.value && !_.isDate(this.value)) {
  1733. this.value = new Date(this.value);
  1734. }
  1735. //Set default date
  1736. if (!this.value) {
  1737. var date = new Date();
  1738. date.setSeconds(0);
  1739. date.setMilliseconds(0);
  1740. this.value = date;
  1741. }
  1742. //Template
  1743. this.template = options.template || this.constructor.template;
  1744. },
  1745. render: function() {
  1746. var options = this.options,
  1747. schema = this.schema,
  1748. $ = Backbone.$;
  1749. var datesOptions = _.map(_.range(1, 32), function(date) {
  1750. return '<option value="' + date + '">' + date + '</option>';
  1751. });
  1752. var monthsOptions = _.map(_.range(0, 12), function(month) {
  1753. var value = (options.showMonthNames) ? options.monthNames[month] : (month + 1);
  1754. return '<option value="' + month + '">' + value + '</option>';
  1755. });
  1756. var yearRange = (schema.yearStart < schema.yearEnd) ? _.range(schema.yearStart, schema.yearEnd + 1) : _.range(schema.yearStart, schema.yearEnd - 1, -1);
  1757. var yearsOptions = _.map(yearRange, function(year) {
  1758. return '<option value="' + year + '">' + year + '</option>';
  1759. });
  1760. //Render the selects
  1761. var $el = $($.trim(this.template({
  1762. dates: datesOptions.join(''),
  1763. months: monthsOptions.join(''),
  1764. years: yearsOptions.join('')
  1765. })));
  1766. //Store references to selects
  1767. this.$date = $el.find('[data-type="date"]');
  1768. this.$month = $el.find('[data-type="month"]');
  1769. this.$year = $el.find('[data-type="year"]');
  1770. //Create the hidden field to store values in case POSTed to server
  1771. this.$hidden = $('<input type="hidden" name="' + this.key + '" />');
  1772. $el.append(this.$hidden);
  1773. //Set value on this and hidden field
  1774. this.setValue(this.value);
  1775. //Remove the wrapper tag
  1776. this.setElement($el);
  1777. this.$el.attr('id', this.id);
  1778. this.$el.attr('name', this.getName());
  1779. if (this.hasFocus) this.trigger('blur', this);
  1780. return this;
  1781. },
  1782. /**
  1783. * @return {Date} Selected date
  1784. */
  1785. getValue: function() {
  1786. var year = this.$year.val(),
  1787. month = this.$month.val(),
  1788. date = this.$date.val();
  1789. if (!year || !month || !date) return null;
  1790. return new Date(year, month, date);
  1791. },
  1792. /**
  1793. * @param {Date} date
  1794. */
  1795. setValue: function(date) {
  1796. this.$date.val(date.getDate());
  1797. this.$month.val(date.getMonth());
  1798. this.$year.val(date.getFullYear());
  1799. this.updateHidden();
  1800. },
  1801. focus: function() {
  1802. if (this.hasFocus) return;
  1803. this.$('select').first().focus();
  1804. },
  1805. blur: function() {
  1806. if (!this.hasFocus) return;
  1807. this.$('select:focus').blur();
  1808. },
  1809. /**
  1810. * Update the hidden input which is maintained for when submitting a form
  1811. * via a normal browser POST
  1812. */
  1813. updateHidden: function() {
  1814. var val = this.getValue();
  1815. if (_.isDate(val)) val = val.toISOString();
  1816. this.$hidden.val(val);
  1817. }
  1818. }, {
  1819. //STATICS
  1820. template: _.template('\
  1821. <div>\
  1822. <select data-type="date"><%= dates %></select>\
  1823. <select data-type="month"><%= months %></select>\
  1824. <select data-type="year"><%= years %></select>\
  1825. </div>\
  1826. ', null, Form.templateSettings),
  1827. //Whether to show month names instead of numbers
  1828. showMonthNames: true,
  1829. //Month names to use if showMonthNames is true
  1830. //Replace for localisation, e.g. Form.editors.Date.monthNames = ['Janvier', 'Fevrier'...]
  1831. monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
  1832. });
  1833. /**
  1834. * DateTime editor
  1835. *
  1836. * @param {Editor} [options.DateEditor] Date editor view to use (not definition)
  1837. * @param {Number} [options.schema.minsInterval] Interval between minutes. Default: 15
  1838. */
  1839. Form.editors.DateTime = Form.editors.Base.extend({
  1840. events: {
  1841. 'change select': function() {
  1842. this.updateHidden();
  1843. this.trigger('change', this);
  1844. },
  1845. 'focus select': function() {
  1846. if (this.hasFocus) return;
  1847. this.trigger('focus', this);
  1848. },
  1849. 'blur select': function() {
  1850. if (!this.hasFocus) return;
  1851. var self = this;
  1852. setTimeout(function() {
  1853. if (self.$('select:focus')[0]) return;
  1854. self.trigger('blur', self);
  1855. }, 0);
  1856. }
  1857. },
  1858. initialize: function(options) {
  1859. options = options || {};
  1860. Form.editors.Base.prototype.initialize.call(this, options);
  1861. //Option defaults
  1862. this.options = _.extend({
  1863. DateEditor: Form.editors.DateTime.DateEditor
  1864. }, options);
  1865. //Schema defaults
  1866. this.schema = _.extend({
  1867. minsInterval: 15
  1868. }, options.schema || {});
  1869. //Create embedded date editor
  1870. this.dateEditor = new this.options.DateEditor(options);
  1871. this.value = this.dateEditor.value;
  1872. //Template
  1873. this.template = options.template || this.constructor.template;
  1874. },
  1875. render: function() {
  1876. function pad(n) {
  1877. return n < 10 ? '0' + n : n;
  1878. }
  1879. var schema = this.schema,
  1880. $ = Backbone.$;
  1881. //Create options
  1882. var hoursOptions = _.map(_.range(0, 24), function(hour) {
  1883. return '<option value="' + hour + '">' + pad(hour) + '</option>';
  1884. });
  1885. var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) {
  1886. return '<option value="' + min + '">' + pad(min) + '</option>';
  1887. });
  1888. //Render time selects
  1889. var $el = $($.trim(this.template({
  1890. hours: hoursOptions.join(),
  1891. mins: minsOptions.join()
  1892. })));
  1893. //Include the date editor
  1894. $el.find('[data-date]').append(this.dateEditor.render().el);
  1895. //Store references to selects
  1896. this.$hour = $el.find('select[data-type="hour"]');
  1897. this.$min = $el.find('select[data-type="min"]');
  1898. //Get the hidden date field to store values in case POSTed to server
  1899. this.$hidden = $el.find('input[type="hidden"]');
  1900. //Set time
  1901. this.setValue(this.value);
  1902. this.setElement($el);
  1903. this.$el.attr('id', this.id);
  1904. this.$el.attr('name', this.getName());
  1905. if (this.hasFocus) this.trigger('blur', this);
  1906. return this;
  1907. },
  1908. /**
  1909. * @return {Date} Selected datetime
  1910. */
  1911. getValue: function() {
  1912. var date = this.dateEditor.getValue();
  1913. var hour = this.$hour.val(),
  1914. min = this.$min.val();
  1915. if (!date || !hour || !min) return null;
  1916. date.setHours(hour);
  1917. date.setMinutes(min);
  1918. return date;
  1919. },
  1920. /**
  1921. * @param {Date}
  1922. */
  1923. setValue: function(date) {
  1924. if (!_.isDate(date)) date = new Date(date);
  1925. this.dateEditor.setValue(date);
  1926. this.$hour.val(date.getHours());
  1927. this.$min.val(date.getMinutes());
  1928. this.updateHidden();
  1929. },
  1930. focus: function() {
  1931. if (this.hasFocus) return;
  1932. this.$('select').first().focus();
  1933. },
  1934. blur: function() {
  1935. if (!this.hasFocus) return;
  1936. this.$('select:focus').blur();
  1937. },
  1938. /**
  1939. * Update the hidden input which is maintained for when submitting a form
  1940. * via a normal browser POST
  1941. */
  1942. updateHidden: function() {
  1943. var val = this.getValue();
  1944. if (_.isDate(val)) val = val.toISOString();
  1945. this.$hidden.val(val);
  1946. },
  1947. /**
  1948. * Remove the Date editor before removing self
  1949. */
  1950. remove: function() {
  1951. this.dateEditor.remove();
  1952. Form.editors.Base.prototype.remove.call(this);
  1953. }
  1954. }, {
  1955. //STATICS
  1956. template: _.template('\
  1957. <div class="bbf-datetime">\
  1958. <div class="bbf-date-container" data-date></div>\
  1959. <select data-type="hour"><%= hours %></select>\
  1960. :\
  1961. <select data-type="min"><%= mins %></select>\
  1962. </div>\
  1963. ', null, Form.templateSettings),
  1964. //The date editor to use (constructor function, not instance)
  1965. DateEditor: Form.editors.Date
  1966. });
  1967. //Metadata
  1968. Form.VERSION = '0.14.0';
  1969. //Exports
  1970. Backbone.Form = Form;
  1971. return Form;
  1972. });