PageRenderTime 64ms CodeModel.GetById 24ms 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

Large files files are truncated, but you can click here to view the full 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'}]

Large files files are truncated, but you can click here to view the full file