+
+ {% include form-handler.html %}
+ {% include site-footer.html %}
+
diff --git a/_pages/claims/new.html b/_pages/claims/new.html
index ba9cf5b..500a9b5 100644
--- a/_pages/claims/new.html
+++ b/_pages/claims/new.html
@@ -3,478 +3,66 @@
---
{% include head.html %}
+
Skip to main content
{% include site-header.html %}
-
+
-
-
+
File a claim
+
+
{% include form-handler.html %}
{% include site-footer.html %}
+
+
+
+
diff --git a/_pages/claims/new/step-2.html b/_pages/claims/new/step-2.html
deleted file mode 100644
index 3e0577a..0000000
--- a/_pages/claims/new/step-2.html
+++ /dev/null
@@ -1,726 +0,0 @@
----
-excerpt: none
----
-
-{% include head.html %}
-
- Skip to main content
- {% include site-header.html %}
-
-
-
-
-
- {% include form-handler.html %}
- {% include site-footer.html %}
-
diff --git a/javascripts/jsonform-defaults.js b/javascripts/jsonform-defaults.js
new file mode 100755
index 0000000..fad62c1
--- /dev/null
+++ b/javascripts/jsonform-defaults.js
@@ -0,0 +1,331 @@
+/**
+ * @fileoverview The JSON Form "defaults" library exposes a setDefaultValues
+ * method that extends the object passed as argument so that it includes
+ * values for all required fields of the JSON schema it is to follow that
+ * define a default value.
+ *
+ * The library is called to complete the configuration settings of a template in
+ * the Factory and to complete datasource settings.
+ *
+ * The library is useless if the settings have already been validated against the
+ * schema using the JSON schema validator (typically, provided the validator is
+ * loaded, submitting the form created from the schema will raise an error when
+ * required properties are missing).
+ *
+ * Note the library does not validate the created object, it merely sets missing
+ * values to the default values specified in the schema. All other values may
+ * be invalid.
+ *
+ * Nota Bene:
+ * - in data-joshfire, the runtime/nodejs/lib/jsonform-defaults.js file is a
+ * symbolic link to the jsonform submodule in deps/jsonform
+ * - in platform-joshfire, the server/public/js/libs/jsonform-defaults.js file
+ * is a symbolic link to the jsonform submodule in deps/jsonform
+ */
+
+(function () {
+ // Establish the root object:
+ // that's "window" in the browser, "global" in node.js
+ var root = this;
+
+ /**
+ * Sets default values, ensuring that fields defined as "required" in the
+ * schema appear in the object. If missing, the hierarchy that leads to
+ * a required key is automatically created.
+ *
+ * @function
+ * @param {Object} obj The object to complete with default values according
+ * to the schema
+ * @param {Object} schema The JSON schema that the object follows
+ * @param {boolean} includeOptionalValues Include default values for fields
+ * that are not "required"
+ * @param {boolean} skipFieldsWithoutDefaultValue Set flag not to include a
+ * generated empty default value for fields marked as "required" in the
+ * schema but that do not define a default value.
+ * @return {Object} The completed object (same instance as obj)
+ */
+ var setDefaultValues = function (obj, schema, includeOptionalValues, skipFieldsWithoutDefaultValue) {
+ if (!obj || !schema) return obj;
+ if (!schema.properties) {
+ schema = { properties: schema };
+ }
+
+ // Inner function that parses the schema recursively to build a flat
+ // list of defaults
+ var defaults = {};
+ var extractDefaultValues = function (schemaItem, path) {
+ var properties = null;
+ var child = null;
+
+ if (!schemaItem || (schemaItem !== Object(schemaItem))) return null;
+
+ if (schemaItem.required) {
+ // Item is required
+ if (schemaItem['default']) {
+ // Item defines a default value, let's use it,
+ // no need to continue in that case, we have the default value
+ // for the whole subtree starting at schemaItem.
+ defaults[path] = schemaItem['default'];
+ return;
+ }
+ else if (skipFieldsWithoutDefaultValue) {
+ // Required but no default value and caller explicitly asked not
+ // include such fields in the returned object.
+ }
+ else if ((schemaItem.type === 'object') || schemaItem.properties) {
+ // Item is a required object
+ defaults[path] = {};
+ }
+ else if ((schemaItem.type === 'array') || schemaItem.items) {
+ // Item is a required array
+ defaults[path] = [];
+ }
+ else if (schemaItem.type === 'string') {
+ defaults[path] = '';
+ }
+ else if ((schemaItem.type === 'number') || (schemaItem.type === 'integer')) {
+ defaults[path] = 0;
+ }
+ else if (schemaItem.type === 'boolean') {
+ defaults[path] = false;
+ }
+ else {
+ // Unknown type, use an empty object by default
+ defaults[path] = {};
+ }
+ }
+ else if (schemaItem['default'] && includeOptionalValues) {
+ // Item is not required but defines a default value and the
+ // include optional values flag is set, so let's use it.
+ // No need to continue in that case, we have the default value
+ // for the whole subtree starting at schemaItem.
+ defaults[path] = schemaItem['default'];
+ return;
+ }
+
+ // Parse schema item's properties recursively
+ properties = schemaItem.properties;
+ if (properties) {
+ for (var key in properties) {
+ if (properties.hasOwnProperty(key)) {
+ extractDefaultValues(properties[key], path + '.' + key);
+ }
+ }
+ }
+
+ // Parse schema item's children recursively
+ if (schemaItem.items) {
+ // Items may be a single item or an array composed of only one item
+ child = schemaItem.items;
+ if (_isArray(child)) {
+ child = child[0];
+ }
+
+ extractDefaultValues(child, path + '[]');
+ }
+ };
+
+ // Build a flat list of default values
+ extractDefaultValues(schema, '');
+
+ // Ensure the object's default values are correctly set
+ for (var key in defaults) {
+ if (defaults.hasOwnProperty(key)) {
+ setObjKey(obj, key, defaults[key]);
+ }
+ }
+ };
+
+
+ /**
+ * Retrieves the default value for the given key in the schema
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with []. For instance:
+ * foo.bar[].baz
+ *
+ * @function
+ * @param {Object} schema The schema to parse
+ * @param {String} key The path to the key whose default value we're
+ * looking for. each level is separated by a dot, and array items are
+ * flagged with [x].
+ * @return {Object} The default value, null if not found.
+ */
+ var getSchemaKeyDefaultValue = function(schema,key) {
+ var schemaKey = key
+ .replace(/\./g, '.properties.')
+ .replace(/\[.*\](\.|$)/g, '.items$1');
+ var schemaDef = getObjKey(schema, schemaKey);
+ if (schemaDef) return schemaDef['default'];
+ return null;
+ };
+
+ /**
+ * Retrieves the key identified by a path selector in the structured object.
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with []. For instance:
+ * foo.bar[].baz
+ *
+ * @function
+ * @param {Object} obj The object to parse
+ * @param {String} key The path to the key whose default value we're
+ * looking for. each level is separated by a dot, and array items are
+ * flagged with [x].
+ * @return {Object} The key definition, null if not found.
+ */
+ var getObjKey = function (obj, key) {
+ var innerobj = obj;
+ var keyparts = key.split('.');
+ var subkey = null;
+ var arrayMatch = null;
+ var reArraySingle = /\[([0-9]*)\](?:\.|$)/;
+
+ for (var i = 0; i < keyparts.length; i++) {
+ if (typeof innerobj !== 'object') return null;
+ subkey = keyparts[i];
+ arrayMatch = subkey.match(reArraySingle);
+ if (arrayMatch) {
+ // Subkey is part of an array
+ subkey = subkey.replace(reArraySingle, '');
+ if (!_isArray(innerobj[subkey])) {
+ return null;
+ }
+ innerobj = innerobj[subkey][parseInt(arrayMatch[1], 10)];
+ }
+ else {
+ innerobj = innerobj[subkey];
+ }
+ }
+
+ return innerobj;
+ };
+
+
+ /**
+ * Sets the key identified by a path selector to the given value.
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with []. For instance:
+ * foo.bar[].baz
+ *
+ * The hierarchy is automatically created if it does not exist yet.
+ *
+ * Default values are added to all array items. Array items are not
+ * automatically created if they do not exist (in particular, the
+ * minItems constraint is not enforced)
+ *
+ * @function
+ * @param {Object} obj The object to build
+ * @param {String} key The path to the key to set where each level
+ * is separated by a dot, and array items are flagged with [x].
+ * @param {Object} value The value to set, may be of any type.
+ */
+ var setObjKey = function (obj, key, value) {
+ var keyparts = key.split('.');
+
+ // Recursive version of setObjKey
+ var recSetObjKey = function (obj, keyparts, value) {
+ var arrayMatch = null;
+ var reArray = /\[([0-9]*)\]$/;
+ var subkey = keyparts.shift();
+ var idx = 0;
+
+ if (keyparts.length > 0) {
+ // Not the end yet, build the hierarchy
+ arrayMatch = subkey.match(reArray);
+ if (arrayMatch) {
+ // Subkey is part of an array, check all existing array items
+ // TODO: review that! Only create the right item!!!
+ subkey = subkey.replace(reArray, '');
+ if (!_isArray(obj[subkey])) {
+ obj[subkey] = [];
+ }
+ obj = obj[subkey];
+ if (arrayMatch[1] !== '') {
+ idx = parseInt(arrayMatch[1], 10);
+ if (!obj[idx]) {
+ obj[idx] = {};
+ }
+ recSetObjKey(obj[idx], keyparts, value);
+ }
+ else {
+ for (var k = 0; k < obj.length; k++) {
+ recSetObjKey(obj[k], keyparts, value);
+ }
+ }
+ return;
+ }
+ else {
+ // "Normal" subkey
+ if (typeof obj[subkey] !== 'object') {
+ obj[subkey] = {};
+ }
+ obj = obj[subkey];
+ recSetObjKey(obj, keyparts, value);
+ }
+ }
+ else {
+ // Last key, time to set the value, unless already defined
+ arrayMatch = subkey.match(reArray);
+ if (arrayMatch) {
+ subkey = subkey.replace(reArray, '');
+ if (!_isArray(obj[subkey])) {
+ obj[subkey] = [];
+ }
+ idx = parseInt(arrayMatch[1], 10);
+ if (!obj[subkey][idx]) {
+ obj[subkey][idx] = value;
+ }
+ }
+ else if (!obj[subkey]) {
+ obj[subkey] = value;
+ }
+ }
+ };
+
+ // Skip first item if empty (key starts with a '.')
+ if (!keyparts[0]) {
+ keyparts.shift();
+ }
+ recSetObjKey(obj, keyparts, value);
+ };
+
+ // Taken from Underscore.js (not included to save bytes)
+ var _isArray = Array.isArray || function (obj) {
+ return Object.prototype.toString.call(obj) == '[object Array]';
+ };
+
+
+ // Export the code as:
+ // 1. an AMD module (the "define" method exists in that case), or
+ // 2. a node.js module ("module.exports" is defined in that case), or
+ // 3. a global JSONForm object (using "root")
+ if (typeof define !== 'undefined') {
+ // AMD module
+ define([], function () {
+ return {
+ setDefaultValues: setDefaultValues,
+ setObjKey: setObjKey,
+ getSchemaKeyDefaultValue: getSchemaKeyDefaultValue
+ };
+ });
+ }
+ else if ((typeof module !== 'undefined') && module.exports) {
+ // Node.js module
+ module.exports = {
+ setDefaultValues: setDefaultValues,
+ setObjKey: setObjKey,
+ getSchemaKeyDefaultValue: getSchemaKeyDefaultValue
+ };
+ }
+ else {
+ // Export the function to the global context, using a "string" for
+ // Google Closure Compiler "advanced" mode
+ // (not sure why it's needed, done by Underscore)
+ root['JSONForm'] = root['JSONForm'] || {};
+ root['JSONForm'].setDefaultValues = setDefaultValues;
+ root['JSONForm'].setObjKey = setObjKey;
+ root['JSONForm'].getSchemaKeyDefaultValue = getSchemaKeyDefaultValue;
+ }
+})();
\ No newline at end of file
diff --git a/javascripts/jsonform-split.js b/javascripts/jsonform-split.js
new file mode 100755
index 0000000..acc657a
--- /dev/null
+++ b/javascripts/jsonform-split.js
@@ -0,0 +1,320 @@
+/**
+ * @fileoverview The JSON Form "split" library exposes a "split" method
+ * that can be used to divide a JSON Form object into two disjoint
+ * JSON Form objects:
+ * - the first one includes the schema and layout of the form that
+ * contains the list of keys given as parameters as well as keys that
+ * cannot be separated from them (typically because they belong to the
+ * same array in the layout)
+ * - the second one includes the schema and layout of a form that does not
+ * contain the list of keys given as parameters.
+ *
+ * The options parameter lets one be explicit about whether it wants to include
+ * fields that are tightly coupled with the provided list of keys or not.
+ */
+/*global exports, _*/
+
+(function (serverside, global, _) {
+ if (serverside && !_) {
+ _ = require('underscore');
+ }
+
+ /**
+ * Splits a JSON Form object into two autonomous JSON Form objects,
+ * one that includes the provided list of schema keys as well as keys
+ * that are tightly coupled to these keys, and the other that does not
+ * include these keys.
+ *
+ * The function operates on the "schema", "form", and "value" properties
+ * of the initial JSON Form object. It copies over the other properties
+ * to the resulting JSON Form objects.
+ *
+ * Note that the split function does not support "*" form definitions. The
+ * "form" property must be set in the provided in the provided JSON Form
+ * object.
+ *
+ * @function
+ * @param {Object} jsonform JSON Form object with a "schema" and "form"
+ * @param {Array(String)} keys Schema keys used to split the form. Each
+ * key must reference a schema key at the first level in the schema
+ * (in other words, the keys cannot include "." or "[]")
+ * @param {Object} options Split options. Set the "excludeCoupledKeys" flag
+ * not to include keys that are tightly coupled with the ones provided in
+ * the included part of the JSON Form object.
+ * @return {Object} An object with an "included" property whose value is
+ * the JSON Form object that includes the keys and an "excluded" property
+ * whose value is the JSON Form object that does not contain any of the
+ * keys. These objects may be empty.
+ */
+ var split = function (jsonform, keys, options) {
+ options = options || {};
+ keys = keys || [];
+ if (!jsonform || !jsonform.form) {
+ return {
+ included: {},
+ excluded: {}
+ };
+ }
+
+ if (_.isString(keys)) {
+ keys = [keys];
+ }
+
+ // Prepare the object that will be returned
+ var result = {
+ included: {
+ schema: {
+ properties: {}
+ },
+ form: []
+ },
+ excluded: {
+ schema: {
+ properties: {}
+ },
+ form: []
+ }
+ };
+
+ // Copy over properties such as "value" or "tpldata" that do not need
+ // to be split (note both forms will reference the same structures)
+ _.each(jsonform, function (value, key) {
+ if ((key !== 'schema') && (key !== 'form')) {
+ result.included[key] = value;
+ result.excluded[key] = value;
+ }
+ });
+
+
+ /**
+ * Helper function that parses the given field and returns true if
+ * it references one of the keys to include directly. Note the function
+ * does not parse the potential children of the field and will thus
+ * return false even if the field actually references a key to include
+ * indirectly.
+ *
+ * @function
+ * @param {Object} formField The form field to parse
+ * @return {boolean} true when the field references one of the keys to
+ * include, false when not
+ */
+ var formFieldReferencesKey = function (formField) {
+ var referencedKey = _.isString(formField) ?
+ formField :
+ formField.key;
+ if (!referencedKey) {
+ return false;
+ }
+ return _.include(keys, referencedKey) ||
+ !!_.find(keys, function (key) {
+ return (referencedKey.indexOf(key + '.') === 0) ||
+ (referencedKey.indexOf(key + '[]') === 0);
+ });
+ };
+
+
+ /**
+ * Helper function that parses the given field and returns true if
+ * it references a key that is not in the list of keys to include.
+ * Note the function does not parse the potential children of the field
+ * and will thus return false even if the field actually references a key
+ * to include indirectly.
+ *
+ * @function
+ * @param {Object} formField The form field to parse
+ * @return {boolean} true when the field references one of the keys to
+ * include, false when not
+ */
+ var formFieldReferencesOtherKey = function (formField) {
+ var referencedKey = _.isString(formField) ?
+ formField :
+ formField.key;
+ if (!referencedKey) {
+ return false;
+ }
+ return !_.include(keys, referencedKey) &&
+ !_.find(keys, function (key) {
+ return (referencedKey.indexOf(key + '.') === 0) ||
+ (referencedKey.indexOf(key + '[]') === 0);
+ });
+ };
+
+
+ /**
+ * Helper function that parses the given field and returns true if
+ * it references one of the keys to include somehow (either directly
+ * or through one of its descendants).
+ *
+ * @function
+ * @param {Object} formField The form field to parse
+ * @return {boolean} true when the field references one of the keys to
+ * include, false when not
+ */
+ var includeFormField = function (formField) {
+ return formFieldReferencesKey(formField) ||
+ formField.items && !!_.some(formField.items, function (item) {
+ return includeFormField(item);
+ });
+ };
+
+
+ /**
+ * Helper function that parses the given field and returns true if
+ * it references a key that is not one of the keys to include somehow
+ * (either directly or through one of its descendants).
+ *
+ * @function
+ * @param {Object} formField The form field to parse
+ * @return {boolean} true when the field references one of the keys to
+ * include, false when not
+ */
+ var excludeFormField = function (formField) {
+ return formFieldReferencesOtherKey(formField) ||
+ formField.items && !!_.some(formField.items, function (item) {
+ return excludeFormField(item);
+ });
+ };
+
+
+ /**
+ * Converts the provided form field for inclusion in the included/excluded
+ * portion of the result. The function returns null if the field should not
+ * appear in the relevant part.
+ *
+ * Note the function is recursive.
+ *
+ * @function
+ * @param {Object} formField The form field to convert
+ * @param {string} splitPart The targeted result part, one of "included",
+ * "excluded", or "all". The "all" string is used in recursions to force
+ * the inclusion of the field even if it does not reference one of the
+ * provided keys.
+ * @param {Object} parentField Pointer to the form field parent. This
+ * parameter is used in recursions to preserve direct children of a
+ * "selectfieldset".
+ * @return {Object} The converted field.
+ */
+ var convertFormField = function (formField, splitPart, parentField) {
+ var convertedField = null;
+
+ var keepField = formField.root ||
+ (splitPart === 'all') ||
+ (parentField && parentField.key &&
+ (parentField.type === 'selectfieldset')) ||
+ (formField.type && formField.type === 'help');
+ if (!keepField) {
+ keepField = (splitPart === 'included') && includeFormField(formField);
+ }
+ if (!keepField) {
+ keepField = (splitPart === 'excluded') && excludeFormField(formField);
+ if (keepField && !options.excludeCoupledKeys) {
+ keepField = !includeFormField(formField);
+ }
+ }
+ if (!keepField) {
+ return null;
+ }
+
+ var childPart = splitPart;
+ if ((childPart === 'included') &&
+ !options.excludeCoupledKeys &&
+ !formField.root) {
+ childPart = 'all';
+ }
+
+ // Make a shallow copy of the field since we will preserve all of its
+ // properties (save perhaps "items")
+ convertedField = _.clone(formField);
+
+ // Recurse through the descendants of the field
+ if (convertedField.items) {
+ convertedField.items = _.map(convertedField.items, function (field) {
+ return convertFormField(field, childPart, convertedField);
+ });
+ convertedField.items = _.compact(convertedField.items);
+ }
+ return convertedField;
+ };
+
+
+ /**
+ * Helper function that checks the given schema key definition
+ * and returns true when the definition is referenced in the given
+ * form field definition
+ *
+ * @function
+ * @param {Object} formField The form field to check
+ * @param {string} schemaKey The key to search in the form field
+ * @return {boolean} true if the form field references the key somehow,
+ * false otherwise.
+ */
+ var includeSchemaKey = function (formField, schemaKey) {
+ if (!formField) return false;
+ if (!schemaKey) return false;
+
+ if (_.isString(formField)) {
+ // Direct reference to a key in the schema
+ return (formField === schemaKey) ||
+ (formField.indexOf(schemaKey + '.') === 0) ||
+ (formField.indexOf(schemaKey + '[]') === 0);
+ }
+
+ if (formField.key) {
+ if ((formField.key === schemaKey) ||
+ (formField.key.indexOf(schemaKey + '.') === 0) ||
+ (formField.key.indexOf(schemaKey + '[]') === 0)
+ ) {
+ return true;
+ }
+ }
+
+ return !!_.some(formField.items, function (item) {
+ return includeSchemaKey(item, schemaKey);
+ });
+ };
+
+
+ // Prepare the included/excluded forms
+ var converted = null;
+ converted = convertFormField({
+ items: jsonform.form,
+ root: true
+ }, 'included');
+ if (converted) {
+ result.included.form = converted.items;
+ }
+ converted = convertFormField({
+ items: jsonform.form,
+ root: true
+ }, 'excluded');
+ if (converted) {
+ result.excluded.form = converted.items;
+ }
+
+ // Split the schema into two schemas.
+ // (note that the "excluded" JSON Form object may contain keys that
+ // are never referenced in the initial JSON Form layout. That's normal)
+ var schemaProperties = jsonform.schema;
+ if (schemaProperties.properties) {
+ schemaProperties = schemaProperties.properties;
+ }
+ _.each(schemaProperties, function (schemaDefinition, schemaKey) {
+ if (_.some(result.included.form, function (formField) {
+ return includeSchemaKey(formField, schemaKey);
+ })) {
+ result.included.schema.properties[schemaKey] = schemaDefinition;
+ }
+ else {
+ result.excluded.schema.properties[schemaKey] = schemaDefinition;
+ }
+ });
+
+ return result;
+ };
+
+ global.JSONForm = global.JSONForm || {};
+ global.JSONForm.split = split;
+
+})((typeof exports !== 'undefined'),
+ ((typeof exports !== 'undefined') ? exports : window),
+ ((typeof _ !== 'undefined') ? _ : null));
diff --git a/javascripts/jsonform.js b/javascripts/jsonform.js
new file mode 100755
index 0000000..e15e2b1
--- /dev/null
+++ b/javascripts/jsonform.js
@@ -0,0 +1,3591 @@
+/* Copyright (c) 2012 Joshfire - MIT license */
+/**
+ * @fileoverview Core of the JSON Form client-side library.
+ *
+ * Generates an HTML form from a structured data model and a layout description.
+ *
+ * The library may also validate inputs entered by the user against the data model
+ * upon form submission and create the structured data object initialized with the
+ * values that were submitted.
+ *
+ * The library depends on:
+ * - jQuery
+ * - the underscore library
+ * - a JSON parser/serializer. Nothing to worry about in modern browsers.
+ * - the JSONFormValidation library (in jsv.js) for validation purpose
+ *
+ * See documentation at:
+ * http://developer.joshfire.com/doc/dev/ref/jsonform
+ *
+ * The library creates and maintains an internal data tree along with the DOM.
+ * That structure is necessary to handle arrays (and nested arrays!) that are
+ * dynamic by essence.
+ */
+
+ /*global window*/
+
+(function(serverside, global, $, _, JSON) {
+ if (serverside) {
+ _ = require('underscore');
+ }
+
+ /**
+ * Regular expressions used to extract array indexes in input field names
+ */
+ var reArray = /\[([0-9]*)\](?=\[|\.|$)/g;
+
+ /**
+ * Template settings for form views
+ */
+ var fieldTemplateSettings = {
+ evaluate : /<%([\s\S]+?)%>/g,
+ interpolate : /<%=([\s\S]+?)%>/g
+ };
+
+ /**
+ * Template settings for value replacement
+ */
+ var valueTemplateSettings = {
+ evaluate : /\{\[([\s\S]+?)\]\}/g,
+ interpolate : /\{\{([\s\S]+?)\}\}/g
+ };
+
+ /**
+ * Returns true if given value is neither "undefined" nor null
+ */
+ var isSet = function (value) {
+ return !(_.isUndefined(value) || _.isNull(value));
+ };
+
+ /**
+ * The jsonform object whose methods will be exposed to the window object
+ */
+ var jsonform = {util:{}};
+
+
+ // From backbonejs
+ var escapeHTML = function (string) {
+ if (!isSet(string)) {
+ return '';
+ }
+ string = '' + string;
+ if (!string) {
+ return '';
+ }
+ return string
+ .replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ .replace(/\//g, '/');
+ };
+
+/**
+ * Escapes selector name for use with jQuery
+ *
+ * All meta-characters listed in jQuery doc are escaped:
+ * http://api.jquery.com/category/selectors/
+ *
+ * @function
+ * @param {String} selector The jQuery selector to escape
+ * @return {String} The escaped selector.
+ */
+var escapeSelector = function (selector) {
+ return selector.replace(/([ \!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '\\$1');
+};
+
+
+/**
+ * Initializes tabular sections in forms. Such sections are generated by the
+ * 'selectfieldset' type of elements in JSON Form.
+ *
+ * Input fields that are not visible are automatically disabled
+ * not to appear in the submitted form. That's on purpose, as tabs
+ * are meant to convey an alternative (and not a sequence of steps).
+ *
+ * The tabs menu is not rendered as tabs but rather as a select field because
+ * it's easier to grasp that it's an alternative.
+ *
+ * Code based on bootstrap-tabs.js, updated to:
+ * - react to option selection instead of tab click
+ * - disable input fields in non visible tabs
+ * - disable the possibility to have dropdown menus (no meaning here)
+ * - act as a regular function instead of as a jQuery plug-in.
+ *
+ * @function
+ * @param {Object} tabs jQuery object that contains the tabular sections
+ * to initialize. The object may reference more than one element.
+ */
+var initializeTabs = function (tabs) {
+ var activate = function (element, container) {
+ container
+ .find('> .active')
+ .removeClass('active');
+ element.addClass('active');
+ };
+
+ var enableFields = function ($target, targetIndex) {
+ // Enable all fields in the targeted tab
+ $target.find('input, textarea, select').removeAttr('disabled');
+
+ // Disable all fields in other tabs
+ $target.parent()
+ .children(':not([data-idx=' + targetIndex + '])')
+ .find('input, textarea, select')
+ .attr('disabled', 'disabled');
+ };
+
+ var optionSelected = function (e) {
+ var $option = $("option:selected", $(this)),
+ $select = $(this),
+ // do not use .attr() as it sometimes unexplicably fails
+ targetIdx = $option.get(0).getAttribute('data-idx') || $option.attr('value'),
+ $target;
+
+ e.preventDefault();
+ if ($option.hasClass('active')) {
+ return;
+ }
+
+ $target = $(this).parents('.tabbable').eq(0).find('.tab-content [data-idx=' + targetIdx + ']');
+
+ activate($option, $select);
+ activate($target, $target.parent());
+ enableFields($target, targetIdx);
+ };
+
+ var tabClicked = function (e) {
+ var $a = $('a', $(this));
+ var $content = $(this).parents('.tabbable').first()
+ .find('.tab-content').first();
+ var targetIdx = $(this).index();
+ var $target = $content.find('[data-idx=' + targetIdx + ']');
+
+ e.preventDefault();
+ activate($(this), $(this).parent());
+ activate($target, $target.parent());
+ if ($(this).parent().hasClass('jsonform-alternative')) {
+ enableFields($target, targetIdx);
+ }
+ };
+
+ tabs.each(function () {
+ $(this).delegate('select.nav', 'change', optionSelected);
+ $(this).find('select.nav').each(function () {
+ $(this).val($(this).find('.active').attr('value'));
+ // do not use .attr() as it sometimes unexplicably fails
+ var targetIdx = $(this).find('option:selected').get(0).getAttribute('data-idx') ||
+ $(this).find('option:selected').attr('value');
+ var $target = $(this).parents('.tabbable').eq(0).find('.tab-content [data-idx=' + targetIdx + ']');
+ enableFields($target, targetIdx);
+ });
+
+ $(this).delegate('ul.nav li', 'click', tabClicked);
+ $(this).find('ul.nav li.active').click();
+ });
+};
+
+
+// Twitter bootstrap-friendly HTML boilerplate for standard inputs
+jsonform.fieldTemplate = function(inner) {
+ return '
',
+ 'fieldtemplate': true,
+ 'inputfield': true,
+ 'onInsert': function (evt, node) {
+ var setup = function () {
+ var formElement = node.formElement || {};
+ var ace = window.ace;
+ var editor = ace.edit($(node.el).find('#' + escapeSelector(node.id) + '__ace').get(0));
+ var idSelector = '#' + escapeSelector(node.id) + '__hidden';
+ // Force editor to use "\n" for new lines, not to bump into ACE "\r" conversion issue
+ // (ACE is ok with "\r" on pasting but fails to return "\r" when value is extracted)
+ editor.getSession().setNewLineMode('unix');
+ editor.renderer.setShowPrintMargin(false);
+ editor.setTheme("ace/theme/"+(formElement.aceTheme||"twilight"));
+
+ if (formElement.aceMode) {
+ editor.getSession().setMode("ace/mode/"+formElement.aceMode);
+ }
+ editor.getSession().setTabSize(2);
+
+ // Set the contents of the initial manifest file
+ editor.getSession().setValue(node.value||"");
+
+ //TODO this is clearly sub-optimal
+ // 'Lazily' bind to the onchange 'ace' event to give
+ // priority to user edits
+ var lazyChanged = _.debounce(function () {
+ $(node.el).find(idSelector).val(editor.getSession().getValue());
+ $(node.el).find(idSelector).change();
+ }, 600);
+ editor.getSession().on('change', lazyChanged);
+
+ editor.on('blur', function() {
+ $(node.el).find(idSelector).change();
+ $(node.el).find(idSelector).trigger("blur");
+ });
+ editor.on('focus', function() {
+ $(node.el).find(idSelector).trigger("focus");
+ });
+ };
+
+ // Is there a setup hook?
+ if (window.jsonform_ace_setup) {
+ window.jsonform_ace_setup(setup);
+ return;
+ }
+
+ // Wait until ACE is loaded
+ var itv = window.setInterval(function() {
+ if (window.ace) {
+ window.clearInterval(itv);
+ setup();
+ }
+ },1000);
+ }
+ },
+ 'checkbox':{
+ 'template': '',
+ 'fieldtemplate': true,
+ 'inputfield': true,
+ 'getElement': function (el) {
+ return $(el).parent().get(0);
+ }
+ },
+ 'file':{
+ 'template':'' +
+ '/>',
+ 'fieldtemplate': true,
+ 'inputfield': true
+ },
+ 'file-hosted-public':{
+ 'template':'<% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %>\' />',
+ 'fieldtemplate': true,
+ 'inputfield': true,
+ 'getElement': function (el) {
+ return $(el).parent().get(0);
+ },
+ 'onBeforeRender': function (data, node) {
+
+ if (!node.ownerTree._transloadit_generic_public_index) {
+ node.ownerTree._transloadit_generic_public_index=1;
+ } else {
+ node.ownerTree._transloadit_generic_public_index++;
+ }
+
+ data.transloaditname = "_transloadit_jsonform_genericupload_public_"+node.ownerTree._transloadit_generic_public_index;
+
+ if (!node.ownerTree._transloadit_generic_elts) node.ownerTree._transloadit_generic_elts = {};
+ node.ownerTree._transloadit_generic_elts[data.transloaditname] = node;
+ },
+ 'onChange': function(evt,elt) {
+ // The "transloadit" function should be called only once to enable
+ // the service when the form is submitted. Has it already been done?
+ if (elt.ownerTree._transloadit_bound) {
+ return false;
+ }
+ elt.ownerTree._transloadit_bound = true;
+
+ // Call the "transloadit" function on the form element
+ var formElt = $(elt.ownerTree.domRoot);
+ formElt.transloadit({
+ autoSubmit: false,
+ wait: true,
+ onSuccess: function (assembly) {
+ // Image has been uploaded. Check the "results" property that
+ // contains the list of files that Transloadit produced. There
+ // should be one image per file input in the form at most.
+ // console.log(assembly.results);
+ var results = _.values(assembly.results);
+ results = _.flatten(results);
+ _.each(results, function (result) {
+ // Save the assembly result in the right hidden input field
+ var id = elt.ownerTree._transloadit_generic_elts[result.field].id;
+ var input = formElt.find('#' + escapeSelector(id));
+ var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {
+ return !!isSet(result.meta[key]);
+ });
+ result.meta = _.pick(result.meta, nonEmptyKeys);
+ input.val(JSON.stringify(result));
+ });
+
+ // Unbind transloadit from the form
+ elt.ownerTree._transloadit_bound = false;
+ formElt.unbind('submit.transloadit');
+
+ // Submit the form on next tick
+ _.delay(function () {
+ console.log('submit form');
+ elt.ownerTree.submit();
+ }, 10);
+ },
+ onError: function (assembly) {
+ // TODO: report the error to the user
+ console.log('assembly error', assembly);
+ }
+ });
+ },
+ 'onInsert': function (evt, node) {
+ $(node.el).find('a._jsonform-delete').on('click', function (evt) {
+ $(node.el).find('._jsonform-preview').remove();
+ $(node.el).find('a._jsonform-delete').remove();
+ $(node.el).find('#' + escapeSelector(node.id)).val('');
+ evt.preventDefault();
+ return false;
+ });
+ },
+ 'onSubmit':function(evt, elt) {
+ if (elt.ownerTree._transloadit_bound) {
+ return false;
+ }
+ return true;
+ }
+
+ },
+ 'file-transloadit': {
+ 'template': '<% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %>\' />',
+ 'fieldtemplate': true,
+ 'inputfield': true,
+ 'getElement': function (el) {
+ return $(el).parent().get(0);
+ },
+ 'onChange': function (evt, elt) {
+ // The "transloadit" function should be called only once to enable
+ // the service when the form is submitted. Has it already been done?
+ if (elt.ownerTree._transloadit_bound) {
+ return false;
+ }
+ elt.ownerTree._transloadit_bound = true;
+
+ // Call the "transloadit" function on the form element
+ var formElt = $(elt.ownerTree.domRoot);
+ formElt.transloadit({
+ autoSubmit: false,
+ wait: true,
+ onSuccess: function (assembly) {
+ // Image has been uploaded. Check the "results" property that
+ // contains the list of files that Transloadit produced. Note
+ // JSONForm only supports 1-to-1 associations, meaning it
+ // expects the "results" property to contain only one image
+ // per file input in the form.
+ // console.log(assembly.results);
+ var results = _.values(assembly.results);
+ results = _.flatten(results);
+ _.each(results, function (result) {
+ // Save the assembly result in the right hidden input field
+ var input = formElt.find('input[name="' +
+ result.field.replace(/^_transloadit_/, '') +
+ '"]');
+ var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {
+ return !!isSet(result.meta[key]);
+ });
+ result.meta = _.pick(result.meta, nonEmptyKeys);
+ input.val(JSON.stringify(result));
+ });
+
+ // Unbind transloadit from the form
+ elt.ownerTree._transloadit_bound = false;
+ formElt.unbind('submit.transloadit');
+
+ // Submit the form on next tick
+ _.delay(function () {
+ console.log('submit form');
+ elt.ownerTree.submit();
+ }, 10);
+ },
+ onError: function (assembly) {
+ // TODO: report the error to the user
+ console.log('assembly error', assembly);
+ }
+ });
+ },
+ 'onInsert': function (evt, node) {
+ $(node.el).find('a._jsonform-delete').on('click', function (evt) {
+ $(node.el).find('._jsonform-preview').remove();
+ $(node.el).find('a._jsonform-delete').remove();
+ $(node.el).find('#' + escapeSelector(node.id)).val('');
+ evt.preventDefault();
+ return false;
+ });
+ },
+ 'onSubmit': function (evt, elt) {
+ if (elt.ownerTree._transloadit_bound) {
+ return false;
+ }
+ return true;
+ }
+ },
+ 'select':{
+ 'template':'',
+ 'fieldtemplate': true,
+ 'inputfield': true
+ },
+ 'imageselect': {
+ 'template': '
',
+ 'fieldtemplate': true,
+ 'array': true,
+ 'childTemplate': function (inner) {
+ if ($('').sortable) {
+ // Insert a "draggable" icon
+ // floating to the left of the main element
+ return '
' +
+ '' +
+ inner +
+ '
';
+ }
+ else {
+ return '
' +
+ inner +
+ '
';
+ }
+ },
+ 'onInsert': function (evt, node) {
+ var $nodeid = $(node.el).find('#' + escapeSelector(node.id));
+ var boundaries = node.getArrayBoundaries();
+
+ // Switch two nodes in an array
+ var moveNodeTo = function (fromIdx, toIdx) {
+ // Note "switchValuesWith" extracts values from the DOM since field
+ // values are not synchronized with the tree data structure, so calls
+ // to render are needed at each step to force values down to the DOM
+ // before next move.
+ // TODO: synchronize field values and data structure completely and
+ // call render only once to improve efficiency.
+ if (fromIdx === toIdx) return;
+ var incr = (fromIdx < toIdx) ? 1: -1;
+ var i = 0;
+ var parentEl = $('> ul', $nodeid);
+ for (i = fromIdx; i !== toIdx; i += incr) {
+ node.children[i].switchValuesWith(node.children[i + incr]);
+ node.children[i].render(parentEl.get(0));
+ node.children[i + incr].render(parentEl.get(0));
+ }
+
+ // No simple way to prevent DOM reordering with jQuery UI Sortable,
+ // so we're going to need to move sorted DOM elements back to their
+ // origin position in the DOM ourselves (we switched values but not
+ // DOM elements)
+ var fromEl = $(node.children[fromIdx].el);
+ var toEl = $(node.children[toIdx].el);
+ fromEl.detach();
+ toEl.detach();
+ if (fromIdx < toIdx) {
+ if (fromIdx === 0) parentEl.prepend(fromEl);
+ else $(node.children[fromIdx-1].el).after(fromEl);
+ $(node.children[toIdx-1].el).after(toEl);
+ }
+ else {
+ if (toIdx === 0) parentEl.prepend(toEl);
+ else $(node.children[toIdx-1].el).after(toEl);
+ $(node.children[fromIdx-1].el).after(fromEl);
+ }
+ };
+
+ $('> span > a._jsonform-array-addmore', $nodeid).click(function (evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ var idx = node.children.length;
+ if (boundaries.maxItems >= 0) {
+ if (node.children.length > boundaries.maxItems - 2) {
+ $nodeid.find('> span > a._jsonform-array-addmore')
+ .addClass('disabled');
+ }
+ if (node.children.length > boundaries.maxItems - 1) {
+ return false;
+ }
+ }
+ node.insertArrayItem(idx, $('> ul', $nodeid).get(0));
+ if ((boundaries.minItems <= 0) ||
+ ((boundaries.minItems > 0) &&
+ (node.children.length > boundaries.minItems - 1))) {
+ $nodeid.find('> span > a._jsonform-array-deletelast')
+ .removeClass('disabled');
+ }
+ });
+
+ //Simulate Users click to setup the form with its minItems
+ var curItems = $('> ul > li', $nodeid).length;
+ if ((boundaries.minItems > 0) &&
+ (curItems < boundaries.minItems)) {
+ for (var i = 0; i < (boundaries.minItems - 1) && ($nodeid.find('> ul > li').length < boundaries.minItems); i++) {
+ //console.log('Calling click: ',$nodeid);
+ //$('> span > a._jsonform-array-addmore', $nodeid).click();
+ node.insertArrayItem(curItems, $nodeid.find('> ul').get(0));
+ }
+ }
+ if ((boundaries.minItems > 0) &&
+ (node.children.length <= boundaries.minItems)) {
+ $nodeid.find('> span > a._jsonform-array-deletelast')
+ .addClass('disabled');
+ }
+
+ $('> span > a._jsonform-array-deletelast', $nodeid).click(function (evt) {
+ var idx = node.children.length - 1;
+ evt.preventDefault();
+ evt.stopPropagation();
+ if (boundaries.minItems > 0) {
+ if (node.children.length < boundaries.minItems + 2) {
+ $nodeid.find('> span > a._jsonform-array-deletelast')
+ .addClass('disabled');
+ }
+ if (node.children.length <= boundaries.minItems) {
+ return false;
+ }
+ }
+ else if (node.children.length === 1) {
+ $nodeid.find('> span > a._jsonform-array-deletelast')
+ .addClass('disabled');
+ }
+ node.deleteArrayItem(idx);
+ if ((boundaries.maxItems >= 0) && (idx <= boundaries.maxItems - 1)) {
+ $nodeid.find('> span > a._jsonform-array-addmore')
+ .removeClass('disabled');
+ }
+ });
+
+ if ($(node.el).sortable) {
+ $('> ul', $nodeid).sortable();
+ $('> ul', $nodeid).bind('sortstop', function (event, ui) {
+ var idx = $(ui.item).data('idx');
+ var newIdx = $(ui.item).index();
+ moveNodeTo(idx, newIdx);
+ });
+ }
+ }
+ },
+ 'tabarray': {
+ 'template': '
';
+ });
+ data.tabs = tabs;
+ },
+ 'onInsert': function (evt, node) {
+ var $nodeid = $(node.el).find('#' + escapeSelector(node.id));
+ var boundaries = node.getArrayBoundaries();
+
+ var moveNodeTo = function (fromIdx, toIdx) {
+ // Note "switchValuesWith" extracts values from the DOM since field
+ // values are not synchronized with the tree data structure, so calls
+ // to render are needed at each step to force values down to the DOM
+ // before next move.
+ // TODO: synchronize field values and data structure completely and
+ // call render only once to improve efficiency.
+ if (fromIdx === toIdx) return;
+ var incr = (fromIdx < toIdx) ? 1: -1;
+ var i = 0;
+ var tabEl = $('> .tabbable > .tab-content', $nodeid).get(0);
+ for (i = fromIdx; i !== toIdx; i += incr) {
+ node.children[i].switchValuesWith(node.children[i + incr]);
+ node.children[i].render(tabEl);
+ node.children[i + incr].render(tabEl);
+ }
+ };
+
+
+ // Refreshes the list of tabs
+ var updateTabs = function (selIdx) {
+ var tabs = '';
+ var activateFirstTab = false;
+ if (selIdx === undefined) {
+ selIdx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx');
+ if (selIdx) {
+ selIdx = parseInt(selIdx, 10);
+ }
+ else {
+ activateFirstTab = true;
+ selIdx = 0;
+ }
+ }
+ if (selIdx >= node.children.length) {
+ selIdx = node.children.length - 1;
+ }
+ _.each(node.children, function (child, idx) {
+ var title = child.legend ||
+ child.title ||
+ ('Item ' + (idx+1));
+ tabs += '
';
+ },
+ 'onBeforeRender': function (data, node) {
+ // Before rendering, this function ensures that:
+ // 1. direct children have IDs (used to show/hide the tabs contents)
+ // 2. the tab to active is flagged accordingly. The active tab is
+ // the first one, except if form values are available, in which case
+ // it's the first tab for which there is some value available (or back
+ // to the first one if there are none)
+ // 3. the HTML of the select field used to select tabs is exposed in the
+ // HTML template data as "tabs"
+
+ var children = null;
+ var choices = [];
+ if (node.schemaElement) {
+ choices = node.schemaElement['enum'] || [];
+ }
+ if (node.options) {
+ children = _.map(node.options, function (option, idx) {
+ var child = node.children[idx];
+ if (option instanceof Object) {
+ option = _.extend({ node: child }, option);
+ option.title = option.title ||
+ child.legend ||
+ child.title ||
+ ('Option ' + (child.childPos+1));
+ option.value = isSet(option.value) ? option.value :
+ isSet(choices[idx]) ? choices[idx] : idx;
+ return option;
+ }
+ else {
+ return {
+ title: option,
+ value: isSet(choices[child.childPos]) ?
+ choices[child.childPos] :
+ child.childPos,
+ node: child
+ };
+ }
+ });
+ }
+ else {
+ children = _.map(node.children, function (child, idx) {
+ return {
+ title: child.legend || child.title || ('Option ' + (child.childPos+1)),
+ value: choices[child.childPos] || child.childPos,
+ node: child
+ };
+ });
+ }
+
+ var activeChild = null;
+ if (data.value) {
+ activeChild = _.find(children, function (child) {
+ return (child.value === node.value);
+ });
+ }
+ if (!activeChild) {
+ activeChild = _.find(children, function (child) {
+ return child.node.hasNonDefaultValue();
+ });
+ }
+ if (!activeChild) {
+ activeChild = children[0];
+ }
+ activeChild.node.active = true;
+ data.value = activeChild.value;
+
+ var elt = node.formElement;
+ var tabs = '';
+
+ data.tabs = tabs;
+ return data;
+ },
+ 'onInsert': function (evt, node) {
+ $(node.el).find('select.nav').first().on('change', function (evt) {
+ var $option = $(this).find('option:selected');
+ $(node.el).find('input[type="hidden"]').first().val($option.attr('value'));
+ });
+ }
+ },
+ 'optionfieldset': {
+ 'template': '
'
+ },
+
+ /**
+ * A "questions" field renders a series of question fields and binds the
+ * result to the value of a schema key.
+ */
+ 'questions': {
+ 'template': '
' +
+ '' +
+ '<%= children %>' +
+ '
',
+ 'fieldtempate': true,
+ 'inputfield': true,
+ 'getElement': function (el) {
+ return $(el).parent().get(0);
+ },
+ 'onInsert': function (evt, node) {
+ if (!node.children || (node.children.length === 0)) return;
+ _.each(node.children, function (child) {
+ $(child.el).hide();
+ });
+ $(node.children[0].el).show();
+ }
+ },
+
+ /**
+ * A "question" field lets user choose a response among possible choices.
+ * The field is not associated with any schema key. A question should be
+ * part of a "questions" field that binds a series of questions to a
+ * schema key.
+ */
+ 'question': {
+ 'template': '
',
+ 'fieldtemplate': true,
+ 'onInsert': function (evt, node) {
+ var activeClass = 'active';
+ var elt = node.formElement || {};
+ if (elt.activeClass) {
+ activeClass += ' ' + elt.activeClass;
+ }
+
+ // Bind to change events on radio buttons
+ $(node.el).find('input[type="radio"]').on('change', function (evt) {
+ var questionNode = null;
+ var option = node.options[$(this).val()];
+ if (!node.parentNode || !node.parentNode.el) return;
+
+ $(this).parent().parent().find('label').removeClass(activeClass);
+ $(this).parent().addClass(activeClass);
+ $(node.el).nextAll().hide();
+ $(node.el).nextAll().find('input[type="radio"]').prop('checked', false);
+
+ // Execute possible actions (set key value, form submission, open link,
+ // move on to next question)
+ if (option.value) {
+ // Set the key of the 'Questions' parent
+ $(node.parentNode.el).find('input[type="hidden"]').val(option.value);
+ }
+ if (option.next) {
+ questionNode = _.find(node.parentNode.children, function (child) {
+ return (child.formElement && (child.formElement.qid === option.next));
+ });
+ $(questionNode.el).show();
+ $(questionNode.el).nextAll().hide();
+ $(questionNode.el).nextAll().find('input[type="radio"]').prop('checked', false);
+ }
+ if (option.href) {
+ if (option.target) {
+ window.open(option.href, option.target);
+ }
+ else {
+ window.location = option.href;
+ }
+ }
+ if (option.submit) {
+ setTimeout(function () {
+ node.ownerTree.submit();
+ }, 0);
+ }
+ });
+ }
+ }
+};
+
+
+//Allow to access subproperties by splitting "."
+/**
+ * Retrieves the key identified by a path selector in the structured object.
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with [x]. For instance:
+ * foo.bar[3].baz
+ *
+ * @function
+ * @param {Object} obj Structured object to parse
+ * @param {String} key Path to the key to retrieve
+ * @param {boolean} ignoreArrays True to use first element in an array when
+ * stucked on a property. This parameter is basically only useful when
+ * parsing a JSON schema for which the "items" property may either be an
+ * object or an array with one object (only one because JSON form does not
+ * support mix of items for arrays).
+ * @return {Object} The key's value.
+ */
+jsonform.util.getObjKey = function (obj, key, ignoreArrays) {
+ var innerobj = obj;
+ var keyparts = key.split(".");
+ var subkey = null;
+ var arrayMatch = null;
+ var prop = null;
+
+ for (var i = 0; i < keyparts.length; i++) {
+ if ((innerobj === null) || (typeof innerobj !== "object")) return null;
+ subkey = keyparts[i];
+ prop = subkey.replace(reArray, '');
+ reArray.lastIndex = 0;
+ arrayMatch = reArray.exec(subkey);
+ if (arrayMatch) {
+ while (true) {
+ if (!_.isArray(innerobj[prop])) return null;
+ innerobj = innerobj[prop][parseInt(arrayMatch[1], 10)];
+ arrayMatch = reArray.exec(subkey);
+ if (!arrayMatch) break;
+ }
+ }
+ else if (ignoreArrays &&
+ !innerobj[prop] &&
+ _.isArray(innerobj) &&
+ innerobj[0]) {
+ innerobj = innerobj[0][prop];
+ }
+ else {
+ innerobj = innerobj[prop];
+ }
+ }
+
+ if (ignoreArrays && _.isArray(innerobj) && innerobj[0]) {
+ return innerobj[0];
+ }
+ else {
+ return innerobj;
+ }
+};
+
+
+/**
+ * Sets the key identified by a path selector to the given value.
+ *
+ * Levels in the path are separated by a dot. Array items are marked
+ * with [x]. For instance:
+ * foo.bar[3].baz
+ *
+ * The hierarchy is automatically created if it does not exist yet.
+ *
+ * @function
+ * @param {Object} obj The object to build
+ * @param {String} key The path to the key to set where each level
+ * is separated by a dot, and array items are flagged with [x].
+ * @param {Object} value The value to set, may be of any type.
+ */
+jsonform.util.setObjKey = function(obj,key,value) {
+ var innerobj = obj;
+ var keyparts = key.split(".");
+ var subkey = null;
+ var arrayMatch = null;
+ var prop = null;
+
+ for (var i = 0; i < keyparts.length-1; i++) {
+ subkey = keyparts[i];
+ prop = subkey.replace(reArray, '');
+ reArray.lastIndex = 0;
+ arrayMatch = reArray.exec(subkey);
+ if (arrayMatch) {
+ // Subkey is part of an array
+ while (true) {
+ if (!_.isArray(innerobj[prop])) {
+ innerobj[prop] = [];
+ }
+ innerobj = innerobj[prop];
+ prop = parseInt(arrayMatch[1], 10);
+ arrayMatch = reArray.exec(subkey);
+ if (!arrayMatch) break;
+ }
+ if ((typeof innerobj[prop] !== 'object') ||
+ (innerobj[prop] === null)) {
+ innerobj[prop] = {};
+ }
+ innerobj = innerobj[prop];
+ }
+ else {
+ // "Normal" subkey
+ if ((typeof innerobj[prop] !== 'object') ||
+ (innerobj[prop] === null)) {
+ innerobj[prop] = {};
+ }
+ innerobj = innerobj[prop];
+ }
+ }
+
+ // Set the final value
+ subkey = keyparts[keyparts.length - 1];
+ prop = subkey.replace(reArray, '');
+ reArray.lastIndex = 0;
+ arrayMatch = reArray.exec(subkey);
+ if (arrayMatch) {
+ while (true) {
+ if (!_.isArray(innerobj[prop])) {
+ innerobj[prop] = [];
+ }
+ innerobj = innerobj[prop];
+ prop = parseInt(arrayMatch[1], 10);
+ arrayMatch = reArray.exec(subkey);
+ if (!arrayMatch) break;
+ }
+ innerobj[prop] = value;
+ }
+ else {
+ innerobj[prop] = value;
+ }
+};
+
+
+/**
+ * Retrieves the key definition from the given schema.
+ *
+ * The key is identified by the path that leads to the key in the
+ * structured object that the schema would generate. Each level is
+ * separated by a '.'. Array levels are marked with []. For instance:
+ * foo.bar[].baz
+ * ... to retrieve the definition of the key at the following location
+ * in the JSON schema (using a dotted path notation):
+ * foo.properties.bar.items.properties.baz
+ *
+ * @function
+ * @param {Object} schema The JSON schema to retrieve the key from
+ * @param {String} key The path to the key, each level being separated
+ * by a dot and array items being flagged with [].
+ * @return {Object} The key definition in the schema, null if not found.
+ */
+var getSchemaKey = function(schema,key) {
+ var schemaKey = key
+ .replace(/\./g, '.properties.')
+ .replace(/\[[0-9]*\]/g, '.items');
+ var schemaDef = jsonform.util.getObjKey(schema, schemaKey, true);
+ if (schemaDef && schemaDef.$ref) {
+ throw new Error('JSONForm does not yet support schemas that use the ' +
+ '$ref keyword. See: https://github.com/joshfire/jsonform/issues/54');
+ }
+ return schemaDef;
+};
+
+
+/**
+ * Truncates the key path to the requested depth.
+ *
+ * For instance, if the key path is:
+ * foo.bar[].baz.toto[].truc[].bidule
+ * and the requested depth is 1, the returned key will be:
+ * foo.bar[].baz.toto
+ *
+ * Note the function includes the path up to the next depth level.
+ *
+ * @function
+ * @param {String} key The path to the key in the schema, each level being
+ * separated by a dot and array items being flagged with [].
+ * @param {Number} depth The array depth
+ * @return {String} The path to the key truncated to the given depth.
+ */
+var truncateToArrayDepth = function (key, arrayDepth) {
+ var depth = 0;
+ var pos = 0;
+ if (!key) return null;
+
+ if (arrayDepth > 0) {
+ while (depth < arrayDepth) {
+ pos = key.indexOf('[]', pos);
+ if (pos === -1) {
+ // Key path is not "deep" enough, simply return the full key
+ return key;
+ }
+ pos = pos + 2;
+ depth += 1;
+ }
+ }
+
+ // Move one step further to the right without including the final []
+ pos = key.indexOf('[]', pos);
+ if (pos === -1) return key;
+ else return key.substring(0, pos);
+};
+
+/**
+ * Applies the array path to the key path.
+ *
+ * For instance, if the key path is:
+ * foo.bar[].baz.toto[].truc[].bidule
+ * and the arrayPath [4, 2], the returned key will be:
+ * foo.bar[4].baz.toto[2].truc[].bidule
+ *
+ * @function
+ * @param {String} key The path to the key in the schema, each level being
+ * separated by a dot and array items being flagged with [].
+ * @param {Array(Number)} arrayPath The array path to apply, e.g. [4, 2]
+ * @return {String} The path to the key that matches the array path.
+ */
+var applyArrayPath = function (key, arrayPath) {
+ var depth = 0;
+ if (!key) return null;
+ if (!arrayPath || (arrayPath.length === 0)) return key;
+ var newKey = key.replace(reArray, function (str, p1) {
+ // Note this function gets called as many times as there are [x] in the ID,
+ // from left to right in the string. The goal is to replace the [x] with
+ // the appropriate index in the new array path, if defined.
+ var newIndex = str;
+ if (isSet(arrayPath[depth])) {
+ newIndex = '[' + arrayPath[depth] + ']';
+ }
+ depth += 1;
+ return newIndex;
+ });
+ return newKey;
+};
+
+
+/**
+ * Returns the initial value that a field identified by its key
+ * should take.
+ *
+ * The "initial" value is defined as:
+ * 1. the previously submitted value if already submitted
+ * 2. the default value defined in the layout of the form
+ * 3. the default value defined in the schema
+ *
+ * The "value" returned is intended for rendering purpose,
+ * meaning that, for fields that define a titleMap property,
+ * the function returns the label, and not the intrinsic value.
+ *
+ * The function handles values that contains template strings,
+ * e.g. {{values.foo[].bar}} or {{idx}}.
+ *
+ * When the form is a string, the function truncates the resulting string
+ * to meet a potential "maxLength" constraint defined in the schema, using
+ * "..." to mark the truncation. Note it does not validate the resulting
+ * string against other constraints (e.g. minLength, pattern) as it would
+ * be hard to come up with an automated course of action to "fix" the value.
+ *
+ * @function
+ * @param {Object} formObject The JSON Form object
+ * @param {String} key The generic key path (e.g. foo[].bar.baz[])
+ * @param {Array(Number)} arrayPath The array path that identifies
+ * the unique value in the submitted form (e.g. [1, 3])
+ * @param {Object} tpldata Template data object
+ * @param {Boolean} usePreviousValues true to use previously submitted values
+ * if defined.
+ */
+var getInitialValue = function (formObject, key, arrayPath, tpldata, usePreviousValues) {
+ var value = null;
+
+ // Complete template data for template function
+ tpldata = tpldata || {};
+ tpldata.idx = tpldata.idx ||
+ (arrayPath ? arrayPath[arrayPath.length-1] : 1);
+ tpldata.value = isSet(tpldata.value) ? tpldata.value : '';
+ tpldata.getValue = tpldata.getValue || function (key) {
+ return getInitialValue(formObject, key, arrayPath, tpldata, usePreviousValues);
+ };
+
+ // Helper function that returns the form element that explicitly
+ // references the given key in the schema.
+ var getFormElement = function (elements, key) {
+ var formElement = null;
+ if (!elements || !elements.length) return null;
+ _.each(elements, function (elt) {
+ if (formElement) return;
+ if (elt === key) {
+ formElement = { key: elt };
+ return;
+ }
+ if (_.isString(elt)) return;
+ if (elt.key === key) {
+ formElement = elt;
+ }
+ else if (elt.items) {
+ formElement = getFormElement(elt.items, key);
+ }
+ });
+ return formElement;
+ };
+ var formElement = getFormElement(formObject.form || [], key);
+ var schemaElement = getSchemaKey(formObject.schema.properties, key);
+
+ if (usePreviousValues && formObject.value) {
+ // If values were previously submitted, use them directly if defined
+ value = jsonform.util.getObjKey(formObject.value, applyArrayPath(key, arrayPath));
+ }
+ if (!isSet(value)) {
+ if (formElement && (typeof formElement['value'] !== 'undefined')) {
+ // Extract the definition of the form field associated with
+ // the key as it may override the schema's default value
+ // (note a "null" value overrides a schema default value as well)
+ value = formElement['value'];
+ }
+ else if (schemaElement) {
+ // Simply extract the default value from the schema
+ if (isSet(schemaElement['default'])) {
+ value = schemaElement['default'];
+ }
+ }
+ if (value && value.indexOf('{{values.') !== -1) {
+ // This label wants to use the value of another input field.
+ // Convert that construct into {{getValue(key)}} for
+ // Underscore to call the appropriate function of formData
+ // when template gets called (note calling a function is not
+ // exactly Mustache-friendly but is supported by Underscore).
+ value = value.replace(
+ /\{\{values\.([^\}]+)\}\}/g,
+ '{{getValue("$1")}}');
+ }
+ if (value) {
+ value = _.template(value, tpldata, valueTemplateSettings);
+ }
+ }
+
+ // Apply titleMap if needed
+ if (isSet(value) && formElement &&
+ formElement.titleMap &&
+ formElement.titleMap[value]) {
+ value = _.template(formElement.titleMap[value],
+ tpldata, valueTemplateSettings);
+ }
+
+ // Check maximum length of a string
+ if (value && _.isString(value) &&
+ schemaElement && schemaElement.maxLength) {
+ if (value.length > schemaElement.maxLength) {
+ // Truncate value to maximum length, adding continuation dots
+ value = value.substr(0, schemaElement.maxLength - 1) + '…';
+ }
+ }
+
+ if (!isSet(value)) {
+ return null;
+ }
+ else {
+ return value;
+ }
+};
+
+
+/**
+ * Represents a node in the form.
+ *
+ * Nodes that have an ID are linked to the corresponding DOM element
+ * when rendered
+ *
+ * Note the form element and the schema elements that gave birth to the
+ * node may be shared among multiple nodes (in the case of arrays).
+ *
+ * @class
+ */
+var formNode = function () {
+ /**
+ * The node's ID (may not be set)
+ */
+ this.id = null;
+
+ /**
+ * The node's key path (may not be set)
+ */
+ this.key = null;
+
+ /**
+ * DOM element associated witht the form element.
+ *
+ * The DOM element is set when the form element is rendered.
+ */
+ this.el = null;
+
+ /**
+ * Link to the form element that describes the node's layout
+ * (note the form element is shared among nodes in arrays)
+ */
+ this.formElement = null;
+
+ /**
+ * Link to the schema element that describes the node's value constraints
+ * (note the schema element is shared among nodes in arrays)
+ */
+ this.schemaElement = null;
+
+ /**
+ * Pointer to the "view" associated with the node, typically the right
+ * object in jsonform.elementTypes
+ */
+ this.view = null;
+
+ /**
+ * Node's subtree (if one is defined)
+ */
+ this.children = [];
+
+ /**
+ * A pointer to the form tree the node is attached to
+ */
+ this.ownerTree = null;
+
+ /**
+ * A pointer to the parent node of the node in the tree
+ */
+ this.parentNode = null;
+
+ /**
+ * Child template for array-like nodes.
+ *
+ * The child template gets cloned to create new array items.
+ */
+ this.childTemplate = null;
+
+
+ /**
+ * Direct children of array-like containers may use the value of a
+ * specific input field in their subtree as legend. The link to the
+ * legend child is kept here and initialized in computeInitialValues
+ * when a child sets "valueInLegend"
+ */
+ this.legendChild = null;
+
+
+ /**
+ * The path of indexes that lead to the current node when the
+ * form element is not at the root array level.
+ *
+ * Note a form element may well be nested element and still be
+ * at the root array level. That's typically the case for "fieldset"
+ * elements. An array level only gets created when a form element
+ * is of type "array" (or a derivated type such as "tabarray").
+ *
+ * The array path of a form element linked to the foo[2].bar.baz[3].toto
+ * element in the submitted values is [2, 3] for instance.
+ *
+ * The array path is typically used to compute the right ID for input
+ * fields. It is also used to update positions when an array item is
+ * created, moved around or suppressed.
+ *
+ * @type {Array(Number)}
+ */
+ this.arrayPath = [];
+
+ /**
+ * Position of the node in the list of children of its parents
+ */
+ this.childPos = 0;
+};
+
+
+/**
+ * Clones a node
+ *
+ * @function
+ * @param {formNode} New parent node to attach the node to
+ * @return {formNode} Cloned node
+ */
+formNode.prototype.clone = function (parentNode) {
+ var node = new formNode();
+ node.arrayPath = _.clone(this.arrayPath);
+ node.ownerTree = this.ownerTree;
+ node.parentNode = parentNode || this.parentNode;
+ node.formElement = this.formElement;
+ node.schemaElement = this.schemaElement;
+ node.view = this.view;
+ node.children = _.map(this.children, function (child) {
+ return child.clone(node);
+ });
+ if (this.childTemplate) {
+ node.childTemplate = this.childTemplate.clone(node);
+ }
+ return node;
+};
+
+
+/**
+ * Returns true if the subtree that starts at the current node
+ * has some non empty value attached to it
+ */
+formNode.prototype.hasNonDefaultValue = function () {
+
+ // hidden elements don't count because they could make the wrong selectfieldset element active
+ if (this.formElement && this.formElement.type=="hidden") {
+ return false;
+ }
+
+ if (this.value && !this.defaultValue) {
+ return true;
+ }
+ var child = _.find(this.children, function (child) {
+ return child.hasNonDefaultValue();
+ });
+ return !!child;
+};
+
+
+/**
+ * Attaches a child node to the current node.
+ *
+ * The child node is appended to the end of the list.
+ *
+ * @function
+ * @param {formNode} node The child node to append
+ * @return {formNode} The inserted node (same as the one given as parameter)
+ */
+formNode.prototype.appendChild = function (node) {
+ node.parentNode = this;
+ node.childPos = this.children.length;
+ this.children.push(node);
+ return node;
+};
+
+
+/**
+ * Removes the last child of the node.
+ *
+ * @function
+ */
+formNode.prototype.removeChild = function () {
+ var child = this.children[this.children.length-1];
+ if (!child) return;
+
+ // Remove the child from the DOM
+ $(child.el).remove();
+
+ // Remove the child from the array
+ return this.children.pop();
+};
+
+
+/**
+ * Moves the user entered values set in the current node's subtree to the
+ * given node's subtree.
+ *
+ * The target node must follow the same structure as the current node
+ * (typically, they should have been generated from the same node template)
+ *
+ * The current node MUST be rendered in the DOM.
+ *
+ * TODO: when current node is not in the DOM, extract values from formNode.value
+ * properties, so that the function be available even when current node is not
+ * in the DOM.
+ *
+ * Moving values around allows to insert/remove array items at arbitrary
+ * positions.
+ *
+ * @function
+ * @param {formNode} node Target node.
+ */
+formNode.prototype.moveValuesTo = function (node) {
+ var values = this.getFormValues(node.arrayPath);
+ node.resetValues();
+ node.computeInitialValues(values, true);
+};
+
+
+/**
+ * Switches nodes user entered values.
+ *
+ * The target node must follow the same structure as the current node
+ * (typically, they should have been generated from the same node template)
+ *
+ * Both nodes MUST be rendered in the DOM.
+ *
+ * TODO: update getFormValues to work even if node is not rendered, using
+ * formNode's "value" property.
+ *
+ * @function
+ * @param {formNode} node Target node
+ */
+formNode.prototype.switchValuesWith = function (node) {
+ var values = this.getFormValues(node.arrayPath);
+ var nodeValues = node.getFormValues(this.arrayPath);
+ node.resetValues();
+ node.computeInitialValues(values, true);
+ this.resetValues();
+ this.computeInitialValues(nodeValues, true);
+};
+
+
+/**
+ * Resets all DOM values in the node's subtree.
+ *
+ * This operation also drops all array item nodes.
+ * Note values are not reset to their default values, they are rather removed!
+ *
+ * @function
+ */
+formNode.prototype.resetValues = function () {
+ var params = null;
+ var idx = 0;
+
+ // Reset value
+ this.value = null;
+
+ // Propagate the array path from the parent node
+ // (adding the position of the child for nodes that are direct
+ // children of array-like nodes)
+ if (this.parentNode) {
+ this.arrayPath = _.clone(this.parentNode.arrayPath);
+ if (this.parentNode.view && this.parentNode.view.array) {
+ this.arrayPath.push(this.childPos);
+ }
+ }
+ else {
+ this.arrayPath = [];
+ }
+
+ if (this.view && this.view.inputfield) {
+ // Simple input field, extract the value from the origin,
+ // set the target value and reset the origin value
+ params = $(':input', this.el).serializeArray();
+ _.each(params, function (param) {
+ // TODO: check this, there may exist corner cases with this approach
+ // (with multiple checkboxes for instance)
+ $('[name="' + escapeSelector(param.name) + '"]', $(this.el)).val('');
+ }, this);
+ }
+ else if (this.view && this.view.array) {
+ // The current node is an array, drop all children
+ while (this.children.length > 0) {
+ this.removeChild();
+ }
+ }
+
+ // Recurse down the tree
+ _.each(this.children, function (child) {
+ child.resetValues();
+ });
+};
+
+
+/**
+ * Sets the child template node for the current node.
+ *
+ * The child template node is used to create additional children
+ * in an array-like form element. The template is never rendered.
+ *
+ * @function
+ * @param {formNode} node The child template node to set
+ */
+formNode.prototype.setChildTemplate = function (node) {
+ this.childTemplate = node;
+ node.parentNode = this;
+};
+
+
+/**
+ * Recursively sets values to all nodes of the current subtree
+ * based on previously submitted values, or based on default
+ * values when the submitted values are not enough
+ *
+ * The function should be called once in the lifetime of a node
+ * in the tree. It expects its parent's arrayPath to be up to date.
+ *
+ * Three cases may arise:
+ * 1. if the form element is a simple input field, the value is
+ * extracted from previously submitted values of from default values
+ * defined in the schema.
+ * 2. if the form element is an array-like node, the child template
+ * is used to create as many children as possible (and at least one).
+ * 3. the function simply recurses down the node's subtree otherwise
+ * (this happens when the form element is a fieldset-like element).
+ *
+ * @function
+ * @param {Object} values Previously submitted values for the form
+ * @param {Boolean} ignoreDefaultValues Ignore default values defined in the
+ * schema when set.
+ */
+formNode.prototype.computeInitialValues = function (values, ignoreDefaultValues) {
+ var self = this;
+ var node = null;
+ var nbChildren = 1;
+ var i = 0;
+ var formData = this.ownerTree.formDesc.tpldata || {};
+
+ // Propagate the array path from the parent node
+ // (adding the position of the child for nodes that are direct
+ // children of array-like nodes)
+ if (this.parentNode) {
+ this.arrayPath = _.clone(this.parentNode.arrayPath);
+ if (this.parentNode.view && this.parentNode.view.array) {
+ this.arrayPath.push(this.childPos);
+ }
+ }
+ else {
+ this.arrayPath = [];
+ }
+
+ // Prepare special data param "idx" for templated values
+ // (is is the index of the child in its wrapping array, starting
+ // at 1 since that's more human-friendly than a zero-based index)
+ formData.idx = (this.arrayPath.length > 0) ?
+ this.arrayPath[this.arrayPath.length-1] + 1 :
+ this.childPos + 1;
+
+ // Prepare special data param "value" for templated values
+ formData.value = '';
+
+ // Prepare special function to compute the value of another field
+ formData.getValue = function (key) {
+ return getInitialValue(self.ownerTree.formDesc,
+ key, self.arrayPath,
+ formData, !!values);
+ };
+
+ if (this.formElement) {
+ // Compute the ID of the field (if needed)
+ if (this.formElement.id) {
+ this.id = applyArrayPath(this.formElement.id, this.arrayPath);
+ }
+ else if (this.view && this.view.array) {
+ this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
+ '-elt-counter-' + _.uniqueId();
+ }
+ else if (this.parentNode && this.parentNode.view &&
+ this.parentNode.view.array) {
+ // Array items need an array to associate the right DOM element
+ // to the form node when the parent is rendered.
+ this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
+ '-elt-counter-' + _.uniqueId();
+ }
+ else if ((this.formElement.type === 'button') ||
+ (this.formElement.type === 'selectfieldset') ||
+ (this.formElement.type === 'question') ||
+ (this.formElement.type === 'buttonquestion')) {
+ // Buttons do need an id for "onClick" purpose
+ this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
+ '-elt-counter-' + _.uniqueId();
+ }
+
+ // Compute the actual key (the form element's key is index-free,
+ // i.e. it looks like foo[].bar.baz[].truc, so we need to apply
+ // the array path of the node to get foo[4].bar.baz[2].truc)
+ if (this.formElement.key) {
+ this.key = applyArrayPath(this.formElement.key, this.arrayPath);
+ this.keydash = this.key.replace(/\./g, '---');
+ }
+
+ // Same idea for the field's name
+ this.name = applyArrayPath(this.formElement.name, this.arrayPath);
+
+ // Consider that label values are template values and apply the
+ // form's data appropriately (note we also apply the array path
+ // although that probably doesn't make much sense for labels...)
+ _.each([
+ 'title',
+ 'legend',
+ 'description',
+ 'append',
+ 'prepend',
+ 'inlinetitle',
+ 'helpvalue',
+ 'value',
+ 'disabled',
+ 'placeholder',
+ 'readOnly'
+ ], function (prop) {
+ if (_.isString(this.formElement[prop])) {
+ if (this.formElement[prop].indexOf('{{values.') !== -1) {
+ // This label wants to use the value of another input field.
+ // Convert that construct into {{jsonform.getValue(key)}} for
+ // Underscore to call the appropriate function of formData
+ // when template gets called (note calling a function is not
+ // exactly Mustache-friendly but is supported by Underscore).
+ this[prop] = this.formElement[prop].replace(
+ /\{\{values\.([^\}]+)\}\}/g,
+ '{{getValue("$1")}}');
+ }
+ else {
+ // Note applying the array path probably doesn't make any sense,
+ // but some geek might want to have a label "foo[].bar[].baz",
+ // with the [] replaced by the appropriate array path.
+ this[prop] = applyArrayPath(this.formElement[prop], this.arrayPath);
+ }
+ if (this[prop]) {
+ this[prop] = _.template(this[prop], formData, valueTemplateSettings);
+ }
+ }
+ else {
+ this[prop] = this.formElement[prop];
+ }
+ }, this);
+
+ // Apply templating to options created with "titleMap" as well
+ if (this.formElement.options) {
+ this.options = _.map(this.formElement.options, function (option) {
+ var title = null;
+ if (_.isObject(option) && option.title) {
+ // See a few lines above for more details about templating
+ // preparation here.
+ if (option.title.indexOf('{{values.') !== -1) {
+ title = option.title.replace(
+ /\{\{values\.([^\}]+)\}\}/g,
+ '{{getValue("$1")}}');
+ }
+ else {
+ title = applyArrayPath(option.title, self.arrayPath);
+ }
+ return _.extend({}, option, {
+ value: (isSet(option.value) ? option.value : ''),
+ title: _.template(title, formData, valueTemplateSettings)
+ });
+ }
+ else {
+ return option;
+ }
+ });
+ }
+ }
+
+ if (this.view && this.view.inputfield && this.schemaElement) {
+ // Case 1: simple input field
+ if (values) {
+ // Form has already been submitted, use former value if defined.
+ // Note we won't set the field to its default value otherwise
+ // (since the user has already rejected it)
+ if (isSet(jsonform.util.getObjKey(values, this.key))) {
+ this.value = jsonform.util.getObjKey(values, this.key);
+ }
+ }
+ else if (!ignoreDefaultValues) {
+ // No previously submitted form result, use default value
+ // defined in the schema if it's available and not already
+ // defined in the form element
+ if (!isSet(this.value) && isSet(this.schemaElement['default'])) {
+ this.value = this.schemaElement['default'];
+ if (_.isString(this.value)) {
+ if (this.value.indexOf('{{values.') !== -1) {
+ // This label wants to use the value of another input field.
+ // Convert that construct into {{jsonform.getValue(key)}} for
+ // Underscore to call the appropriate function of formData
+ // when template gets called (note calling a function is not
+ // exactly Mustache-friendly but is supported by Underscore).
+ this.value = this.value.replace(
+ /\{\{values\.([^\}]+)\}\}/g,
+ '{{getValue("$1")}}');
+ }
+ else {
+ // Note applying the array path probably doesn't make any sense,
+ // but some geek might want to have a label "foo[].bar[].baz",
+ // with the [] replaced by the appropriate array path.
+ this.value = applyArrayPath(this.value, this.arrayPath);
+ }
+ if (this.value) {
+ this.value = _.template(this.value, formData, valueTemplateSettings);
+ }
+ }
+ this.defaultValue = true;
+ }
+ }
+ }
+ else if (this.view && this.view.array) {
+ // Case 2: array-like node
+ nbChildren = 0;
+ if (values) {
+ nbChildren = this.getPreviousNumberOfItems(values, this.arrayPath);
+ }
+ // TODO: use default values at the array level when form has not been
+ // submitted before. Note it's not that easy because each value may
+ // be a complex structure that needs to be pushed down the subtree.
+ // The easiest way is probably to generate a "values" object and
+ // compute initial values from that object
+ /*
+ else if (this.schemaElement['default']) {
+ nbChildren = this.schemaElement['default'].length;
+ }
+ */
+ else if (nbChildren === 0) {
+ // If form has already been submitted with no children, the array
+ // needs to be rendered without children. If there are no previously
+ // submitted values, the array gets rendered with one empty item as
+ // it's more natural from a user experience perspective. That item can
+ // be removed with a click on the "-" button.
+ nbChildren = 1;
+ }
+ for (i = 0; i < nbChildren; i++) {
+ this.appendChild(this.childTemplate.clone());
+ }
+ }
+
+ // Case 3 and in any case: recurse through the list of children
+ _.each(this.children, function (child) {
+ child.computeInitialValues(values, ignoreDefaultValues);
+ });
+
+ // If the node's value is to be used as legend for its "container"
+ // (typically the array the node belongs to), ensure that the container
+ // has a direct link to the node for the corresponding tab.
+ if (this.formElement && this.formElement.valueInLegend) {
+ node = this;
+ while (node) {
+ if (node.parentNode &&
+ node.parentNode.view &&
+ node.parentNode.view.array) {
+ node.legendChild = this;
+ if (node.formElement && node.formElement.legend) {
+ node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);
+ formData.idx = (node.arrayPath.length > 0) ?
+ node.arrayPath[node.arrayPath.length-1] + 1 :
+ node.childPos + 1;
+ formData.value = isSet(this.value) ? this.value : '';
+ node.legend = _.template(node.legend, formData, valueTemplateSettings);
+ break;
+ }
+ }
+ node = node.parentNode;
+ }
+ }
+};
+
+
+/**
+ * Returns the number of items that the array node should have based on
+ * previously submitted values.
+ *
+ * The whole difficulty is that values may be hidden deep in the subtree
+ * of the node and may actually target different arrays in the JSON schema.
+ *
+ * @function
+ * @param {Object} values Previously submitted values
+ * @param {Array(Number)} arrayPath the array path we're interested in
+ * @return {Number} The number of items in the array
+ */
+formNode.prototype.getPreviousNumberOfItems = function (values, arrayPath) {
+ var key = null;
+ var arrayValue = null;
+ var childNumbers = null;
+ var idx = 0;
+
+ if (!values) {
+ // No previously submitted values, no need to go any further
+ return 0;
+ }
+
+ if (this.view.inputfield && this.schemaElement) {
+ // Case 1: node is a simple input field that links to a key in the schema.
+ // The schema key looks typically like:
+ // foo.bar[].baz.toto[].truc[].bidule
+ // The goal is to apply the array path and truncate the key to the last
+ // array we're interested in, e.g. with an arrayPath [4, 2]:
+ // foo.bar[4].baz.toto[2]
+ key = truncateToArrayDepth(this.formElement.key, arrayPath.length);
+ key = applyArrayPath(key, arrayPath);
+ arrayValue = jsonform.util.getObjKey(values, key);
+ if (!arrayValue) {
+ // No key? That means this field had been left empty
+ // in previous submit
+ return 0;
+ }
+ childNumbers = _.map(this.children, function (child) {
+ return child.getPreviousNumberOfItems(values, arrayPath);
+ });
+ return _.max([_.max(childNumbers) || 0, arrayValue.length]);
+ }
+ else if (this.view.array) {
+ // Case 2: node is an array-like node, look for input fields
+ // in its child template
+ return this.childTemplate.getPreviousNumberOfItems(values, arrayPath);
+ }
+ else {
+ // Case 3: node is a leaf or a container,
+ // recurse through the list of children and return the maximum
+ // number of items found in each subtree
+ childNumbers = _.map(this.children, function (child) {
+ return child.getPreviousNumberOfItems(values, arrayPath);
+ });
+ return _.max(childNumbers) || 0;
+ }
+};
+
+
+/**
+ * Returns the structured object that corresponds to the form values entered
+ * by the user for the node's subtree.
+ *
+ * The returned object follows the structure of the JSON schema that gave
+ * birth to the form.
+ *
+ * Obviously, the node must have been rendered before that function may
+ * be called.
+ *
+ * @function
+ * @param {Array(Number)} updateArrayPath Array path to use to pretend that
+ * the entered values were actually entered for another item in an array
+ * (this is used to move values around when an item is inserted/removed/moved
+ * in an array)
+ * @return {Object} The object that follows the data schema and matches the
+ * values entered by the user.
+ */
+formNode.prototype.getFormValues = function (updateArrayPath) {
+ // The values object that will be returned
+ var values = {};
+
+ if (!this.el) {
+ throw new Error('formNode.getFormValues can only be called on nodes that are associated with a DOM element in the tree');
+ }
+
+ // Form fields values
+ var formArray = $(':input', this.el).serializeArray();
+
+ // Set values to false for unset checkboxes and radio buttons
+ // because serializeArray() ignores them
+ formArray = formArray.concat(
+ $(':input[type=checkbox]:not(:disabled):not(:checked)', this.el).map( function() {
+ return {"name": this.name, "value": this.checked}
+ }).get()
+ );
+
+ if (updateArrayPath) {
+ _.each(formArray, function (param) {
+ param.name = applyArrayPath(param.name, updateArrayPath);
+ });
+ }
+
+ // The underlying data schema
+ var formSchema = this.ownerTree.formDesc.schema;
+
+ for (var i = 0; i < formArray.length; i++) {
+ // Retrieve the key definition from the data schema
+ var name = formArray[i].name;
+ var eltSchema = getSchemaKey(formSchema.properties, name);
+ var arrayMatch = null;
+ var cval = null;
+
+ // Skip the input field if it's not part of the schema
+ if (!eltSchema) continue;
+
+ // Handle multiple checkboxes separately as the idea is to generate
+ // an array that contains the list of enumeration items that the user
+ // selected.
+ if (eltSchema._jsonform_checkboxes_as_array) {
+ arrayMatch = name.match(/\[([0-9]*)\]$/);
+ if (arrayMatch) {
+ name = name.replace(/\[([0-9]*)\]$/, '');
+ cval = jsonform.util.getObjKey(values, name) || [];
+ if (formArray[i].value === '1') {
+ // Value selected, push the corresponding enumeration item
+ // to the data result
+ cval.push(eltSchema['enum'][parseInt(arrayMatch[1],10)]);
+ }
+ jsonform.util.setObjKey(values, name, cval);
+ continue;
+ }
+ }
+
+ // Type casting
+ if (eltSchema.type === 'boolean') {
+ if (formArray[i].value === '0') {
+ formArray[i].value = false;
+ } else {
+ formArray[i].value = !!formArray[i].value;
+ }
+ }
+ if ((eltSchema.type === 'number') ||
+ (eltSchema.type === 'integer')) {
+ if (_.isString(formArray[i].value)) {
+ if (!formArray[i].value.length) {
+ formArray[i].value = null;
+ } else if (!isNaN(Number(formArray[i].value))) {
+ formArray[i].value = Number(formArray[i].value);
+ }
+ }
+ }
+ if ((eltSchema.type === 'string') &&
+ (formArray[i].value === '') &&
+ !eltSchema._jsonform_allowEmpty) {
+ formArray[i].value=null;
+ }
+ if ((eltSchema.type === 'object') &&
+ _.isString(formArray[i].value) &&
+ (formArray[i].value.substring(0,1) === '{')) {
+ try {
+ formArray[i].value = JSON.parse(formArray[i].value);
+ } catch (e) {
+ formArray[i].value = {};
+ }
+ }
+ //TODO is this due to a serialization bug?
+ if ((eltSchema.type === 'object') &&
+ (formArray[i].value === 'null' || formArray[i].value === '')) {
+ formArray[i].value = null;
+ }
+
+ if (formArray[i].name && (formArray[i].value !== null)) {
+ jsonform.util.setObjKey(values, formArray[i].name, formArray[i].value);
+ }
+ }
+ // console.log("Form value",values);
+ return values;
+};
+
+
+
+/**
+ * Renders the node.
+ *
+ * Rendering is done in three steps: HTML generation, DOM element creation
+ * and insertion, and an enhance step to bind event handlers.
+ *
+ * @function
+ * @param {Node} el The DOM element where the node is to be rendered. The
+ * node is inserted at the right position based on its "childPos" property.
+ */
+formNode.prototype.render = function (el) {
+ var html = this.generate();
+ this.setContent(html, el);
+ this.enhance();
+};
+
+
+/**
+ * Inserts/Updates the HTML content of the node in the DOM.
+ *
+ * If the HTML is an update, the new HTML content replaces the old one.
+ * The new HTML content is not moved around in the DOM in particular.
+ *
+ * The HTML is inserted at the right position in its parent's DOM subtree
+ * otherwise (well, provided there are enough children, but that should always
+ * be the case).
+ *
+ * @function
+ * @param {string} html The HTML content to render
+ * @param {Node} parentEl The DOM element that is to contain the DOM node.
+ * This parameter is optional (the node's parent is used otherwise) and
+ * is ignored if the node to render is already in the DOM tree.
+ */
+formNode.prototype.setContent = function (html, parentEl) {
+ var node = $(html);
+ var parentNode = parentEl ||
+ (this.parentNode ? this.parentNode.el : this.ownerTree.domRoot);
+ var nextSibling = null;
+
+ if (this.el) {
+ // Replace the contents of the DOM element if the node is already in the tree
+ $(this.el).replaceWith(node);
+ }
+ else {
+ // Insert the node in the DOM if it's not already there
+ nextSibling = $(parentNode).children().get(this.childPos);
+ if (nextSibling) {
+ $(nextSibling).before(node);
+ }
+ else {
+ $(parentNode).append(node);
+ }
+ }
+
+ // Save the link between the form node and the generated HTML
+ this.el = node;
+
+ // Update the node's subtree, extracting DOM elements that match the nodes
+ // from the generated HTML
+ this.updateElement(this.el);
+};
+
+
+/**
+ * Updates the DOM element associated with the node.
+ *
+ * Only nodes that have ID are directly associated with a DOM element.
+ *
+ * @function
+ */
+formNode.prototype.updateElement = function (domNode) {
+ if (this.id) {
+ this.el = $('#' + escapeSelector(this.id), domNode).get(0);
+ if (this.view && this.view.getElement) {
+ this.el = this.view.getElement(this.el);
+ }
+ if ((this.fieldtemplate !== false) &&
+ this.view && this.view.fieldtemplate) {
+ // The field template wraps the element two or three level deep
+ // in the DOM tree, depending on whether there is anything prepended
+ // or appended to the input field
+ this.el = $(this.el).parent().parent();
+ if (this.prepend || this.prepend) {
+ this.el = this.el.parent();
+ }
+ this.el = this.el.get(0);
+ }
+ if (this.parentNode && this.parentNode.view &&
+ this.parentNode.view.childTemplate) {
+ // TODO: the child template may introduce more than one level,
+ // so the number of levels introduced should rather be exposed
+ // somehow in jsonform.fieldtemplate.
+ this.el = $(this.el).parent().get(0);
+ }
+ }
+
+ _.each(this.children, function (child) {
+ child.updateElement(this.el || domNode);
+ });
+};
+
+
+/**
+ * Generates the view's HTML content for the underlying model.
+ *
+ * @function
+ */
+formNode.prototype.generate = function () {
+ var data = {
+ id: this.id,
+ keydash: this.keydash,
+ elt: this.formElement,
+ schema: this.schemaElement,
+ node: this,
+ value: isSet(this.value) ? this.value : '',
+ escape: escapeHTML
+ };
+ var template = null;
+ var html = '';
+
+ // Complete the data context if needed
+ if (this.ownerTree.formDesc.onBeforeRender) {
+ this.ownerTree.formDesc.onBeforeRender(data, this);
+ }
+ if (this.view.onBeforeRender) {
+ this.view.onBeforeRender(data, this);
+ }
+
+ // Use the template that 'onBeforeRender' may have set,
+ // falling back to that of the form element otherwise
+ if (this.template) {
+ template = this.template;
+ }
+ else if (this.formElement && this.formElement.template) {
+ template = this.formElement.template;
+ }
+ else {
+ template = this.view.template;
+ }
+
+ // Wrap the view template in the generic field template
+ // (note the strict equality to 'false', needed as we fallback
+ // to the view's setting otherwise)
+ if ((this.fieldtemplate !== false) &&
+ (this.fieldtemplate || this.view.fieldtemplate)) {
+ template = jsonform.fieldTemplate(template);
+ }
+
+ // Wrap the content in the child template of its parent if necessary.
+ if (this.parentNode && this.parentNode.view &&
+ this.parentNode.view.childTemplate) {
+ template = this.parentNode.view.childTemplate(template);
+ }
+
+ // Prepare the HTML of the children
+ var childrenhtml = '';
+ _.each(this.children, function (child) {
+ childrenhtml += child.generate();
+ });
+ data.children = childrenhtml;
+
+ data.fieldHtmlClass = '';
+ if (this.ownerTree &&
+ this.ownerTree.formDesc &&
+ this.ownerTree.formDesc.params &&
+ this.ownerTree.formDesc.params.fieldHtmlClass) {
+ data.fieldHtmlClass = this.ownerTree.formDesc.params.fieldHtmlClass;
+ }
+ if (this.formElement &&
+ (typeof this.formElement.fieldHtmlClass !== 'undefined')) {
+ data.fieldHtmlClass = this.formElement.fieldHtmlClass;
+ }
+
+ // Apply the HTML template
+ html = _.template(template, data, fieldTemplateSettings);
+ return html;
+};
+
+
+/**
+ * Enhances the view with additional logic, binding event handlers
+ * in particular.
+ *
+ * The function also runs the "insert" event handler of the view and
+ * form element if they exist (starting with that of the view)
+ *
+ * @function
+ */
+formNode.prototype.enhance = function () {
+ var node = this;
+ var handlers = null;
+ var handler = null;
+ var formData = _.clone(this.ownerTree.formDesc.tpldata) || {};
+
+ if (this.formElement) {
+ // Check the view associated with the node as it may define an "onInsert"
+ // event handler to be run right away
+ if (this.view.onInsert) {
+ this.view.onInsert({ target: $(this.el) }, this);
+ }
+
+ handlers = this.handlers || this.formElement.handlers;
+
+ // Trigger the "insert" event handler
+ handler = this.onInsert || this.formElement.onInsert;
+ if (handler) {
+ handler({ target: $(this.el) }, this);
+ }
+ if (handlers) {
+ _.each(handlers, function (handler, onevent) {
+ if (onevent === 'insert') {
+ handler({ target: $(this.el) }, this);
+ }
+ }, this);
+ }
+
+ // No way to register event handlers if the DOM element is unknown
+ // TODO: find some way to register event handlers even when this.el is not set.
+ if (this.el) {
+
+ // Register specific event handlers
+ // TODO: Add support for other event handlers
+ if (this.onChange)
+ $(this.el).bind('change', function(evt) { node.onChange(evt, node); });
+ if (this.view.onChange)
+ $(this.el).bind('change', function(evt) { node.view.onChange(evt, node); });
+ if (this.formElement.onChange)
+ $(this.el).bind('change', function(evt) { node.formElement.onChange(evt, node); });
+
+ if (this.onClick)
+ $(this.el).bind('click', function(evt) { node.onClick(evt, node); });
+ if (this.view.onClick)
+ $(this.el).bind('click', function(evt) { node.view.onClick(evt, node); });
+ if (this.formElement.onClick)
+ $(this.el).bind('click', function(evt) { node.formElement.onClick(evt, node); });
+
+ if (this.onKeyUp)
+ $(this.el).bind('keyup', function(evt) { node.onKeyUp(evt, node); });
+ if (this.view.onKeyUp)
+ $(this.el).bind('keyup', function(evt) { node.view.onKeyUp(evt, node); });
+ if (this.formElement.onKeyUp)
+ $(this.el).bind('keyup', function(evt) { node.formElement.onKeyUp(evt, node); });
+
+ if (handlers) {
+ _.each(handlers, function (handler, onevent) {
+ if (onevent !== 'insert') {
+ $(this.el).bind(onevent, function(evt) { handler(evt, node); });
+ }
+ }, this);
+ }
+ }
+
+ // Auto-update legend based on the input field that's associated with it
+ if (this.legendChild && this.legendChild.formElement) {
+ $(this.legendChild.el).bind('keyup', function (evt) {
+ if (node.formElement && node.formElement.legend && node.parentNode) {
+ node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);
+ formData.idx = (node.arrayPath.length > 0) ?
+ node.arrayPath[node.arrayPath.length-1] + 1 :
+ node.childPos + 1;
+ formData.value = $(evt.target).val();
+ node.legend = _.template(node.legend, formData, valueTemplateSettings);
+ $(node.parentNode.el).trigger('legendUpdated');
+ }
+ });
+ }
+ }
+
+ // Recurse down the tree to enhance children
+ _.each(this.children, function (child) {
+ child.enhance();
+ });
+};
+
+
+
+/**
+ * Inserts an item in the array at the requested position and renders the item.
+ *
+ * @function
+ * @param {Number} idx Insertion index
+ */
+formNode.prototype.insertArrayItem = function (idx, domElement) {
+ var i = 0;
+
+ // Insert element at the end of the array if index is not given
+ if (idx === undefined) {
+ idx = this.children.length;
+ }
+
+ // Create the additional array item at the end of the list,
+ // using the item template created when tree was initialized
+ // (the call to resetValues ensures that 'arrayPath' is correctly set)
+ var child = this.childTemplate.clone();
+ this.appendChild(child);
+ child.resetValues();
+
+ // To create a blank array item at the requested position,
+ // shift values down starting at the requested position
+ // one to insert (note we start with the end of the array on purpose)
+ for (i = this.children.length-2; i >= idx; i--) {
+ this.children[i].moveValuesTo(this.children[i+1]);
+ }
+
+ // Initialize the blank node we've created with default values
+ this.children[idx].resetValues();
+ this.children[idx].computeInitialValues();
+
+ // Re-render all children that have changed
+ for (i = idx; i < this.children.length; i++) {
+ this.children[i].render(domElement);
+ }
+};
+
+
+/**
+ * Remove an item from an array
+ *
+ * @function
+ * @param {Number} idx The index number of the item to remove
+ */
+formNode.prototype.deleteArrayItem = function (idx) {
+ var i = 0;
+ var child = null;
+
+ // Delete last item if no index is given
+ if (idx === undefined) {
+ idx = this.children.length - 1;
+ }
+
+ // Move values up in the array
+ for (i = idx; i < this.children.length-1; i++) {
+ this.children[i+1].moveValuesTo(this.children[i]);
+ this.children[i].render();
+ }
+
+ // Remove the last array item from the DOM tree and from the form tree
+ this.removeChild();
+};
+
+/**
+ * Returns the minimum/maximum number of items that an array field
+ * is allowed to have according to the schema definition of the fields
+ * it contains.
+ *
+ * The function parses the schema definitions of the array items that
+ * compose the current "array" node and returns the minimum value of
+ * "maxItems" it encounters as the maximum number of items, and the
+ * maximum value of "minItems" as the minimum number of items.
+ *
+ * The function reports a -1 for either of the boundaries if the schema
+ * does not put any constraint on the number of elements the current
+ * array may have of if the current node is not an array.
+ *
+ * Note that array boundaries should be defined in the JSON Schema using
+ * "minItems" and "maxItems". The code also supports "minLength" and
+ * "maxLength" as a fallback, mostly because it used to by mistake (see #22)
+ * and because other people could make the same mistake.
+ *
+ * @function
+ * @return {Object} An object with properties "minItems" and "maxItems"
+ * that reports the corresponding number of items that the array may
+ * have (value is -1 when there is no constraint for that boundary)
+ */
+formNode.prototype.getArrayBoundaries = function () {
+ var boundaries = {
+ minItems: -1,
+ maxItems: -1
+ };
+ if (!this.view || !this.view.array) return boundaries;
+
+ var getNodeBoundaries = function (node, initialNode) {
+ var schemaKey = null;
+ var arrayKey = null;
+ var boundaries = {
+ minItems: -1,
+ maxItems: -1
+ };
+ initialNode = initialNode || node;
+
+ if (node.view && node.view.array && (node !== initialNode)) {
+ // New array level not linked to an array in the schema,
+ // so no size constraints
+ return boundaries;
+ }
+
+ if (node.key) {
+ // Note the conversion to target the actual array definition in the
+ // schema where minItems/maxItems may be defined. If we're still looking
+ // at the initial node, the goal is to convert from:
+ // foo[0].bar[3].baz to foo[].bar[].baz
+ // If we're not looking at the initial node, the goal is to look at the
+ // closest array parent:
+ // foo[0].bar[3].baz to foo[].bar
+ arrayKey = node.key.replace(/\[[0-9]+\]/g, '[]');
+ if (node !== initialNode) {
+ arrayKey = arrayKey.replace(/\[\][^\[\]]*$/, '');
+ }
+ schemaKey = getSchemaKey(
+ node.ownerTree.formDesc.schema.properties,
+ arrayKey
+ );
+ if (!schemaKey) return boundaries;
+ return {
+ minItems: schemaKey.minItems || schemaKey.minLength || -1,
+ maxItems: schemaKey.maxItems || schemaKey.maxLength || -1
+ };
+ }
+ else {
+ _.each(node.children, function (child) {
+ var subBoundaries = getNodeBoundaries(child, initialNode);
+ if (subBoundaries.minItems !== -1) {
+ if (boundaries.minItems !== -1) {
+ boundaries.minItems = Math.max(
+ boundaries.minItems,
+ subBoundaries.minItems
+ );
+ }
+ else {
+ boundaries.minItems = subBoundaries.minItems;
+ }
+ }
+ if (subBoundaries.maxItems !== -1) {
+ if (boundaries.maxItems !== -1) {
+ boundaries.maxItems = Math.min(
+ boundaries.maxItems,
+ subBoundaries.maxItems
+ );
+ }
+ else {
+ boundaries.maxItems = subBoundaries.maxItems;
+ }
+ }
+ });
+ }
+ return boundaries;
+ };
+ return getNodeBoundaries(this);
+};
+
+
+/**
+ * Form tree class.
+ *
+ * Holds the internal representation of the form.
+ * The tree is always in sync with the rendered form, this allows to parse
+ * it easily.
+ *
+ * @class
+ */
+var formTree = function () {
+ this.eventhandlers = [];
+ this.root = null;
+ this.formDesc = null;
+};
+
+/**
+ * Initializes the form tree structure from the JSONForm object
+ *
+ * This function is the main entry point of the JSONForm library.
+ *
+ * Initialization steps:
+ * 1. the internal tree structure that matches the JSONForm object
+ * gets created (call to buildTree)
+ * 2. initial values are computed from previously submitted values
+ * or from the default values defined in the JSON schema.
+ *
+ * When the function returns, the tree is ready to be rendered through
+ * a call to "render".
+ *
+ * @function
+ */
+formTree.prototype.initialize = function (formDesc) {
+ formDesc = formDesc || {};
+
+ // Keep a pointer to the initial JSONForm
+ // (note clone returns a shallow copy, only first-level is cloned)
+ this.formDesc = _.clone(formDesc);
+
+ // Compute form prefix if no prefix is given.
+ this.formDesc.prefix = this.formDesc.prefix ||
+ 'jsonform-' + _.uniqueId();
+
+ // JSON schema shorthand
+ if (this.formDesc.schema && !this.formDesc.schema.properties) {
+ this.formDesc.schema = {
+ properties: this.formDesc.schema
+ };
+ }
+
+ // Ensure layout is set
+ this.formDesc.form = this.formDesc.form || [
+ '*',
+ {
+ type: 'actions',
+ items: [
+ {
+ type: 'submit',
+ value: 'Submit'
+ }
+ ]
+ }
+ ];
+ this.formDesc.form = (_.isArray(this.formDesc.form) ?
+ this.formDesc.form :
+ [this.formDesc.form]);
+
+ this.formDesc.params = this.formDesc.params || {};
+
+ // Create the root of the tree
+ this.root = new formNode();
+ this.root.ownerTree = this;
+ this.root.view = jsonform.elementTypes['root'];
+
+ // Generate the tree from the form description
+ this.buildTree();
+
+ // Compute the values associated with each node
+ // (for arrays, the computation actually creates the form nodes)
+ this.computeInitialValues();
+};
+
+
+/**
+ * Constructs the tree from the form description.
+ *
+ * The function must be called once when the tree is first created.
+ *
+ * @function
+ */
+formTree.prototype.buildTree = function () {
+ // Parse and generate the form structure based on the elements encountered:
+ // - '*' means "generate all possible fields using default layout"
+ // - a key reference to target a specific data element
+ // - a more complex object to generate specific form sections
+ _.each(this.formDesc.form, function (formElement) {
+ if (formElement === '*') {
+ _.each(this.formDesc.schema.properties, function (element, key) {
+ this.root.appendChild(this.buildFromLayout({
+ key: key
+ }));
+ }, this);
+ }
+ else {
+ if (_.isString(formElement)) {
+ formElement = {
+ key: formElement
+ };
+ }
+ this.root.appendChild(this.buildFromLayout(formElement));
+ }
+ }, this);
+};
+
+
+/**
+ * Builds the internal form tree representation from the requested layout.
+ *
+ * The function is recursive, generating the node children as necessary.
+ * The function extracts the values from the previously submitted values
+ * (this.formDesc.value) or from default values defined in the schema.
+ *
+ * @function
+ * @param {Object} formElement JSONForm element to render
+ * @param {Object} context The parsing context (the array depth in particular)
+ * @return {Object} The node that matches the element.
+ */
+formTree.prototype.buildFromLayout = function (formElement, context) {
+ var schemaElement = null;
+ var node = new formNode();
+ var view = null;
+ var key = null;
+
+ // The form element parameter directly comes from the initial
+ // JSONForm object. We'll make a shallow copy of it and of its children
+ // not to pollute the original object.
+ // (note JSON.parse(JSON.stringify()) cannot be used since there may be
+ // event handlers in there!)
+ formElement = _.clone(formElement);
+ if (formElement.items) {
+ if (_.isArray(formElement.items)) {
+ formElement.items = _.map(formElement.items, _.clone);
+ }
+ else {
+ formElement.items = [ _.clone(formElement.items) ];
+ }
+ }
+
+ if (formElement.key) {
+ // The form element is directly linked to an element in the JSON
+ // schema. The properties of the form element override those of the
+ // element in the JSON schema. Properties from the JSON schema complete
+ // those of the form element otherwise.
+
+ // Retrieve the element from the JSON schema
+ schemaElement = getSchemaKey(
+ this.formDesc.schema.properties,
+ formElement.key);
+ if (!schemaElement) {
+ // The JSON Form is invalid!
+ throw new Error('The JSONForm object references the schema key "' +
+ formElement.key + '" but that key does not exist in the JSON schema');
+ }
+
+ // Schema element has just been found, let's trigger the
+ // "onElementSchema" event
+ // (tidoust: not sure what the use case for this is, keeping the
+ // code for backward compatibility)
+ if (this.formDesc.onElementSchema) {
+ this.formDesc.onElementSchema(formElement, schemaElement);
+ }
+
+ formElement.name =
+ formElement.name ||
+ formElement.key;
+ formElement.title =
+ formElement.title ||
+ schemaElement.title;
+ formElement.description =
+ formElement.description ||
+ schemaElement.description;
+ formElement.readOnly =
+ formElement.readOnly ||
+ schemaElement.readOnly ||
+ formElement.readonly ||
+ schemaElement.readonly;
+
+ // Compute the ID of the input field
+ if (!formElement.id) {
+ formElement.id = escapeSelector(this.formDesc.prefix) +
+ '-elt-' + formElement.key;
+ }
+
+ // Should empty strings be included in the final value?
+ // TODO: it's rather unclean to pass it through the schema.
+ if (formElement.allowEmpty) {
+ schemaElement._jsonform_allowEmpty = true;
+ }
+
+ // If the form element does not define its type, use the type of
+ // the schema element.
+ if (!formElement.type) {
+ if ((schemaElement.type === 'string') &&
+ (schemaElement.format === 'color')) {
+ formElement.type = 'color';
+ } else if ((schemaElement.type === 'number' ||
+ schemaElement.type === 'integer' ||
+ schemaElement.type === 'string' ||
+ schemaElement.type === 'any') &&
+ !schemaElement['enum']) {
+ formElement.type = 'text';
+ } else if (schemaElement.type === 'boolean') {
+ formElement.type = 'checkbox';
+ } else if (schemaElement.type === 'object') {
+ if (schemaElement.properties) {
+ formElement.type = 'fieldset';
+ } else {
+ formElement.type = 'textarea';
+ }
+ } else if (!_.isUndefined(schemaElement['enum'])) {
+ formElement.type = 'select';
+ } else {
+ formElement.type = schemaElement.type;
+ }
+ }
+
+ // Unless overridden in the definition of the form element (or unless
+ // there's a titleMap defined), use the enumeration list defined in
+ // the schema
+ if (!formElement.options && schemaElement['enum']) {
+ if (formElement.titleMap) {
+ formElement.options = _.map(schemaElement['enum'], function (value) {
+ return {
+ value: value,
+ title: formElement.titleMap[value] || value
+ };
+ });
+ }
+ else {
+ formElement.options = schemaElement['enum'];
+ }
+ }
+
+ // Flag a list of checkboxes with multiple choices
+ if ((formElement.type === 'checkboxes') && schemaElement.items) {
+ var itemsEnum = schemaElement.items['enum'];
+ if (itemsEnum) {
+ schemaElement.items._jsonform_checkboxes_as_array = true;
+ }
+ if (!itemsEnum && schemaElement.items[0]) {
+ itemsEnum = schemaElement.items[0]['enum'];
+ if (itemsEnum) {
+ schemaElement.items[0]._jsonform_checkboxes_as_array = true;
+ }
+ }
+ }
+
+ // If the form element targets an "object" in the JSON schema,
+ // we need to recurse through the list of children to create an
+ // input field per child property of the object in the JSON schema
+ if (schemaElement.type === 'object') {
+ _.each(schemaElement.properties, function (prop, propName) {
+ node.appendChild(this.buildFromLayout({
+ key: formElement.key + '.' + propName
+ }));
+ }, this);
+ }
+ }
+
+ if (!formElement.type) {
+ formElement.type = 'none';
+ }
+ view = jsonform.elementTypes[formElement.type];
+ if (!view) {
+ throw new Error('The JSONForm contains an element whose type is unknown: "' +
+ formElement.type + '"');
+ }
+
+
+ if (schemaElement) {
+ // The form element is linked to an element in the schema.
+ // Let's make sure the types are compatible.
+ // In particular, the element must not be a "container"
+ // (or must be an "object" or "array" container)
+ if (!view.inputfield && !view.array &&
+ (formElement.type !== 'selectfieldset') &&
+ (schemaElement.type !== 'object')) {
+ throw new Error('The JSONForm contains an element that links to an ' +
+ 'element in the JSON schema (key: "' + formElement.key + '") ' +
+ 'and that should not based on its type ("' + formElement.type + '")');
+ }
+ }
+ else {
+ // The form element is not linked to an element in the schema.
+ // This means the form element must be a "container" element,
+ // and must not define an input field.
+ if (view.inputfield && (formElement.type !== 'selectfieldset')) {
+ throw new Error('The JSONForm defines an element of type ' +
+ '"' + formElement.type + '" ' +
+ 'but no "key" property to link the input field to the JSON schema');
+ }
+ }
+
+ // A few characters need to be escaped to use the ID as jQuery selector
+ formElement.iddot = escapeSelector(formElement.id || '');
+
+ // Initialize the form node from the form element and schema element
+ node.formElement = formElement;
+ node.schemaElement = schemaElement;
+ node.view = view;
+ node.ownerTree = this;
+
+ // Set event handlers
+ if (!formElement.handlers) {
+ formElement.handlers = {};
+ }
+
+ // Parse children recursively
+ if (node.view.array) {
+ // The form element is an array. The number of items in an array
+ // is by definition dynamic, up to the form user (through "Add more",
+ // "Delete" commands). The positions of the items in the array may
+ // also change over time (through "Move up", "Move down" commands).
+ //
+ // The form node stores a "template" node that serves as basis for
+ // the creation of an item in the array.
+ //
+ // Array items may be complex forms themselves, allowing for nesting.
+ //
+ // The initial values set the initial number of items in the array.
+ // Note a form element contains at least one item when it is rendered.
+ if (formElement.items) {
+ key = formElement.items[0] || formElement.items;
+ }
+ else {
+ key = formElement.key + '[]';
+ }
+ if (_.isString(key)) {
+ key = { key: key };
+ }
+ node.setChildTemplate(this.buildFromLayout(key));
+ }
+ else if (formElement.items) {
+ // The form element defines children elements
+ _.each(formElement.items, function (item) {
+ if (_.isString(item)) {
+ item = { key: item };
+ }
+ node.appendChild(this.buildFromLayout(item));
+ }, this);
+ }
+
+ return node;
+};
+
+
+/**
+ * Computes the values associated with each input field in the tree based
+ * on previously submitted values or default values in the JSON schema.
+ *
+ * For arrays, the function actually creates and inserts additional
+ * nodes in the tree based on previously submitted values (also ensuring
+ * that the array has at least one item).
+ *
+ * The function sets the array path on all nodes.
+ * It should be called once in the lifetime of a form tree right after
+ * the tree structure has been created.
+ *
+ * @function
+ */
+formTree.prototype.computeInitialValues = function () {
+ this.root.computeInitialValues(this.formDesc.value);
+};
+
+
+/**
+ * Renders the form tree
+ *
+ * @function
+ * @param {Node} domRoot The "form" element in the DOM tree that serves as
+ * root for the form
+ */
+formTree.prototype.render = function (domRoot) {
+ if (!domRoot) return;
+ this.domRoot = domRoot;
+ this.root.render();
+
+ // If the schema defines required fields, flag the form with the
+ // "jsonform-hasrequired" class for styling purpose
+ // (typically so that users may display a legend)
+ if (this.hasRequiredField()) {
+ $(domRoot).addClass('jsonform-hasrequired');
+ }
+};
+
+/**
+ * Walks down the element tree with a callback
+ *
+ * @function
+ * @param {Function} callback The callback to call on each element
+ */
+formTree.prototype.forEachElement = function (callback) {
+
+ var f = function(root) {
+ for (var i=0;i tag in the DOM
+ * @return {Object} The object that follows the data schema and matches the
+ * values entered by the user.
+ */
+jsonform.getFormValue = function (formelt) {
+ var form = $(formelt).data('jsonform-tree');
+ if (!form) return null;
+ return form.root.getFormValues();
+};
+
+
+/**
+ * Highlights errors reported by the JSON schema validator in the document.
+ *
+ * @function
+ * @param {Object} errors List of errors reported by the JSON schema validator
+ * @param {Object} options The JSON Form object that describes the form
+ * (unused for the time being, could be useful to store example values or
+ * specific error messages)
+ */
+$.fn.jsonFormErrors = function(errors, options) {
+ $(".error", this).removeClass("error");
+ $(".warning", this).removeClass("warning");
+
+ $(".jsonform-errortext", this).hide();
+ if (!errors) return;
+
+ var errorSelectors = [];
+ for (var i = 0; i < errors.length; i++) {
+ // Compute the address of the input field in the form from the URI
+ // returned by the JSON schema validator.
+ // These URIs typically look like:
+ // urn:uuid:cccc265e-ffdd-4e40-8c97-977f7a512853#/pictures/1/thumbnail
+ // What we need from that is the path in the value object:
+ // pictures[1].thumbnail
+ // ... and the jQuery-friendly class selector of the input field:
+ // .jsonform-error-pictures\[1\]---thumbnail
+ var key = errors[i].uri
+ .replace(/.*#\//, '')
+ .replace(/\//g, '.')
+ .replace(/\.([0-9]+)(?=\.|$)/g, '[$1]');
+ var errormarkerclass = ".jsonform-error-" +
+ escapeSelector(key.replace(/\./g,"---"));
+ errorSelectors.push(errormarkerclass);
+
+ var errorType = errors[i].type || "error";
+ $(errormarkerclass, this).addClass(errorType);
+ $(errormarkerclass + " .jsonform-errortext", this).html(errors[i].message).show();
+ }
+
+ // Look for the first error in the DOM and ensure the element
+ // is visible so that the user understands that something went wrong
+ errorSelectors = errorSelectors.join(',');
+ var firstError = $(errorSelectors).get(0);
+ if (firstError && firstError.scrollIntoView) {
+ firstError.scrollIntoView(true, {
+ behavior: 'smooth'
+ });
+ }
+};
+
+
+/**
+ * Generates the HTML form from the given JSON Form object and renders the form.
+ *
+ * Main entry point of the library. Defined as a jQuery function that typically
+ * needs to be applied to a