Browse Source

fix for loop add jsonform

Tobias Simetsreiter 4 years ago
parent
commit
d3e112633b
7 changed files with 18037 additions and 12813 deletions
  1. 33 33
      bundle/js_yaml.js
  2. 498 5
      bundle/json2csv.js
  3. 5766 0
      bundle/jsonform.js
  4. 11212 12737
      bundle/mssql.js
  5. 497 38
      package-lock.json
  6. 30 0
      package.json
  7. 1 0
      src/jsonform.js

+ 33 - 33
bundle/js_yaml.js

@@ -1,7 +1,4 @@
 (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
-window.js_yaml = require('js-yaml');
-
-},{"js-yaml":2}],2:[function(require,module,exports){
 'use strict';
 
 
@@ -10,7 +7,7 @@ var yaml = require('./lib/js-yaml.js');
 
 module.exports = yaml;
 
-},{"./lib/js-yaml.js":3}],3:[function(require,module,exports){
+},{"./lib/js-yaml.js":2}],2:[function(require,module,exports){
 'use strict';
 
 
@@ -51,7 +48,7 @@ module.exports.parse          = deprecated('parse');
 module.exports.compose        = deprecated('compose');
 module.exports.addConstructor = deprecated('addConstructor');
 
-},{"./js-yaml/dumper":5,"./js-yaml/exception":6,"./js-yaml/loader":7,"./js-yaml/schema":9,"./js-yaml/schema/core":10,"./js-yaml/schema/default_full":11,"./js-yaml/schema/default_safe":12,"./js-yaml/schema/failsafe":13,"./js-yaml/schema/json":14,"./js-yaml/type":15}],4:[function(require,module,exports){
+},{"./js-yaml/dumper":4,"./js-yaml/exception":5,"./js-yaml/loader":6,"./js-yaml/schema":8,"./js-yaml/schema/core":9,"./js-yaml/schema/default_full":10,"./js-yaml/schema/default_safe":11,"./js-yaml/schema/failsafe":12,"./js-yaml/schema/json":13,"./js-yaml/type":14}],3:[function(require,module,exports){
 'use strict';
 
 
@@ -112,7 +109,7 @@ module.exports.repeat         = repeat;
 module.exports.isNegativeZero = isNegativeZero;
 module.exports.extend         = extend;
 
-},{}],5:[function(require,module,exports){
+},{}],4:[function(require,module,exports){
 'use strict';
 
 /*eslint-disable no-use-before-define*/
@@ -964,7 +961,7 @@ function safeDump(input, options) {
 module.exports.dump     = dump;
 module.exports.safeDump = safeDump;
 
-},{"./common":4,"./exception":6,"./schema/default_full":11,"./schema/default_safe":12}],6:[function(require,module,exports){
+},{"./common":3,"./exception":5,"./schema/default_full":10,"./schema/default_safe":11}],5:[function(require,module,exports){
 // YAML error class. http://stackoverflow.com/questions/8458984
 //
 'use strict';
@@ -1009,7 +1006,7 @@ YAMLException.prototype.toString = function toString(compact) {
 
 module.exports = YAMLException;
 
-},{}],7:[function(require,module,exports){
+},{}],6:[function(require,module,exports){
 'use strict';
 
 /*eslint-disable max-len,no-use-before-define*/
@@ -2655,7 +2652,7 @@ module.exports.load        = load;
 module.exports.safeLoadAll = safeLoadAll;
 module.exports.safeLoad    = safeLoad;
 
-},{"./common":4,"./exception":6,"./mark":8,"./schema/default_full":11,"./schema/default_safe":12}],8:[function(require,module,exports){
+},{"./common":3,"./exception":5,"./mark":7,"./schema/default_full":10,"./schema/default_safe":11}],7:[function(require,module,exports){
 'use strict';
 
 
@@ -2733,7 +2730,7 @@ Mark.prototype.toString = function toString(compact) {
 
 module.exports = Mark;
 
-},{"./common":4}],9:[function(require,module,exports){
+},{"./common":3}],8:[function(require,module,exports){
 'use strict';
 
 /*eslint-disable max-len*/
@@ -2843,7 +2840,7 @@ Schema.create = function createSchema() {
 
 module.exports = Schema;
 
-},{"./common":4,"./exception":6,"./type":15}],10:[function(require,module,exports){
+},{"./common":3,"./exception":5,"./type":14}],9:[function(require,module,exports){
 // Standard YAML's Core schema.
 // http://www.yaml.org/spec/1.2/spec.html#id2804923
 //
@@ -2863,7 +2860,7 @@ module.exports = new Schema({
   ]
 });
 
-},{"../schema":9,"./json":14}],11:[function(require,module,exports){
+},{"../schema":8,"./json":13}],10:[function(require,module,exports){
 // JS-YAML's default schema for `load` function.
 // It is not described in the YAML specification.
 //
@@ -2890,7 +2887,7 @@ module.exports = Schema.DEFAULT = new Schema({
   ]
 });
 
-},{"../schema":9,"../type/js/function":20,"../type/js/regexp":21,"../type/js/undefined":22,"./default_safe":12}],12:[function(require,module,exports){
+},{"../schema":8,"../type/js/function":19,"../type/js/regexp":20,"../type/js/undefined":21,"./default_safe":11}],11:[function(require,module,exports){
 // JS-YAML's default schema for `safeLoad` function.
 // It is not described in the YAML specification.
 //
@@ -2920,7 +2917,7 @@ module.exports = new Schema({
   ]
 });
 
-},{"../schema":9,"../type/binary":16,"../type/merge":24,"../type/omap":26,"../type/pairs":27,"../type/set":29,"../type/timestamp":31,"./core":10}],13:[function(require,module,exports){
+},{"../schema":8,"../type/binary":15,"../type/merge":23,"../type/omap":25,"../type/pairs":26,"../type/set":28,"../type/timestamp":30,"./core":9}],12:[function(require,module,exports){
 // Standard YAML's Failsafe schema.
 // http://www.yaml.org/spec/1.2/spec.html#id2802346
 
@@ -2939,7 +2936,7 @@ module.exports = new Schema({
   ]
 });
 
-},{"../schema":9,"../type/map":23,"../type/seq":28,"../type/str":30}],14:[function(require,module,exports){
+},{"../schema":8,"../type/map":22,"../type/seq":27,"../type/str":29}],13:[function(require,module,exports){
 // Standard YAML's JSON schema.
 // http://www.yaml.org/spec/1.2/spec.html#id2803231
 //
@@ -2966,7 +2963,7 @@ module.exports = new Schema({
   ]
 });
 
-},{"../schema":9,"../type/bool":17,"../type/float":18,"../type/int":19,"../type/null":25,"./failsafe":13}],15:[function(require,module,exports){
+},{"../schema":8,"../type/bool":16,"../type/float":17,"../type/int":18,"../type/null":24,"./failsafe":12}],14:[function(require,module,exports){
 'use strict';
 
 var YAMLException = require('./exception');
@@ -3029,7 +3026,7 @@ function Type(tag, options) {
 
 module.exports = Type;
 
-},{"./exception":6}],16:[function(require,module,exports){
+},{"./exception":5}],15:[function(require,module,exports){
 'use strict';
 
 /*eslint-disable no-bitwise*/
@@ -3169,7 +3166,7 @@ module.exports = new Type('tag:yaml.org,2002:binary', {
   represent: representYamlBinary
 });
 
-},{"../type":15}],17:[function(require,module,exports){
+},{"../type":14}],16:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3206,7 +3203,7 @@ module.exports = new Type('tag:yaml.org,2002:bool', {
   defaultStyle: 'lowercase'
 });
 
-},{"../type":15}],18:[function(require,module,exports){
+},{"../type":14}],17:[function(require,module,exports){
 'use strict';
 
 var common = require('../common');
@@ -3324,7 +3321,7 @@ module.exports = new Type('tag:yaml.org,2002:float', {
   defaultStyle: 'lowercase'
 });
 
-},{"../common":4,"../type":15}],19:[function(require,module,exports){
+},{"../common":3,"../type":14}],18:[function(require,module,exports){
 'use strict';
 
 var common = require('../common');
@@ -3499,7 +3496,7 @@ module.exports = new Type('tag:yaml.org,2002:int', {
   }
 });
 
-},{"../common":4,"../type":15}],20:[function(require,module,exports){
+},{"../common":3,"../type":14}],19:[function(require,module,exports){
 'use strict';
 
 var esprima;
@@ -3594,7 +3591,7 @@ module.exports = new Type('tag:yaml.org,2002:js/function', {
   represent: representJavascriptFunction
 });
 
-},{"../../type":15}],21:[function(require,module,exports){
+},{"../../type":14}],20:[function(require,module,exports){
 'use strict';
 
 var Type = require('../../type');
@@ -3656,7 +3653,7 @@ module.exports = new Type('tag:yaml.org,2002:js/regexp', {
   represent: representJavascriptRegExp
 });
 
-},{"../../type":15}],22:[function(require,module,exports){
+},{"../../type":14}],21:[function(require,module,exports){
 'use strict';
 
 var Type = require('../../type');
@@ -3686,7 +3683,7 @@ module.exports = new Type('tag:yaml.org,2002:js/undefined', {
   represent: representJavascriptUndefined
 });
 
-},{"../../type":15}],23:[function(require,module,exports){
+},{"../../type":14}],22:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3696,7 +3693,7 @@ module.exports = new Type('tag:yaml.org,2002:map', {
   construct: function (data) { return data !== null ? data : {}; }
 });
 
-},{"../type":15}],24:[function(require,module,exports){
+},{"../type":14}],23:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3710,7 +3707,7 @@ module.exports = new Type('tag:yaml.org,2002:merge', {
   resolve: resolveYamlMerge
 });
 
-},{"../type":15}],25:[function(require,module,exports){
+},{"../type":14}],24:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3746,7 +3743,7 @@ module.exports = new Type('tag:yaml.org,2002:null', {
   defaultStyle: 'lowercase'
 });
 
-},{"../type":15}],26:[function(require,module,exports){
+},{"../type":14}],25:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3792,7 +3789,7 @@ module.exports = new Type('tag:yaml.org,2002:omap', {
   construct: constructYamlOmap
 });
 
-},{"../type":15}],27:[function(require,module,exports){
+},{"../type":14}],26:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3847,7 +3844,7 @@ module.exports = new Type('tag:yaml.org,2002:pairs', {
   construct: constructYamlPairs
 });
 
-},{"../type":15}],28:[function(require,module,exports){
+},{"../type":14}],27:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3857,7 +3854,7 @@ module.exports = new Type('tag:yaml.org,2002:seq', {
   construct: function (data) { return data !== null ? data : []; }
 });
 
-},{"../type":15}],29:[function(require,module,exports){
+},{"../type":14}],28:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3888,7 +3885,7 @@ module.exports = new Type('tag:yaml.org,2002:set', {
   construct: constructYamlSet
 });
 
-},{"../type":15}],30:[function(require,module,exports){
+},{"../type":14}],29:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3898,7 +3895,7 @@ module.exports = new Type('tag:yaml.org,2002:str', {
   construct: function (data) { return data !== null ? data : ''; }
 });
 
-},{"../type":15}],31:[function(require,module,exports){
+},{"../type":14}],30:[function(require,module,exports){
 'use strict';
 
 var Type = require('../type');
@@ -3988,4 +3985,7 @@ module.exports = new Type('tag:yaml.org,2002:timestamp', {
   represent: representYamlTimestamp
 });
 
-},{"../type":15}]},{},[1]);
+},{"../type":14}],31:[function(require,module,exports){
+window.js_yaml = require('js-yaml');
+
+},{"js-yaml":1}]},{},[31]);

+ 498 - 5
bundle/json2csv.js

@@ -245,6 +245,477 @@ module.exports={
 },{}],5:[function(require,module,exports){
 'use strict';
 
+let {Json2Csv} = require('./json2csv'), // Require our json-2-csv code
+    {Csv2Json} = require('./csv2json'), // Require our csv-2-json code
+    utils = require('./utils');
+
+module.exports = {
+    json2csv: (data, callback, options) => convert(Json2Csv, data, callback, options),
+    csv2json: (data, callback, options) => convert(Csv2Json, data, callback, options),
+    json2csvAsync: (json, options) => convertAsync(Json2Csv, json, options),
+    csv2jsonAsync: (csv, options) => convertAsync(Csv2Json, csv, options),
+
+    /**
+     * @deprecated Since v3.0.0
+     */
+    json2csvPromisified: (json, options) => deprecatedAsync(Json2Csv, 'json2csvPromisified()', 'json2csvAsync()', json, options),
+
+    /**
+     * @deprecated Since v3.0.0
+     */
+    csv2jsonPromisified: (csv, options) => deprecatedAsync(Csv2Json, 'csv2jsonPromisified()', 'csv2jsonAsync()', csv, options)
+};
+
+/**
+ * Abstracted converter function for json2csv and csv2json functionality
+ * Takes in the converter to be used (either Json2Csv or Csv2Json)
+ * @param converter
+ * @param data
+ * @param callback
+ * @param options
+ */
+function convert(converter, data, callback, options) {
+    return utils.convert({
+        data: data,
+        callback,
+        options,
+        converter: converter
+    });
+}
+
+/**
+ * Simple way to promisify a callback version of json2csv or csv2json
+ * @param converter
+ * @param data
+ * @param options
+ * @returns {Promise<any>}
+ */
+function convertAsync(converter, data, options) {
+    return new Promise((resolve, reject) => convert(converter, data, (err, data) => {
+        if (err) {
+            return reject(err);
+        }
+        return resolve(data);
+    }, options));
+}
+
+/**
+ * Simple way to provide a deprecation warning for previous promisified versions
+ * of module functionality.
+ * @param converter
+ * @param deprecatedName
+ * @param replacementName
+ * @param data
+ * @param options
+ * @returns {Promise<any>}
+ */
+function deprecatedAsync(converter, deprecatedName, replacementName, data, options) {
+    console.warn('WARNING: ' + deprecatedName + ' is deprecated and will be removed soon. Please use ' + replacementName + ' instead.');
+    return convertAsync(converter, data, options);
+}
+
+},{"./csv2json":6,"./json2csv":7,"./utils":8}],6:[function(require,module,exports){
+'use strict';
+
+let path = require('doc-path'),
+    constants = require('./constants.json'),
+    utils = require('./utils');
+
+const Csv2Json = function(options) {
+    const escapedWrapDelimiterRegex = new RegExp(options.delimiter.wrap + options.delimiter.wrap, 'g'),
+        excelBOMRegex = new RegExp('^' + constants.values.excelBOM);
+
+    /**
+     * Trims the header key, if specified by the user via the provided options
+     * @param headerKey
+     * @returns {*}
+     */
+    function processHeaderKey(headerKey) {
+        headerKey = removeWrapDelimitersFromValue(headerKey);
+        if (options.trimHeaderFields) {
+            return headerKey.split('.')
+                .map((component) => component.trim())
+                .join('.');
+        }
+        return headerKey;
+    }
+
+    /**
+     * Generate the JSON heading from the CSV
+     * @param lines {String[]} csv lines split by EOL delimiter
+     * @returns {*}
+     */
+    function retrieveHeading(lines) {
+        let params = {lines},
+            // Generate and return the heading keys
+            headerRow = params.lines[0];
+        params.headerFields = headerRow.map((headerKey, index) => ({
+            value: processHeaderKey(headerKey),
+            index: index
+        }));
+
+        // If the user provided keys, filter the generated keys to just the user provided keys so we also have the key index
+        if (options.keys) {
+            params.headerFields = params.headerFields.filter((headerKey) => options.keys.includes(headerKey.value));
+        }
+
+        return params;
+    }
+
+    /**
+     * Splits the lines of the CSV string by the EOL delimiter and resolves and array of strings (lines)
+     * @param csv
+     * @returns {Promise.<String[]>}
+     */
+    function splitCsvLines(csv) {
+        return Promise.resolve(splitLines(csv));
+    }
+
+    /**
+     * Removes the Excel BOM value, if specified by the options object
+     * @param csv
+     * @returns {Promise.<String>}
+     */
+    function stripExcelBOM(csv) {
+        if (options.excelBOM) {
+            return Promise.resolve(csv.replace(excelBOMRegex, ''));
+        }
+        return Promise.resolve(csv);
+    }
+
+    /**
+     * Helper function that splits a line so that we can handle wrapped fields
+     * @param csv
+     */
+    function splitLines(csv) {
+        // Parse out the line...
+        let lines = [],
+            splitLine = [],
+            character,
+            charBefore,
+            charAfter,
+            nextNChar,
+            lastCharacterIndex = csv.length - 1,
+            eolDelimiterLength = options.delimiter.eol.length,
+            stateVariables = {
+                insideWrapDelimiter: false,
+                parsingValue: true,
+                justParsedDoubleQuote: false,
+                startIndex: 0
+            },
+            index = 0;
+
+        // Loop through each character in the line to identify where to split the values
+        while (index < csv.length) {
+            // Current character
+            character = csv[index];
+            // Previous character
+            charBefore = index ? csv[index - 1] : '';
+            // Next character
+            charAfter = index < lastCharacterIndex ? csv[index + 1] : '';
+            // Next n characters, including the current character, where n = length(EOL delimiter)
+            // This allows for the checking of an EOL delimiter when if it is more than a single character (eg. '\r\n')
+            nextNChar = utils.getNCharacters(csv, index, eolDelimiterLength);
+
+            if ((nextNChar === options.delimiter.eol && !stateVariables.insideWrapDelimiter ||
+                index === lastCharacterIndex) && charBefore === options.delimiter.field) {
+                // If we reached an EOL delimiter or the end of the csv and the previous character is a field delimiter...
+
+                // If the start index is the current index (and since the previous character is a comma),
+                //   then the value being parsed is an empty value accordingly, add an empty string
+                if (nextNChar === options.delimiter.eol && stateVariables.startIndex === index) {
+                    splitLine.push('');
+                } else if (character === options.delimiter.field) {
+                    // If we reached the end of the CSV, there's no new line, and the current character is a comma
+                    // then add an empty string for the current value
+                    splitLine.push('');
+                } else {
+                    // Otherwise, there's a valid value, and the start index isn't the current index, grab the whole value
+                    splitLine.push(csv.substr(stateVariables.startIndex));
+                }
+
+                // Since the last character is a comma, there's still an additional implied field value trailing the comma.
+                //   Since this value is empty, we push an extra empty value
+                splitLine.push('');
+
+                // Finally, push the split line values into the lines array and clear the split line
+                lines.push(splitLine);
+                splitLine = [];
+                stateVariables.startIndex = index + eolDelimiterLength;
+                stateVariables.parsingValue = true;
+                stateVariables.insideWrapDelimiter = charAfter === options.delimiter.wrap;
+            } else if (index === lastCharacterIndex && character === options.delimiter.field) {
+                // If we reach the end of the CSV and the current character is a field delimiter
+
+                // Parse the previously seen value and add it to the line
+                let parsedValue = csv.substring(stateVariables.startIndex, index);
+                splitLine.push(parsedValue);
+
+                // Then add an empty string to the line since the last character being a field delimiter indicates an empty field
+                splitLine.push('');
+                lines.push(splitLine);
+            } else if (index === lastCharacterIndex || nextNChar === options.delimiter.eol &&
+                // if we aren't inside wrap delimiters or if we are but the character before was a wrap delimiter and we didn't just see two
+                (!stateVariables.insideWrapDelimiter ||
+                    stateVariables.insideWrapDelimiter && charBefore === options.delimiter.wrap && !stateVariables.justParsedDoubleQuote)) {
+                // Otherwise if we reached the end of the line or csv (and current character is not a field delimiter)
+
+                let toIndex = index !== lastCharacterIndex || charBefore === options.delimiter.wrap ? index : undefined;
+
+                // Retrieve the remaining value and add it to the split line list of values
+                splitLine.push(csv.substring(stateVariables.startIndex, toIndex));
+
+                // Finally, push the split line values into the lines array and clear the split line
+                lines.push(splitLine);
+                splitLine = [];
+                stateVariables.startIndex = index + eolDelimiterLength;
+                stateVariables.parsingValue = true;
+                stateVariables.insideWrapDelimiter = charAfter === options.delimiter.wrap;
+            } else if ((charBefore !== options.delimiter.wrap || stateVariables.justParsedDoubleQuote && charBefore === options.delimiter.wrap) &&
+                character === options.delimiter.wrap && utils.getNCharacters(csv, index + 1, eolDelimiterLength) === options.delimiter.eol) {
+                // If we reach a wrap which is not preceded by a wrap delim and the next character is an EOL delim (ie. *"\n)
+
+                stateVariables.insideWrapDelimiter = false;
+                stateVariables.parsingValue = false;
+                // Next iteration will substring, add the value to the line, and push the line onto the array of lines
+            } else if (character === options.delimiter.wrap && (index === 0 || utils.getNCharacters(csv, index - eolDelimiterLength, eolDelimiterLength) === options.delimiter.eol)) {
+                // If the line starts with a wrap delimiter (ie. "*)
+
+                stateVariables.insideWrapDelimiter = true;
+                stateVariables.parsingValue = true;
+                stateVariables.startIndex = index;
+            } else if (character === options.delimiter.wrap && charAfter === options.delimiter.field) {
+                // If we reached a wrap delimiter with a field delimiter after it (ie. *",)
+
+                splitLine.push(csv.substring(stateVariables.startIndex, index + 1));
+                stateVariables.startIndex = index + 2; // next value starts after the field delimiter
+                stateVariables.insideWrapDelimiter = false;
+                stateVariables.parsingValue = false;
+            } else if (character === options.delimiter.wrap && charBefore === options.delimiter.field &&
+                !stateVariables.insideWrapDelimiter && !stateVariables.parsingValue) {
+                // If we reached a wrap delimiter after a comma and we aren't inside a wrap delimiter
+
+                stateVariables.startIndex = index;
+                stateVariables.insideWrapDelimiter = true;
+                stateVariables.parsingValue = true;
+            } else if (character === options.delimiter.wrap && charBefore === options.delimiter.field &&
+                !stateVariables.insideWrapDelimiter && stateVariables.parsingValue) {
+                // If we reached a wrap delimiter with a field delimiter after it (ie. ,"*)
+
+                splitLine.push(csv.substring(stateVariables.startIndex, index - 1));
+                stateVariables.insideWrapDelimiter = true;
+                stateVariables.parsingValue = true;
+                stateVariables.startIndex = index;
+            } else if (character === options.delimiter.wrap && charAfter === options.delimiter.wrap) {
+                // If we run into an escaped quote (ie. "") skip past the second quote
+
+                index += 2;
+                stateVariables.justParsedDoubleQuote = true;
+                continue;
+            } else if (character === options.delimiter.field && charBefore !== options.delimiter.wrap &&
+                charAfter !== options.delimiter.wrap && !stateVariables.insideWrapDelimiter &&
+                stateVariables.parsingValue) {
+                // If we reached a field delimiter and are not inside the wrap delimiters (ie. *,*)
+
+                splitLine.push(csv.substring(stateVariables.startIndex, index));
+                stateVariables.startIndex = index + 1;
+            } else if (character === options.delimiter.field && charBefore === options.delimiter.wrap &&
+                charAfter !== options.delimiter.wrap && !stateVariables.parsingValue) {
+                // If we reached a field delimiter, the previous character was a wrap delimiter, and the
+                //   next character is not a wrap delimiter (ie. ",*)
+
+                stateVariables.insideWrapDelimiter = false;
+                stateVariables.parsingValue = true;
+                stateVariables.startIndex = index + 1;
+            }
+            // Otherwise increment to the next character
+            index++;
+            // Reset the double quote state variable
+            stateVariables.justParsedDoubleQuote = false;
+        }
+
+        return lines;
+    }
+
+    /**
+     * Retrieves the record lines from the split CSV lines and sets it on the params object
+     * @param params
+     * @returns {*}
+     */
+    function retrieveRecordLines(params) {
+        params.recordLines = params.lines.splice(1); // All lines except for the header line
+
+        return params;
+    }
+
+    /**
+     * Retrieves the value for the record from the line at the provided key.
+     * @param line {String[]} split line values for the record
+     * @param key {Object} {index: Number, value: String}
+     */
+    function retrieveRecordValueFromLine(line, key) {
+        // If there is a value at the key's index, use it; otherwise, null
+        let value = line[key.index];
+
+        // Perform any necessary value conversions on the record value
+        return processRecordValue(value);
+    }
+
+    /**
+     * Processes the record's value by parsing the data to ensure the CSV is
+     * converted to the JSON that created it.
+     * @param fieldValue {String}
+     * @returns {*}
+     */
+    function processRecordValue(fieldValue) {
+        // If the value is an array representation, convert it
+        let parsedJson = parseValue(fieldValue);
+        // If parsedJson is anything aside from an error, then we want to use the parsed value
+        // This allows us to interpret values like 'null' --> null, 'false' --> false
+        if (!utils.isError(parsedJson) && !utils.isInvalid(parsedJson)) {
+            fieldValue = parsedJson;
+        } else if (fieldValue === 'undefined') {
+            fieldValue = undefined;
+        }
+
+        return fieldValue;
+    }
+
+    /**
+     * Trims the record value, if specified by the user via the options object
+     * @param fieldValue
+     * @returns {String|null}
+     */
+    function trimRecordValue(fieldValue) {
+        if (options.trimFieldValues && !utils.isNull(fieldValue)) {
+            return fieldValue.trim();
+        }
+        return fieldValue;
+    }
+
+    /**
+     * Create a JSON document with the given keys (designated by the CSV header)
+     *   and the values (from the given line)
+     * @param keys String[]
+     * @param line String
+     * @returns {Object} created json document
+     */
+    function createDocument(keys, line) {
+        // Reduce the keys into a JSON document representing the given line
+        return keys.reduce((document, key) => {
+            // If there is a value at the key's index in the line, set the value; otherwise null
+            let value = retrieveRecordValueFromLine(line, key);
+
+            // Otherwise add the key and value to the document
+            return path.setPath(document, key.value, value);
+        }, {});
+    }
+
+    /**
+     * Removes the outermost wrap delimiters from a value, if they are present
+     * Otherwise, the non-wrapped value is returned as is
+     * @param fieldValue
+     * @returns {String}
+     */
+    function removeWrapDelimitersFromValue(fieldValue) {
+        let firstChar = fieldValue[0],
+            lastIndex = fieldValue.length - 1,
+            lastChar = fieldValue[lastIndex];
+        // If the field starts and ends with a wrap delimiter
+        if (firstChar === options.delimiter.wrap && lastChar === options.delimiter.wrap) {
+            return fieldValue.substr(1, lastIndex - 1);
+        }
+        return fieldValue;
+    }
+
+    /**
+     * Unescapes wrap delimiters by replacing duplicates with a single (eg. "" -> ")
+     * This is done in order to parse RFC 4180 compliant CSV back to JSON
+     * @param fieldValue
+     * @returns {String}
+     */
+    function unescapeWrapDelimiterInField(fieldValue) {
+        return fieldValue.replace(escapedWrapDelimiterRegex, options.delimiter.wrap);
+    }
+
+    /**
+     * Main helper function to convert the CSV to the JSON document array
+     * @param params {Object} {lines: [String], callback: Function}
+     * @returns {Array}
+     */
+    function transformRecordLines(params) {
+        params.json = params.recordLines.reduce((generatedJsonObjects, line) => { // For each line, create the document and add it to the array of documents
+            line = line.map((fieldValue) => {
+                // Perform the necessary operations on each line
+                fieldValue = removeWrapDelimitersFromValue(fieldValue);
+                fieldValue = unescapeWrapDelimiterInField(fieldValue);
+                fieldValue = trimRecordValue(fieldValue);
+
+                return fieldValue;
+            });
+
+            let generatedDocument = createDocument(params.headerFields, line);
+            return generatedJsonObjects.concat(generatedDocument);
+        }, []);
+
+        return params;
+    }
+
+    /**
+     * Attempts to parse the provided value. If it is not parsable, then an error is returned
+     * @param value
+     * @returns {*}
+     */
+    function parseValue(value) {
+        try {
+            if (utils.isStringRepresentation(value, options) && !utils.isDateRepresentation(value)) {
+                return value;
+            }
+
+            let parsedJson = JSON.parse(value);
+
+            // If the parsed value is an array, then we also need to trim record values, if specified
+            if (Array.isArray(parsedJson)) {
+                return parsedJson.map(trimRecordValue);
+            }
+
+            return parsedJson;
+        } catch (err) {
+            return err;
+        }
+    }
+
+    /**
+     * Internally exported csv2json function
+     * Takes options as a document, data as a CSV string, and a callback that will be used to report the results
+     * @param data String csv string
+     * @param callback Function callback function
+     */
+    function convert(data, callback) {
+        // Split the CSV into lines using the specified EOL option
+        // validateCsv(data, callback)
+        //     .then(stripExcelBOM)
+        stripExcelBOM(data)
+            .then(splitCsvLines)
+            .then(retrieveHeading) // Retrieve the headings from the CSV, unless the user specified the keys
+            .then(retrieveRecordLines) // Retrieve the record lines from the CSV
+            .then(transformRecordLines) // Retrieve the JSON document array
+            .then((params) => callback(null, params.json)) // Send the data back to the caller
+            .catch(callback);
+    }
+
+    return {
+        convert,
+        validationFn: utils.isString,
+        validationMessages: constants.errors.csv2json
+    };
+};
+
+module.exports = { Csv2Json };
+
+},{"./constants.json":4,"./utils":8,"doc-path":3}],7:[function(require,module,exports){
+'use strict';
+
 let path = require('doc-path'),
     deeks = require('deeks'),
     constants = require('./constants.json'),
@@ -374,7 +845,12 @@ const Json2Csv = function(options) {
      * @returns {*}
      */
     function generateCsvHeader(params) {
-        params.header = params.headerFields.join(options.delimiter.field);
+        params.header = params.headerFields
+            .map(function(field) {
+                const headerKey = options.fieldTitleMap[field] ? options.fieldTitleMap[field] : field;
+                return wrapFieldValueIfNecessary(headerKey);
+            })
+            .join(options.delimiter.field);
         return params;
     }
 
@@ -385,6 +861,16 @@ const Json2Csv = function(options) {
      * @returns {Promise}
      */
     function retrieveHeaderFields(data) {
+        if (options.keys) {
+            options.keys = options.keys.map((key) => {
+                if (utils.isObject(key) && key.field) {
+                    options.fieldTitleMap[key.field] = key.title || key.field;
+                    return key.field;
+                }
+                return key;
+            });
+        }
+
         if (options.keys && !options.unwindArrays) {
             return Promise.resolve(options.keys)
                 .then(sortHeaderFields);
@@ -402,6 +888,7 @@ const Json2Csv = function(options) {
      *   expandArrayObjects option. If not specified, this passes the params
      *   argument through to the next function in the promise chain.
      * @param params {Object}
+     * @param finalPass {boolean} Used to trigger one last pass to ensure no more arrays need to be expanded
      * @returns {Promise}
      */
     function unwindRecordsIfNecessary(params, finalPass = false) {
@@ -646,7 +1133,7 @@ const Json2Csv = function(options) {
 
 module.exports = { Json2Csv };
 
-},{"./constants.json":4,"./utils":6,"deeks":1,"doc-path":3}],6:[function(require,module,exports){
+},{"./constants.json":4,"./utils":8,"deeks":1,"doc-path":3}],8:[function(require,module,exports){
 'use strict';
 
 let path = require('doc-path'),
@@ -687,9 +1174,12 @@ module.exports = {
  */
 function buildOptions(opts) {
     opts = {...constants.defaultOptions, ...opts || {}};
+    opts.fieldTitleMap = new Map();
 
-    // Note: Object.assign does a shallow default, we need to deep copy the delimiter object
-    opts.delimiter = {...constants.defaultOptions.delimiter, ...opts.delimiter};
+    // Copy the delimiter fields over since the spread operator does a shallow copy
+    opts.delimiter.field = opts.delimiter.field || constants.defaultOptions.delimiter.field;
+    opts.delimiter.wrap = opts.delimiter.wrap || constants.defaultOptions.delimiter.wrap;
+    opts.delimiter.eol = opts.delimiter.eol || constants.defaultOptions.delimiter.eol;
 
     // Otherwise, send the options back
     return opts;
@@ -960,4 +1450,7 @@ function isInvalid(parsedJson) {
         parsedJson === -Infinity;
 }
 
-},{"./constants.json":4,"doc-path":3}]},{},[5]);
+},{"./constants.json":4,"doc-path":3}],9:[function(require,module,exports){
+window.json2csv = require('json-2-csv')
+
+},{"json-2-csv":5}]},{},[9]);

+ 5766 - 0
bundle/jsonform.js

@@ -0,0 +1,5766 @@
+(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+/* 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));
+  };
+
+  /**
+   * Returns true if given property is directly property of an object
+   */
+  var hasOwnProperty = function (obj, prop) {
+    return typeof obj === 'object' && obj.hasOwnProperty(prop);
+  }
+
+  /**
+   * 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, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&#x27;')
+      .replace(/\//g, '&#x2F;');
+  };
+
+/**
+ * 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');
+};
+
+/**
+ *
+ * Slugifies a string by replacing spaces with _. Used to create
+ * valid classnames and ids for the form.
+ *
+ * @function
+ * @param {String} str The string to slugify
+ * @return {String} The slugified string.
+ */
+var slugify = function(str) {
+  return str.replace(/\ /g, '_');
+}
+
+/**
+ * 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();
+    // The `>` here is to prevent activating selectfieldsets inside a tabarray
+    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 '<div ' +
+    '<% for(var key in elt.htmlMetaData) {%>' +
+      '<%= key %>="<%= elt.htmlMetaData[key] %>" ' +
+    '<% }%>' +
+    'class="form-group jsonform-error-<%= keydash %>' +
+    '<%= elt.htmlClass ? " " + elt.htmlClass : "" %>' +
+    '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " jsonform-required" : "") %>' +
+    '<%= (node.readOnly ? " jsonform-readonly" : "") %>' +
+    '<%= (node.disabled ? " jsonform-disabled" : "") %>' +
+    '">' +
+    '<% if (!elt.notitle) { %>' +
+      '<label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label>' +
+    '<% } %>' +
+    '<div class="controls">' +
+      '<% if (node.prepend || node.append) { %>' +
+      '<div class="<% if (node.prepend) { %>input-group<% } %>' +
+        '<% if (node.append) { %> input-group<% } %>">' +
+        '<% if (node.prepend) { %>' +
+          '<span class="input-group-addon"><%= node.prepend %></span>' +
+        '<% } %>' +
+      '<% } %>' +
+      inner +
+      '<% if (node.append) { %>' +
+        '<span class="input-group-addon"><%= node.append %></span>' +
+      '<% } %>' +
+      '<% if (node.prepend || node.append) { %>' +
+        '</div>' +
+      '<% } %>' +
+      '<% if (node.description) { %>' +
+        '<span class="help-block"><%= node.description %></span>' +
+      '<% } %>' +
+      '<span class="help-block jsonform-errortext" style="display:none;"></span>' +
+    '</div></div>';
+};
+
+var fileDisplayTemplate = '<div class="_jsonform-preview">' +
+  '<% if (value.type=="image") { %>' +
+  '<img class="jsonform-preview" id="jsonformpreview-<%= id %>" src="<%= value.url %>" />' +
+  '<% } else { %>' +
+  '<a href="<%= value.url %>"><%= value.name %></a> (<%= Math.ceil(value.size/1024) %>kB)' +
+  '<% } %>' +
+  '</div>' +
+  '<a href="#" class="btn btn-default _jsonform-delete"><i class="glyphicon glyphicon-remove" title="Remove"></i></a> ';
+
+var inputFieldTemplate = function (type) {
+  return {
+    'template': '<input type="' + type + '" ' +
+      'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' +
+      'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
+      '<%= (node.schemaElement && (node.schemaElement.step > 0 || node.schemaElement.step == "any") ? " step=\'" + node.schemaElement.step + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' +
+      '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
+      ' />',
+    'fieldtemplate': true,
+    'inputfield': true
+  }
+};
+
+jsonform.elementTypes = {
+  'none': {
+    'template': ''
+  },
+  'root': {
+    'template': '<div><%= children %></div>'
+  },
+  'text': inputFieldTemplate('text'),
+  'password': inputFieldTemplate('password'),
+  'date': inputFieldTemplate('date'),
+  'datetime': inputFieldTemplate('datetime'),
+  'datetime-local': inputFieldTemplate('datetime-local'),
+  'email': inputFieldTemplate('email'),
+  'month': inputFieldTemplate('month'),
+  'number': inputFieldTemplate('number'),
+  'search': inputFieldTemplate('search'),
+  'tel': inputFieldTemplate('tel'),
+  'time': inputFieldTemplate('time'),
+  'url': inputFieldTemplate('url'),
+  'week': inputFieldTemplate('week'),
+  'range': {
+    'template': '<input type="range" ' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      ' min=<%= range.min %>' +
+      ' max=<%= range.max %>' +
+      ' step=<%= range.step %>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      ' />',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      data.range = {
+        min: 1,
+        max: 100,
+        step: 1
+      };
+      if (!node || !node.schemaElement) return;
+      if (node.formElement && node.formElement.step) {
+        data.range.step = node.formElement.step;
+      }
+      if (typeof node.schemaElement.minimum !== 'undefined') {
+        if (node.schemaElement.exclusiveMinimum) {
+          data.range.min = node.schemaElement.minimum + data.range.step;
+        }
+        else {
+          data.range.min = node.schemaElement.minimum;
+        }
+      }
+      if (typeof node.schemaElement.maximum !== 'undefined') {
+        if (node.schemaElement.exclusiveMaximum) {
+          data.range.max = node.schemaElement.maximum - data.range.step;
+        }
+        else {
+          data.range.max = node.schemaElement.maximum;
+        }
+      }
+    }
+  },
+  'color':{
+    'template':'<input type="text" ' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      ' />',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function(evt, node) {
+      $(node.el).find('#' + escapeSelector(node.id)).spectrum({
+        preferredFormat: "hex",
+        showInput: true
+      });
+    }
+  },
+  'textarea':{
+    'template':'<textarea id="<%= id %>" name="<%= node.name %>" ' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      'style="height:<%= elt.height || "150px" %>;width:<%= elt.width || "100%" %>;"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
+      '><%= value %></textarea>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'wysihtml5':{
+    'template':'<textarea id="<%= id %>" name="<%= node.name %>" style="height:<%= elt.height || "300px" %>;width:<%= elt.width || "100%" %>;"' +
+      '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
+      '><%= value %></textarea>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function (evt, node) {
+      var setup = function () {
+        //protect from double init
+        if ($(node.el).data("wysihtml5")) return;
+        $(node.el).data("wysihtml5_loaded",true);
+
+        $(node.el).find('#' + escapeSelector(node.id)).wysihtml5({
+          "html": true,
+          "link": true,
+          "font-styles":true,
+          "image": false,
+          "events": {
+            "load": function () {
+              // In chrome, if an element is required and hidden, it leads to
+              // the error 'An invalid form control with name='' is not focusable'
+              // See http://stackoverflow.com/questions/7168645/invalid-form-control-only-in-google-chrome
+              $(this.textareaElement).removeAttr('required');
+            }
+          }
+        });
+      };
+
+      // Is there a setup hook?
+      if (window.jsonform_wysihtml5_setup) {
+        window.jsonform_wysihtml5_setup(setup);
+        return;
+      }
+
+      // Wait until wysihtml5 is loaded
+      var itv = window.setInterval(function() {
+        if (window.wysihtml5) {
+          window.clearInterval(itv);
+          setup();
+        }
+      },1000);
+    }
+  },
+  'ace':{
+    'template':'<div id="<%= id %>" style="position:relative;height:<%= elt.height || "300px" %>;"><div id="<%= id %>__ace" style="width:<%= elt.width || "100%" %>;height:<%= elt.height || "300px" %>;"></div><input type="hidden" name="<%= node.name %>" id="<%= id %>__hidden" value="<%= escape(value) %>"/></div>',
+    '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': '<div class="checkbox"><label><input type="checkbox" id="<%= id %>" ' +
+      '<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %>' +
+      'name="<%= node.name %>" value="1" <% if (value) {%>checked<% } %>' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' +
+      ' /><%= node.inlinetitle || "" %>' +
+      '</label></div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    }
+  },
+  'file':{
+    'template':'<input class="input-file" id="<%= id %>" name="<%= node.name %>" type="file" ' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '/>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'file-hosted-public':{
+    'template':'<span><% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %><input class="input-file" id="_transloadit_<%= id %>" type="file" name="<%= transloaditname %>" /><input data-transloadit-name="_transloadit_<%= transloaditname %>" type="hidden" id="<%= id %>" name="<%= node.name %>" value=\'<%= escape(JSON.stringify(node.value)) %>\' /></span>',
+    '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': '<span><% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %><input class="input-file" id="_transloadit_<%= id %>" type="file" name="_transloadit_<%= node.name %>" /><input type="hidden" id="<%= id %>" name="<%= node.name %>" value=\'<%= escape(JSON.stringify(node.value)) %>\' /></span>',
+    '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':'<select name="<%= node.name %>" id="<%= id %>"' +
+      'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' +
+      '<%= (node.schemaElement && node.schemaElement.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '> ' +
+      '<% _.each(node.options, function(key, val) { if(key instanceof Object) { if (value === key.value) { %> <option selected value="<%= key.value %>"><%= key.title %></option> <% } else { %> <option value="<%= key.value %>"><%= key.title %></option> <% }} else { if (value === key) { %> <option selected value="<%= key %>"><%= key %></option> <% } else { %><option value="<%= key %>"><%= key %></option> <% }}}); %> ' +
+      '</select>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'imageselect': {
+    'template': '<div>' +
+      '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' +
+      '<div class="dropdown">' +
+      '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } else { %> btn-default<% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' +
+        '<% if (node.value) { %><img src="<% if (!node.value.match(/^https?:/)) { %><%= prefix %><% } %><%= node.value %><%= suffix %>" alt="" /><% } else { %><%= buttonTitle %><% } %>' +
+      '</a>' +
+      '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' +
+        '<div>' +
+        '<% _.each(node.options, function(key, idx) { if ((idx > 0) && ((idx % columns) === 0)) { %></div><div><% } %><a class="btn<% if (buttonClass) { %> <%= buttonClass %><% } else { %> btn-default<% } %>" style="max-width:<%= width %>px;max-height:<%= height %>px"><% if (key instanceof Object) { %><img src="<% if (!key.value.match(/^https?:/)) { %><%= prefix %><% } %><%= key.value %><%= suffix %>" alt="<%= key.title %>" /></a><% } else { %><img src="<% if (!key.match(/^https?:/)) { %><%= prefix %><% } %><%= key %><%= suffix %>" alt="" /><% } %></a> <% }); %>' +
+        '</div>' +
+        '<div class="pagination-right"><a class="btn btn-default">Reset</a></div>' +
+      '</div>' +
+      '</div>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      var elt = node.formElement || {};
+      var nbRows = null;
+      var maxColumns = elt.imageSelectorColumns || 5;
+      data.buttonTitle = elt.imageSelectorTitle || 'Select...';
+      data.prefix = elt.imagePrefix || '';
+      data.suffix = elt.imageSuffix || '';
+      data.width = elt.imageWidth || 32;
+      data.height = elt.imageHeight || 32;
+      data.buttonClass = elt.imageButtonClass || false;
+      if (node.options.length > maxColumns) {
+        nbRows = Math.ceil(node.options.length / maxColumns);
+        data.columns = Math.ceil(node.options.length / nbRows);
+      }
+      else {
+        data.columns = maxColumns;
+      }
+    },
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).on('click', '.dropdown-menu a', function (evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+        var img = (evt.target.nodeName.toLowerCase() === 'img') ?
+          $(evt.target) :
+          $(evt.target).find('img');
+        var value = img.attr('src');
+        var elt = node.formElement || {};
+        var prefix = elt.imagePrefix || '';
+        var suffix = elt.imageSuffix || '';
+        var width = elt.imageWidth || 32;
+        var height = elt.imageHeight || 32;
+        if (value) {
+          if (value.indexOf(prefix) === 0) {
+            value = value.substring(prefix.length);
+          }
+          value = value.substring(0, value.length - suffix.length);
+          $(node.el).find('input').attr('value', value);
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .addClass(elt.imageButtonClass)
+            .attr('style', 'max-width:' + width + 'px;max-height:' + height + 'px')
+            .html('<img src="' + (!value.match(/^https?:/) ? prefix : '') + value + suffix + '" alt="" />');
+        }
+        else {
+          $(node.el).find('input').attr('value', '');
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .removeClass(elt.imageButtonClass)
+            .removeAttr('style')
+            .html(elt.imageSelectorTitle || 'Select...');
+        }
+      });
+    }
+  },
+  'iconselect': {
+    'template': '<div>' +
+      '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' +
+      '<div class="dropdown">' +
+      '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' +
+        '<% if (node.value) { %><i class="icon-<%= node.value %>" /><% } else { %><%= buttonTitle %><% } %>' +
+      '</a>' +
+      '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' +
+        '<div>' +
+        '<% _.each(node.options, function(key, idx) { if ((idx > 0) && ((idx % columns) === 0)) { %></div><div><% } %><a class="btn<% if (buttonClass) { %> <%= buttonClass %><% } %>" ><% if (key instanceof Object) { %><i class="icon-<%= key.value %>" alt="<%= key.title %>" /></a><% } else { %><i class="icon-<%= key %>" alt="" /><% } %></a> <% }); %>' +
+        '</div>' +
+        '<div class="pagination-right"><a class="btn">Reset</a></div>' +
+      '</div>' +
+      '</div>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      var elt = node.formElement || {};
+      var nbRows = null;
+      var maxColumns = elt.imageSelectorColumns || 5;
+      data.buttonTitle = elt.imageSelectorTitle || 'Select...';
+      data.buttonClass = elt.imageButtonClass || false;
+      if (node.options.length > maxColumns) {
+        nbRows = Math.ceil(node.options.length / maxColumns);
+        data.columns = Math.ceil(node.options.length / nbRows);
+      }
+      else {
+        data.columns = maxColumns;
+      }
+    },
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'onInsert': function (evt, node) {
+      $(node.el).on('click', '.dropdown-menu a', function (evt) {
+        evt.preventDefault();
+        evt.stopPropagation();
+        var i = (evt.target.nodeName.toLowerCase() === 'i') ?
+          $(evt.target) :
+          $(evt.target).find('i');
+        var value = i.attr('class');
+        var elt = node.formElement || {};
+        if (value) {
+          value = value;
+          $(node.el).find('input').attr('value', value);
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .addClass(elt.imageButtonClass)
+            .html('<i class="'+ value +'" alt="" />');
+        }
+        else {
+          $(node.el).find('input').attr('value', '');
+          $(node.el).find('a[data-toggle="dropdown"]')
+            .removeClass(elt.imageButtonClass)
+            .html(elt.imageSelectorTitle || 'Select...');
+        }
+      });
+    }
+  },
+  'radios':{
+    'template': '<div id="<%= node.id %>"><% _.each(node.options, function(key, val) { %><div class="radio"><label><input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" <% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>"' +
+      '<%= (node.disabled? " disabled" : "")%>' +
+      '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
+      '/><%= (key instanceof Object ? key.title : key) %></label></div> <% }); %></div>',
+    'fieldtemplate': true,
+    'inputfield': true
+  },
+  'radiobuttons': {
+    'template': '<div id="<%= node.id %>">' +
+      '<% _.each(node.options, function(key, val) { %>' +
+        '<label class="btn btn-default">' +
+        '<input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" style="position:absolute;left:-9999px;" ' +
+        '<% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>" />' +
+        '<span><%= (key instanceof Object ? key.title : key) %></span></label> ' +
+        '<% }); %>' +
+      '</div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onInsert': function (evt, node) {
+      var activeClass = 'active';
+      var elt = node.formElement || {};
+      if (elt.activeClass) {
+        activeClass += ' ' + elt.activeClass;
+      }
+      $(node.el).find('label').on('click', function () {
+        $(this).parent().find('label').removeClass(activeClass);
+        $(this).addClass(activeClass);
+      });
+    }
+  },
+  'checkboxes':{
+    'template': '<div><%= choiceshtml %></div>',
+    'fieldtemplate': true,
+    'inputfield': true,
+    'onBeforeRender': function (data, node) {
+      // Build up choices from the enumeration list
+      var choices = null;
+      var choiceshtml = null;
+      var template = '<div class="checkbox"><label>' +
+        '<input type="checkbox" <% if (value) { %> checked="checked" <% } %> name="<%= name %>" value="1"' +
+        '<%= (node.disabled? " disabled" : "")%>' +
+        '/><%= title %></label></div>';
+      if (!node || !node.schemaElement) return;
+
+      if (node.schemaElement.items) {
+        choices =
+          node.schemaElement.items["enum"] ||
+          node.schemaElement.items[0]["enum"];
+      } else {
+        choices = node.schemaElement["enum"];
+      }
+      if (!choices) return;
+
+      choiceshtml = '';
+      _.each(choices, function (choice, idx) {
+        choiceshtml += _.template(template, fieldTemplateSettings)({
+          name: node.key + '[' + idx + ']',
+          value: _.include(node.value, choice),
+          title: hasOwnProperty(node.formElement.titleMap, choice) ? node.formElement.titleMap[choice] : choice,
+          node: node
+        });
+      });
+
+      data.choiceshtml = choiceshtml;
+    }
+  },
+  'array': {
+    'template': '<div id="<%= id %>"><ul class="_jsonform-array-ul" style="list-style-type:none;"><%= children %></ul>' +
+      '<span class="_jsonform-array-buttons">' +
+        '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' +
+        '<a href="#" class="btn btn-default _jsonform-array-deletelast"><i class="glyphicon glyphicon-minus-sign" title="Delete last"></i></a>' +
+      '</span>' +
+      '</div>',
+    'fieldtemplate': true,
+    'array': true,
+    'childTemplate': function (inner) {
+      if ($('').sortable) {
+        // Insert a "draggable" icon
+        // floating to the left of the main element
+        return '<li data-idx="<%= node.childPos %>">' +
+          '<span class="draggable line"><i class="glyphicon glyphicon-list" title="Move item"></i></span>' +
+          inner +
+          '</li>';
+      }
+      else {
+        return '<li data-idx="<%= node.childPos %>">' +
+          inner +
+          '</li>';
+      }
+    },
+    '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': '<div id="<%= id %>"><div class="tabbable tabs-left">' +
+      '<ul class="nav nav-tabs">' +
+        '<%= tabs %>' +
+      '</ul>' +
+      '<div class="tab-content">' +
+        '<%= children %>' +
+      '</div>' +
+      '</div>' +
+      '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' +
+      '<a href="#" class="btn btn-default _jsonform-array-deleteitem"><i class="glyphicon glyphicon-minus-sign" title="Delete item"></i></a></div>',
+    'fieldtemplate': true,
+    'array': true,
+    'childTemplate': function (inner) {
+      return '<div data-idx="<%= node.childPos %>" class="tab-pane">' +
+        inner +
+        '</div>';
+    },
+    'onBeforeRender': function (data, node) {
+      // Generate the initial 'tabs' from the children
+      var tabs = '';
+      _.each(node.children, function (child, idx) {
+        var title = child.legend ||
+          child.title ||
+          ('Item ' + (idx+1));
+        tabs += '<li data-idx="' + idx + '"' +
+          ((idx === 0) ? ' class="active"' : '') +
+          '><a class="draggable tab" data-toggle="tab">' +
+          escapeHTML(title) +
+          '</a></li>';
+      });
+      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) {
+          $('> .tabbable > .tab-content > [data-idx="' + idx + '"] > fieldset > legend', $nodeid).html(child.legend);
+          var title = child.legend || child.title || ('Item ' + (idx+1));
+          tabs += '<li data-idx="' + idx + '">' +
+                  '<a class="draggable tab" data-toggle="tab">' +
+                  escapeHTML(title) +
+                  '</a></li>';
+        });
+        $('> .tabbable > .nav-tabs', $nodeid).html(tabs);
+        if (activateFirstTab) {
+          $('> .tabbable > .nav-tabs [data-idx="0"]', $nodeid).addClass('active');
+        }
+        $('> .tabbable > .nav-tabs [data-toggle="tab"]', $nodeid).eq(selIdx).click();
+      };
+
+      $('> a._jsonform-array-deleteitem', $nodeid).click(function (evt) {
+        var idx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx');
+        evt.preventDefault();
+        evt.stopPropagation();
+        if (boundaries.minItems > 0) {
+          if (node.children.length < boundaries.minItems + 1) {
+            $nodeid.find('> a._jsonform-array-deleteitem')
+              .addClass('disabled');
+          }
+          if (node.children.length <= boundaries.minItems) return false;
+        }
+        node.deleteArrayItem(idx);
+        updateTabs();
+        if ((node.children.length < boundaries.minItems + 1) ||
+            (node.children.length === 0)) {
+          $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
+        }
+        if ((boundaries.maxItems >= 0) &&
+            (node.children.length <= boundaries.maxItems)) {
+          $nodeid.find('> a._jsonform-array-addmore').removeClass('disabled');
+        }
+      });
+
+      $('> a._jsonform-array-addmore', $nodeid).click(function (evt) {
+        var idx = node.children.length;
+        if (boundaries.maxItems>=0) {
+          if (node.children.length>boundaries.maxItems-2) {
+            $('> a._jsonform-array-addmore', $nodeid).addClass("disabled");
+          }
+          if (node.children.length > boundaries.maxItems - 1) {
+            return false;
+          }
+        }
+        evt.preventDefault();
+        evt.stopPropagation();
+        node.insertArrayItem(idx,
+          $nodeid.find('> .tabbable > .tab-content').get(0));
+        updateTabs(idx);
+        if ((boundaries.minItems <= 0) ||
+            ((boundaries.minItems > 0) && (idx > boundaries.minItems - 1))) {
+          $nodeid.find('> a._jsonform-array-deleteitem').removeClass('disabled');
+        }
+      });
+
+      $(node.el).on('legendUpdated', function (evt) {
+        updateTabs();
+        evt.preventDefault();
+        evt.stopPropagation();
+      });
+
+      if ($(node.el).sortable) {
+        $('> .tabbable > .nav-tabs', $nodeid).sortable({
+          containment: node.el,
+          tolerance: 'pointer'
+        });
+        $('> .tabbable > .nav-tabs', $nodeid).bind('sortstop', function (event, ui) {
+          var idx = $(ui.item).data('idx');
+          var newIdx = $(ui.item).index();
+          moveNodeTo(idx, newIdx);
+          updateTabs(newIdx);
+        });
+      }
+
+      // Simulate User's click to setup the form with its minItems
+      if ((boundaries.minItems >= 0)  && 
+          (node.children.length <= boundaries.minItems)) {
+        for (var i = 0; i < (boundaries.minItems - 1); i++) {
+          $nodeid.find('> a._jsonform-array-addmore').click();
+        }
+        $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
+        updateTabs();
+      }
+
+      if ((boundaries.maxItems >= 0) &&
+          (node.children.length >= boundaries.maxItems)) {
+        $nodeid.find('> a._jsonform-array-addmore').addClass('disabled');
+      }
+      if ((boundaries.minItems >= 0) &&
+          (node.children.length <= boundaries.minItems)) {
+        $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
+      }
+    }
+  },
+  'help': {
+    'template':'<span class="help-block" style="padding-top:5px"><%= elt.helpvalue %></span>',
+    'fieldtemplate': true
+  },
+  'msg': {
+    'template': '<%= elt.msg %>'
+  },
+  'fieldset': {
+    'template': '<fieldset class="form-group jsonform-error-<%= keydash %> <% if (elt.expandable) { %>expandable<% } %> <%= elt.htmlClass?elt.htmlClass:"" %>" ' +
+      '<% if (id) { %> id="<%= id %>"<% } %>' +
+      '>' +
+      '<% if (node.title || node.legend) { %><legend><%= node.title || node.legend %></legend><% } %>' +
+      '<% if (elt.expandable) { %><div class="form-group"><% } %>' +
+      '<%= children %>' +
+      '<% if (elt.expandable) { %></div><% } %>' +
+      '</fieldset>',
+    onInsert: function (evt, node) {
+      $('.expandable > div, .expandable > fieldset', node.el).hide();
+      // See #233 
+      $(".expandable", node.el).removeClass("expanded");
+    }
+  },
+  'advancedfieldset': {
+    'template': '<fieldset' +
+      '<% if (id) { %> id="<%= id %>"<% } %>' +
+      ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' +
+      '<legend><%= (node.title || node.legend) ? (node.title || node.legend) : "Advanced options" %></legend>' +
+      '<div class="form-group">' +
+      '<%= children %>' +
+      '</div>' +
+      '</fieldset>',
+    onInsert: function (evt, node) {
+      $('.expandable > div, .expandable > fieldset', node.el).hide();
+      // See #233 
+      $(".expandable", node.el).removeClass("expanded");
+    }
+  },
+  'authfieldset': {
+    'template': '<fieldset' +
+      '<% if (id) { %> id="<%= id %>"<% } %>' +
+      ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' +
+      '<legend><%= (node.title || node.legend) ? (node.title || node.legend) : "Authentication settings" %></legend>' +
+      '<div class="form-group">' +
+      '<%= children %>' +
+      '</div>' +
+      '</fieldset>',
+    onInsert: function (evt, node) {
+      $('.expandable > div, .expandable > fieldset', node.el).hide();
+      // See #233 
+      $(".expandable", node.el).removeClass("expanded");
+    }
+  },
+  'submit':{
+    'template':'<input type="submit" <% if (id) { %> id="<%= id %>" <% } %> class="btn btn-primary <%= elt.htmlClass?elt.htmlClass:"" %>" value="<%= value || node.title %>"<%= (node.disabled? " disabled" : "")%>/>'
+  },
+  'button':{
+    'template':' <button type="button" <% if (id) { %> id="<%= id %>" <% } %> class="btn btn-default <%= elt.htmlClass?elt.htmlClass:"" %>"><%= node.title %></button> '
+  },
+  'actions':{
+    'template':'<div class="<%= elt.htmlClass?elt.htmlClass:"" %>"><%= children %></div>'
+  },
+  'hidden':{
+    'template':'<input type="hidden" id="<%= id %>" name="<%= node.name %>" value="<%= escape(value) %>" />',
+    'inputfield': true
+  },
+  'selectfieldset': {
+    'template': '<fieldset class="tab-container <%= elt.htmlClass?elt.htmlClass:"" %>">' +
+      '<% if (node.legend) { %><legend><%= node.legend %></legend><% } %>' +
+      '<% if (node.formElement.key) { %><input type="hidden" id="<%= node.id %>" name="<%= node.name %>" value="<%= escape(value) %>" /><% } else { %>' +
+        '<a id="<%= node.id %>"></a><% } %>' +
+      '<div class="tabbable">' +
+        '<div class="form-group<%= node.formElement.hideMenu ? " hide" : "" %>">' +
+          '<% if (!elt.notitle) { %><label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label><% } %>' +
+          '<div class="controls"><%= tabs %></div>' +
+        '</div>' +
+        '<div class="tab-content">' +
+          '<%= children %>' +
+        '</div>' +
+      '</div>' +
+      '</fieldset>',
+    'inputfield': true,
+    'getElement': function (el) {
+      return $(el).parent().get(0);
+    },
+    'childTemplate': function (inner) {
+      return '<div data-idx="<%= node.childPos %>" class="tab-pane' +
+        '<% if (node.active) { %> active<% } %>">' +
+        inner +
+        '</div>';
+    },
+    '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];
+          child.childPos = idx; // When nested the childPos is always 0.
+          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) {
+          child.childPos = idx; // When nested the childPos is always 0.
+          return {
+            title: child.legend || child.title || ('Option ' + (child.childPos+1)),
+            value: choices[child.childPos] || child.childPos,
+            node: child
+          };
+        });
+      }
+
+      // Reset each children to inactive so that they are not shown on insert
+      // The active one will then be shown later one. This is useful when sorting
+      // arrays with selectfieldset, otherwise both fields could be active at the
+      // same time.
+      _.each(children, function (child, idx) {
+        child.node.active = false
+      });
+
+      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 = '<select class="nav"' +
+        (node.disabled ? ' disabled' : '') +
+        '>';
+      _.each(children, function (child, idx) {
+        tabs += '<option data-idx="' + idx + '" value="' + child.value + '"' +
+          (child.node.active ? ' selected="selected" class="active"' : '') +
+          '>' +
+          escapeHTML(child.title) +
+          '</option>';
+      });
+      tabs += '</select>';
+
+      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': '<div' +
+      '<% if (node.id) { %> id="<%= node.id %>"<% } %>' +
+      '>' +
+      '<%= children %>' +
+      '</div>'
+  },
+  'section': {
+    'template': '<div' +
+      '<% if (elt.htmlClass) { %> class="<%= elt.htmlClass %>"<% } %>' +
+      '<% if (node.id) { %> id="<%= node.id %>"<% } %>' +
+      '><%= children %></div>'
+  },
+
+  /**
+   * A "questions" field renders a series of question fields and binds the
+   * result to the value of a schema key.
+   */
+  'questions': {
+    'template': '<div>' +
+      '<input type="hidden" id="<%= node.id %>" name="<%= node.name %>" value="<%= escape(value) %>" />' +
+      '<%= children %>' +
+      '</div>',
+    'fieldtemplate': 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': '<div id="<%= node.id %>"><% _.each(node.options, function(key, val) { %><label class="<%= (node.formElement.optionsType === "radiobuttons") ? "btn btn-default" : "" %><%= ((key instanceof Object && key.htmlClass) ? " " + key.htmlClass : "") %>"><input type="radio" <% if (node.formElement.optionsType === "radiobuttons") { %> style="position:absolute;left:-9999px;" <% } %>name="<%= node.id %>" value="<%= val %>"<%= (node.disabled? " disabled" : "")%>/><span><%= (key instanceof Object ? key.title : key) %></span></label> <% }); %></div>',
+    '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 (prop && !_.isArray(innerobj[prop])) return null;
+        innerobj = prop ? innerobj[prop][parseInt(arrayMatch[1])] : innerobj[parseInt(arrayMatch[1])];
+        arrayMatch = reArray.exec(subkey);
+        if (!arrayMatch) break;
+        // In the case of multidimensional arrays,
+        // we should not take innerobj[prop][0] anymore,
+        // but innerobj[0] directly
+        prop = null;
+      }
+    } 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, valueTemplateSettings)(tpldata);
+    }
+  }
+
+  // TODO: handle on the formElement.options, because user can setup it too.
+  // Apply titleMap if needed
+  if (isSet(value) && formElement && hasOwnProperty(formElement.titleMap, value)) {
+    value = _.template(formElement.titleMap[value], valueTemplateSettings)(tpldata);
+  }
+
+  // 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) {
+    if (!values) {
+      return '';
+    }
+    var returnValue = values;
+    var listKey = key.split('[].');
+    var i;
+    for (i = 0; i < listKey.length - 1; i++) {
+      returnValue = returnValue[listKey[i]][self.arrayPath[i]];
+    }
+    return returnValue[listKey[i]];
+  };
+
+  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 = slugify(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], valueTemplateSettings)(formData);
+        }
+      }
+      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, valueTemplateSettings)(formData)
+          });
+        }
+        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, valueTemplateSettings)(formData);
+          }
+        }
+        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, valueTemplateSettings)(formData);
+          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, fieldTemplateSettings)(data);
+  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) {
+      var onChangeHandler = 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, valueTemplateSettings)(formData);
+          $(node.parentNode.el).trigger('legendUpdated');
+        }
+      };
+      $(this.legendChild.el).bind('change', onChangeHandler);
+      $(this.legendChild.el).bind('keyup', onChangeHandler);
+    }
+  }
+
+  // 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-' + slugify(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 schema type is an array containing only a type and "null",
+      // remove null and make the element non-required
+      if (_.isArray(schemaElement.type)) {
+        if (_.contains(schemaElement.type, "null")) {
+          schemaElement.type = _.without(schemaElement.type, "null");
+          schemaElement.required = false;
+        }
+        if (schemaElement.type.length > 1) {
+          throw new Error("Cannot process schema element with multiple types.");
+        }
+        schemaElement.type = _.first(schemaElement.type);
+      }
+
+      if ((schemaElement.type === 'string') &&
+        (schemaElement.format === 'color')) {
+        formElement.type = 'color';
+      } else if ((schemaElement.type === 'number' ||
+        schemaElement.type === 'integer') &&
+        !schemaElement['enum']) {
+       formElement.type = 'number';
+       if (schemaElement.type === 'number') schemaElement.step = 'any';
+      } else if ((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: hasOwnProperty(formElement.titleMap, value) ? 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<root.children.length;i++) {
+      callback(root.children[i]);
+      f(root.children[i]);
+    }
+  };
+  f(this.root);
+
+};
+
+formTree.prototype.validate = function(noErrorDisplay) {
+
+  var values = jsonform.getFormValue(this.domRoot);
+  var errors = false;
+
+  var options = this.formDesc;
+
+  if (options.validate!==false) {
+    var validator = false;
+    if (typeof options.validate!="object") {
+      if (global.JSONFormValidator) {
+        validator = global.JSONFormValidator.createEnvironment("json-schema-draft-03");
+      }
+    } else {
+      validator = options.validate;
+    }
+    if (validator) {
+      var v = validator.validate(values, this.formDesc.schema);
+      $(this.domRoot).jsonFormErrors(false,options);
+      if (v.errors.length) {
+        if (!errors) errors = [];
+        errors = errors.concat(v.errors);
+      }
+    }
+  }
+
+  if (errors && !noErrorDisplay) {
+    if (options.displayErrors) {
+      options.displayErrors(errors,this.domRoot);
+    } else {
+      $(this.domRoot).jsonFormErrors(errors,options);
+    }
+  }
+
+  return {"errors":errors}
+
+}
+
+formTree.prototype.submit = function(evt) {
+
+  var stopEvent = function() {
+    if (evt) {
+      evt.preventDefault();
+      evt.stopPropagation();
+    }
+    return false;
+  };
+  var values = jsonform.getFormValue(this.domRoot);
+  var options = this.formDesc;
+
+  var brk=false;
+  this.forEachElement(function(elt) {
+    if (brk) return;
+    if (elt.view.onSubmit) {
+      brk = !elt.view.onSubmit(evt, elt); //may be called multiple times!!
+    }
+  });
+
+  if (brk) return stopEvent();
+
+  var validated = this.validate();
+
+  if (options.onSubmit && !options.onSubmit(validated.errors,values)) {
+    return stopEvent();
+  }
+
+  if (validated.errors) return stopEvent();
+
+  if (options.onSubmitValid && !options.onSubmitValid(values)) {
+    return stopEvent();
+  }
+
+  return false;
+
+};
+
+
+/**
+ * Returns true if the form displays a "required" field.
+ *
+ * To keep things simple, the function parses the form's schema and returns
+ * true as soon as it finds a "required" flag even though, in theory, that
+ * schema key may not appear in the final form.
+ *
+ * Note that a "required" constraint on a boolean type is always enforced,
+ * the code skips such definitions.
+ *
+ * @function
+ * @return {boolean} True when the form has some required field,
+ *  false otherwise.
+ */
+formTree.prototype.hasRequiredField = function () {
+  var parseElement = function (element) {
+    if (!element) return null;
+    if (element.required && (element.type !== 'boolean')) {
+      return element;
+    }
+
+    var prop = _.find(element.properties, function (property) {
+      return parseElement(property);
+    });
+    if (prop) {
+      return prop;
+    }
+
+    if (element.items) {
+      if (_.isArray(element.items)) {
+        prop = _.find(element.items, function (item) {
+          return parseElement(item);
+        });
+      }
+      else {
+        prop = parseElement(element.items);
+      }
+      if (prop) {
+        return prop;
+      }
+    }
+  };
+
+  return parseElement(this.formDesc.schema);
+};
+
+
+/**
+ * Returns the structured object that corresponds to the form values entered
+ * by the use for the given form.
+ *
+ * The form must have been previously rendered through a call to jsonform.
+ *
+ * @function
+ * @param {Node} The <form> 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 <form> element in the document.
+ *
+ * The function handles the following properties for the JSON Form object it
+ * receives as parameter:
+ * - schema (required): The JSON Schema that describes the form to render
+ * - form: The options form layout description, overrides default layout
+ * - prefix: String to use to prefix computed IDs. Default is an empty string.
+ *  Use this option if JSON Form is used multiple times in an application with
+ *  schemas that have overlapping parameter names to avoid running into multiple
+ *  IDs issues. Default value is "jsonform-[counter]".
+ * - transloadit: Transloadit parameters when transloadit is used
+ * - validate: Validates form against schema upon submission. Uses the value
+ * of the "validate" property as validator if it is an object.
+ * - displayErrors: Function to call with errors upon form submission.
+ *  Default is to render the errors next to the input fields.
+ * - submitEvent: Name of the form submission event to bind to.
+ *  Default is "submit". Set this option to false to avoid event binding.
+ * - onSubmit: Callback function to call when form is submitted
+ * - onSubmitValid: Callback function to call when form is submitted without
+ *  errors.
+ *
+ * @function
+ * @param {Object} options The JSON Form object to use as basis for the form
+ */
+$.fn.jsonForm = function(options) {
+  var formElt = this;
+
+  options = _.defaults({}, options, {submitEvent: 'submit'});
+
+  var form = new formTree();
+  form.initialize(options);
+  form.render(formElt.get(0));
+
+  // TODO: move that to formTree.render
+  if (options.transloadit) {
+    formElt.append('<input type="hidden" name="params" value=\'' +
+      escapeHTML(JSON.stringify(options.transloadit.params)) +
+      '\'>');
+  }
+
+  // Keep a direct pointer to the JSON schema for form submission purpose
+  formElt.data("jsonform-tree", form);
+
+  if (options.submitEvent) {
+    formElt.unbind((options.submitEvent)+'.jsonform');
+    formElt.bind((options.submitEvent)+'.jsonform', function(evt) {
+      form.submit(evt);
+    });
+  }
+
+  // Initialize tabs sections, if any
+  initializeTabs(formElt);
+
+  // Initialize expandable sections, if any
+  $('.expandable > div, .expandable > fieldset', formElt).hide();
+  formElt.on('click', '.expandable > legend', function () {
+    var parent = $(this).parent();
+    parent.toggleClass('expanded');
+    $('> div', parent).slideToggle(100);
+  });
+
+  return form;
+};
+
+
+/**
+ * Retrieves the structured values object generated from the values
+ * entered by the user and the data schema that gave birth to the form.
+ *
+ * Defined as a jQuery function that typically needs to be applied to
+ * a <form> element whose content has previously been generated by a
+ * call to "jsonForm".
+ *
+ * Unless explicitly disabled, the values are automatically validated
+ * against the constraints expressed in the schema.
+ *
+ * @function
+ * @return {Object} Structured values object that matches the user inputs
+ *  and the data schema.
+ */
+$.fn.jsonFormValue = function() {
+  return jsonform.getFormValue(this);
+};
+
+// Expose the getFormValue method to the global object
+// (other methods exposed as jQuery functions)
+global.JSONForm = global.JSONForm || {util:{}};
+global.JSONForm.getFormValue = jsonform.getFormValue;
+global.JSONForm.fieldTemplate = jsonform.fieldTemplate;
+global.JSONForm.fieldTypes = jsonform.elementTypes;
+global.JSONForm.getInitialValue = getInitialValue;
+global.JSONForm.util.getObjKey = jsonform.util.getObjKey;
+global.JSONForm.util.setObjKey = jsonform.util.setObjKey;
+
+})((typeof exports !== 'undefined'),
+  ((typeof exports !== 'undefined') ? exports : window),
+  ((typeof jQuery !== 'undefined') ? jQuery : { fn: {} }),
+  ((typeof _ !== 'undefined') ? _ : null),
+  JSON);
+
+},{"underscore":2}],2:[function(require,module,exports){
+(function (global){(function (){
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define('underscore', factory) :
+  (global = global || self, (function () {
+    var current = global._;
+    var exports = global._ = factory();
+    exports.noConflict = function () { global._ = current; return exports; };
+  }()));
+}(this, (function () {
+  //     Underscore.js 1.12.0
+  //     https://underscorejs.org
+  //     (c) 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+  //     Underscore may be freely distributed under the MIT license.
+
+  // Current version.
+  var VERSION = '1.12.0';
+
+  // Establish the root object, `window` (`self`) in the browser, `global`
+  // on the server, or `this` in some virtual machines. We use `self`
+  // instead of `window` for `WebWorker` support.
+  var root = typeof self == 'object' && self.self === self && self ||
+            typeof global == 'object' && global.global === global && global ||
+            Function('return this')() ||
+            {};
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype;
+  var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var push = ArrayProto.push,
+      slice = ArrayProto.slice,
+      toString = ObjProto.toString,
+      hasOwnProperty = ObjProto.hasOwnProperty;
+
+  // Modern feature detection.
+  var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined',
+      supportsDataView = typeof DataView !== 'undefined';
+
+  // All **ECMAScript 5+** native function implementations that we hope to use
+  // are declared here.
+  var nativeIsArray = Array.isArray,
+      nativeKeys = Object.keys,
+      nativeCreate = Object.create,
+      nativeIsView = supportsArrayBuffer && ArrayBuffer.isView;
+
+  // Create references to these builtin functions because we override them.
+  var _isNaN = isNaN,
+      _isFinite = isFinite;
+
+  // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
+  var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
+  var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
+    'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
+
+  // The largest integer that can be represented exactly.
+  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
+
+  // Some functions take a variable number of arguments, or a few expected
+  // arguments at the beginning and then a variable number of values to operate
+  // on. This helper accumulates all remaining arguments past the function’s
+  // argument length (or an explicit `startIndex`), into an array that becomes
+  // the last argument. Similar to ES6’s "rest parameter".
+  function restArguments(func, startIndex) {
+    startIndex = startIndex == null ? func.length - 1 : +startIndex;
+    return function() {
+      var length = Math.max(arguments.length - startIndex, 0),
+          rest = Array(length),
+          index = 0;
+      for (; index < length; index++) {
+        rest[index] = arguments[index + startIndex];
+      }
+      switch (startIndex) {
+        case 0: return func.call(this, rest);
+        case 1: return func.call(this, arguments[0], rest);
+        case 2: return func.call(this, arguments[0], arguments[1], rest);
+      }
+      var args = Array(startIndex + 1);
+      for (index = 0; index < startIndex; index++) {
+        args[index] = arguments[index];
+      }
+      args[startIndex] = rest;
+      return func.apply(this, args);
+    };
+  }
+
+  // Is a given variable an object?
+  function isObject(obj) {
+    var type = typeof obj;
+    return type === 'function' || type === 'object' && !!obj;
+  }
+
+  // Is a given value equal to null?
+  function isNull(obj) {
+    return obj === null;
+  }
+
+  // Is a given variable undefined?
+  function isUndefined(obj) {
+    return obj === void 0;
+  }
+
+  // Is a given value a boolean?
+  function isBoolean(obj) {
+    return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+  }
+
+  // Is a given value a DOM element?
+  function isElement(obj) {
+    return !!(obj && obj.nodeType === 1);
+  }
+
+  // Internal function for creating a `toString`-based type tester.
+  function tagTester(name) {
+    var tag = '[object ' + name + ']';
+    return function(obj) {
+      return toString.call(obj) === tag;
+    };
+  }
+
+  var isString = tagTester('String');
+
+  var isNumber = tagTester('Number');
+
+  var isDate = tagTester('Date');
+
+  var isRegExp = tagTester('RegExp');
+
+  var isError = tagTester('Error');
+
+  var isSymbol = tagTester('Symbol');
+
+  var isArrayBuffer = tagTester('ArrayBuffer');
+
+  var isFunction = tagTester('Function');
+
+  // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old
+  // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
+  var nodelist = root.document && root.document.childNodes;
+  if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
+    isFunction = function(obj) {
+      return typeof obj == 'function' || false;
+    };
+  }
+
+  var isFunction$1 = isFunction;
+
+  var hasObjectTag = tagTester('Object');
+
+  // In IE 10 - Edge 13, `DataView` has string tag `'[object Object]'`.
+  // In IE 11, the most common among them, this problem also applies to
+  // `Map`, `WeakMap` and `Set`.
+  var hasStringTagBug = (
+        supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8)))
+      ),
+      isIE11 = (typeof Map !== 'undefined' && hasObjectTag(new Map));
+
+  var isDataView = tagTester('DataView');
+
+  // In IE 10 - Edge 13, we need a different heuristic
+  // to determine whether an object is a `DataView`.
+  function ie10IsDataView(obj) {
+    return obj != null && isFunction$1(obj.getInt8) && isArrayBuffer(obj.buffer);
+  }
+
+  var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView);
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native `Array.isArray`.
+  var isArray = nativeIsArray || tagTester('Array');
+
+  // Internal function to check whether `key` is an own property name of `obj`.
+  function has(obj, key) {
+    return obj != null && hasOwnProperty.call(obj, key);
+  }
+
+  var isArguments = tagTester('Arguments');
+
+  // Define a fallback version of the method in browsers (ahem, IE < 9), where
+  // there isn't any inspectable "Arguments" type.
+  (function() {
+    if (!isArguments(arguments)) {
+      isArguments = function(obj) {
+        return has(obj, 'callee');
+      };
+    }
+  }());
+
+  var isArguments$1 = isArguments;
+
+  // Is a given object a finite number?
+  function isFinite$1(obj) {
+    return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj));
+  }
+
+  // Is the given value `NaN`?
+  function isNaN$1(obj) {
+    return isNumber(obj) && _isNaN(obj);
+  }
+
+  // Predicate-generating function. Often useful outside of Underscore.
+  function constant(value) {
+    return function() {
+      return value;
+    };
+  }
+
+  // Common internal logic for `isArrayLike` and `isBufferLike`.
+  function createSizePropertyCheck(getSizeProperty) {
+    return function(collection) {
+      var sizeProperty = getSizeProperty(collection);
+      return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
+    }
+  }
+
+  // Internal helper to generate a function to obtain property `key` from `obj`.
+  function shallowProperty(key) {
+    return function(obj) {
+      return obj == null ? void 0 : obj[key];
+    };
+  }
+
+  // Internal helper to obtain the `byteLength` property of an object.
+  var getByteLength = shallowProperty('byteLength');
+
+  // Internal helper to determine whether we should spend extensive checks against
+  // `ArrayBuffer` et al.
+  var isBufferLike = createSizePropertyCheck(getByteLength);
+
+  // Is a given value a typed array?
+  var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;
+  function isTypedArray(obj) {
+    // `ArrayBuffer.isView` is the most future-proof, so use it when available.
+    // Otherwise, fall back on the above regular expression.
+    return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) :
+                  isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
+  }
+
+  var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false);
+
+  // Internal helper to obtain the `length` property of an object.
+  var getLength = shallowProperty('length');
+
+  // Internal helper to create a simple lookup structure.
+  // `collectNonEnumProps` used to depend on `_.contains`, but this led to
+  // circular imports. `emulatedSet` is a one-off solution that only works for
+  // arrays of strings.
+  function emulatedSet(keys) {
+    var hash = {};
+    for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true;
+    return {
+      contains: function(key) { return hash[key]; },
+      push: function(key) {
+        hash[key] = true;
+        return keys.push(key);
+      }
+    };
+  }
+
+  // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't
+  // be iterated by `for key in ...` and thus missed. Extends `keys` in place if
+  // needed.
+  function collectNonEnumProps(obj, keys) {
+    keys = emulatedSet(keys);
+    var nonEnumIdx = nonEnumerableProps.length;
+    var constructor = obj.constructor;
+    var proto = isFunction$1(constructor) && constructor.prototype || ObjProto;
+
+    // Constructor is a special case.
+    var prop = 'constructor';
+    if (has(obj, prop) && !keys.contains(prop)) keys.push(prop);
+
+    while (nonEnumIdx--) {
+      prop = nonEnumerableProps[nonEnumIdx];
+      if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) {
+        keys.push(prop);
+      }
+    }
+  }
+
+  // Retrieve the names of an object's own properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`.
+  function keys(obj) {
+    if (!isObject(obj)) return [];
+    if (nativeKeys) return nativeKeys(obj);
+    var keys = [];
+    for (var key in obj) if (has(obj, key)) keys.push(key);
+    // Ahem, IE < 9.
+    if (hasEnumBug) collectNonEnumProps(obj, keys);
+    return keys;
+  }
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  function isEmpty(obj) {
+    if (obj == null) return true;
+    // Skip the more expensive `toString`-based type checks if `obj` has no
+    // `.length`.
+    var length = getLength(obj);
+    if (typeof length == 'number' && (
+      isArray(obj) || isString(obj) || isArguments$1(obj)
+    )) return length === 0;
+    return getLength(keys(obj)) === 0;
+  }
+
+  // Returns whether an object has a given set of `key:value` pairs.
+  function isMatch(object, attrs) {
+    var _keys = keys(attrs), length = _keys.length;
+    if (object == null) return !length;
+    var obj = Object(object);
+    for (var i = 0; i < length; i++) {
+      var key = _keys[i];
+      if (attrs[key] !== obj[key] || !(key in obj)) return false;
+    }
+    return true;
+  }
+
+  // If Underscore is called as a function, it returns a wrapped object that can
+  // be used OO-style. This wrapper holds altered versions of all functions added
+  // through `_.mixin`. Wrapped objects may be chained.
+  function _(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  }
+
+  _.VERSION = VERSION;
+
+  // Extracts the result from a wrapped and chained object.
+  _.prototype.value = function() {
+    return this._wrapped;
+  };
+
+  // Provide unwrapping proxies for some methods used in engine operations
+  // such as arithmetic and JSON stringification.
+  _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
+
+  _.prototype.toString = function() {
+    return String(this._wrapped);
+  };
+
+  // Internal function to wrap or shallow-copy an ArrayBuffer,
+  // typed array or DataView to a new view, reusing the buffer.
+  function toBufferView(bufferSource) {
+    return new Uint8Array(
+      bufferSource.buffer || bufferSource,
+      bufferSource.byteOffset || 0,
+      getByteLength(bufferSource)
+    );
+  }
+
+  // We use this string twice, so give it a name for minification.
+  var tagDataView = '[object DataView]';
+
+  // Internal recursive comparison function for `_.isEqual`.
+  function eq(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) return a !== 0 || 1 / a === 1 / b;
+    // `null` or `undefined` only equal to itself (strict comparison).
+    if (a == null || b == null) return false;
+    // `NaN`s are equivalent, but non-reflexive.
+    if (a !== a) return b !== b;
+    // Exhaust primitive checks
+    var type = typeof a;
+    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
+    return deepEq(a, b, aStack, bStack);
+  }
+
+  // Internal recursive comparison function for `_.isEqual`.
+  function deepEq(a, b, aStack, bStack) {
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className !== toString.call(b)) return false;
+    // Work around a bug in IE 10 - Edge 13.
+    if (hasStringTagBug && className == '[object Object]' && isDataView$1(a)) {
+      if (!isDataView$1(b)) return false;
+      className = tagDataView;
+    }
+    switch (className) {
+      // These types are compared by value.
+      case '[object RegExp]':
+        // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return '' + a === '' + b;
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive.
+        // Object(NaN) is equivalent to NaN.
+        if (+a !== +a) return +b !== +b;
+        // An `egal` comparison is performed for other numeric values.
+        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a === +b;
+      case '[object Symbol]':
+        return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
+      case '[object ArrayBuffer]':
+      case tagDataView:
+        // Coerce to typed array so we can fall through.
+        return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
+    }
+
+    var areArrays = className === '[object Array]';
+    if (!areArrays && isTypedArray$1(a)) {
+        var byteLength = getByteLength(a);
+        if (byteLength !== getByteLength(b)) return false;
+        if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
+        areArrays = true;
+    }
+    if (!areArrays) {
+      if (typeof a != 'object' || typeof b != 'object') return false;
+
+      // Objects with different constructors are not equivalent, but `Object`s or `Array`s
+      // from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor &&
+                               isFunction$1(bCtor) && bCtor instanceof bCtor)
+                          && ('constructor' in a && 'constructor' in b)) {
+        return false;
+      }
+    }
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+
+    // Initializing stack of traversed objects.
+    // It's done here since we only need them for objects and arrays comparison.
+    aStack = aStack || [];
+    bStack = bStack || [];
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] === a) return bStack[length] === b;
+    }
+
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+
+    // Recursively compare objects and arrays.
+    if (areArrays) {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      length = a.length;
+      if (length !== b.length) return false;
+      // Deep compare the contents, ignoring non-numeric properties.
+      while (length--) {
+        if (!eq(a[length], b[length], aStack, bStack)) return false;
+      }
+    } else {
+      // Deep compare objects.
+      var _keys = keys(a), key;
+      length = _keys.length;
+      // Ensure that both objects contain the same number of properties before comparing deep equality.
+      if (keys(b).length !== length) return false;
+      while (length--) {
+        // Deep compare each member
+        key = _keys[length];
+        if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return true;
+  }
+
+  // Perform a deep comparison to check if two objects are equal.
+  function isEqual(a, b) {
+    return eq(a, b);
+  }
+
+  // Retrieve all the enumerable property names of an object.
+  function allKeys(obj) {
+    if (!isObject(obj)) return [];
+    var keys = [];
+    for (var key in obj) keys.push(key);
+    // Ahem, IE < 9.
+    if (hasEnumBug) collectNonEnumProps(obj, keys);
+    return keys;
+  }
+
+  // Since the regular `Object.prototype.toString` type tests don't work for
+  // some types in IE 11, we use a fingerprinting heuristic instead, based
+  // on the methods. It's not great, but it's the best we got.
+  // The fingerprint method lists are defined below.
+  function ie11fingerprint(methods) {
+    var length = getLength(methods);
+    return function(obj) {
+      if (obj == null) return false;
+      // `Map`, `WeakMap` and `Set` have no enumerable keys.
+      var keys = allKeys(obj);
+      if (getLength(keys)) return false;
+      for (var i = 0; i < length; i++) {
+        if (!isFunction$1(obj[methods[i]])) return false;
+      }
+      // If we are testing against `WeakMap`, we need to ensure that
+      // `obj` doesn't have a `forEach` method in order to distinguish
+      // it from a regular `Map`.
+      return methods !== weakMapMethods || !isFunction$1(obj[forEachName]);
+    };
+  }
+
+  // In the interest of compact minification, we write
+  // each string in the fingerprints only once.
+  var forEachName = 'forEach',
+      hasName = 'has',
+      commonInit = ['clear', 'delete'],
+      mapTail = ['get', hasName, 'set'];
+
+  // `Map`, `WeakMap` and `Set` each have slightly different
+  // combinations of the above sublists.
+  var mapMethods = commonInit.concat(forEachName, mapTail),
+      weakMapMethods = commonInit.concat(mapTail),
+      setMethods = ['add'].concat(commonInit, forEachName, hasName);
+
+  var isMap = isIE11 ? ie11fingerprint(mapMethods) : tagTester('Map');
+
+  var isWeakMap = isIE11 ? ie11fingerprint(weakMapMethods) : tagTester('WeakMap');
+
+  var isSet = isIE11 ? ie11fingerprint(setMethods) : tagTester('Set');
+
+  var isWeakSet = tagTester('WeakSet');
+
+  // Retrieve the values of an object's properties.
+  function values(obj) {
+    var _keys = keys(obj);
+    var length = _keys.length;
+    var values = Array(length);
+    for (var i = 0; i < length; i++) {
+      values[i] = obj[_keys[i]];
+    }
+    return values;
+  }
+
+  // Convert an object into a list of `[key, value]` pairs.
+  // The opposite of `_.object` with one argument.
+  function pairs(obj) {
+    var _keys = keys(obj);
+    var length = _keys.length;
+    var pairs = Array(length);
+    for (var i = 0; i < length; i++) {
+      pairs[i] = [_keys[i], obj[_keys[i]]];
+    }
+    return pairs;
+  }
+
+  // Invert the keys and values of an object. The values must be serializable.
+  function invert(obj) {
+    var result = {};
+    var _keys = keys(obj);
+    for (var i = 0, length = _keys.length; i < length; i++) {
+      result[obj[_keys[i]]] = _keys[i];
+    }
+    return result;
+  }
+
+  // Return a sorted list of the function names available on the object.
+  function functions(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (isFunction$1(obj[key])) names.push(key);
+    }
+    return names.sort();
+  }
+
+  // An internal function for creating assigner functions.
+  function createAssigner(keysFunc, defaults) {
+    return function(obj) {
+      var length = arguments.length;
+      if (defaults) obj = Object(obj);
+      if (length < 2 || obj == null) return obj;
+      for (var index = 1; index < length; index++) {
+        var source = arguments[index],
+            keys = keysFunc(source),
+            l = keys.length;
+        for (var i = 0; i < l; i++) {
+          var key = keys[i];
+          if (!defaults || obj[key] === void 0) obj[key] = source[key];
+        }
+      }
+      return obj;
+    };
+  }
+
+  // Extend a given object with all the properties in passed-in object(s).
+  var extend = createAssigner(allKeys);
+
+  // Assigns a given object with all the own properties in the passed-in
+  // object(s).
+  // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
+  var extendOwn = createAssigner(keys);
+
+  // Fill in a given object with default properties.
+  var defaults = createAssigner(allKeys, true);
+
+  // Create a naked function reference for surrogate-prototype-swapping.
+  function ctor() {
+    return function(){};
+  }
+
+  // An internal function for creating a new object that inherits from another.
+  function baseCreate(prototype) {
+    if (!isObject(prototype)) return {};
+    if (nativeCreate) return nativeCreate(prototype);
+    var Ctor = ctor();
+    Ctor.prototype = prototype;
+    var result = new Ctor;
+    Ctor.prototype = null;
+    return result;
+  }
+
+  // Creates an object that inherits from the given prototype object.
+  // If additional properties are provided then they will be added to the
+  // created object.
+  function create(prototype, props) {
+    var result = baseCreate(prototype);
+    if (props) extendOwn(result, props);
+    return result;
+  }
+
+  // Create a (shallow-cloned) duplicate of an object.
+  function clone(obj) {
+    if (!isObject(obj)) return obj;
+    return isArray(obj) ? obj.slice() : extend({}, obj);
+  }
+
+  // Invokes `interceptor` with the `obj` and then returns `obj`.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  function tap(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  }
+
+  // Normalize a (deep) property `path` to array.
+  // Like `_.iteratee`, this function can be customized.
+  function toPath(path) {
+    return isArray(path) ? path : [path];
+  }
+  _.toPath = toPath;
+
+  // Internal wrapper for `_.toPath` to enable minification.
+  // Similar to `cb` for `_.iteratee`.
+  function toPath$1(path) {
+    return _.toPath(path);
+  }
+
+  // Internal function to obtain a nested property in `obj` along `path`.
+  function deepGet(obj, path) {
+    var length = path.length;
+    for (var i = 0; i < length; i++) {
+      if (obj == null) return void 0;
+      obj = obj[path[i]];
+    }
+    return length ? obj : void 0;
+  }
+
+  // Get the value of the (deep) property on `path` from `object`.
+  // If any property in `path` does not exist or if the value is
+  // `undefined`, return `defaultValue` instead.
+  // The `path` is normalized through `_.toPath`.
+  function get(object, path, defaultValue) {
+    var value = deepGet(object, toPath$1(path));
+    return isUndefined(value) ? defaultValue : value;
+  }
+
+  // Shortcut function for checking if an object has a given property directly on
+  // itself (in other words, not on a prototype). Unlike the internal `has`
+  // function, this public version can also traverse nested properties.
+  function has$1(obj, path) {
+    path = toPath$1(path);
+    var length = path.length;
+    for (var i = 0; i < length; i++) {
+      var key = path[i];
+      if (!has(obj, key)) return false;
+      obj = obj[key];
+    }
+    return !!length;
+  }
+
+  // Keep the identity function around for default iteratees.
+  function identity(value) {
+    return value;
+  }
+
+  // Returns a predicate for checking whether an object has a given set of
+  // `key:value` pairs.
+  function matcher(attrs) {
+    attrs = extendOwn({}, attrs);
+    return function(obj) {
+      return isMatch(obj, attrs);
+    };
+  }
+
+  // Creates a function that, when passed an object, will traverse that object’s
+  // properties down the given `path`, specified as an array of keys or indices.
+  function property(path) {
+    path = toPath$1(path);
+    return function(obj) {
+      return deepGet(obj, path);
+    };
+  }
+
+  // Internal function that returns an efficient (for current engines) version
+  // of the passed-in callback, to be repeatedly applied in other Underscore
+  // functions.
+  function optimizeCb(func, context, argCount) {
+    if (context === void 0) return func;
+    switch (argCount == null ? 3 : argCount) {
+      case 1: return function(value) {
+        return func.call(context, value);
+      };
+      // The 2-argument case is omitted because we’re not using it.
+      case 3: return function(value, index, collection) {
+        return func.call(context, value, index, collection);
+      };
+      case 4: return function(accumulator, value, index, collection) {
+        return func.call(context, accumulator, value, index, collection);
+      };
+    }
+    return function() {
+      return func.apply(context, arguments);
+    };
+  }
+
+  // An internal function to generate callbacks that can be applied to each
+  // element in a collection, returning the desired result — either `_.identity`,
+  // an arbitrary callback, a property matcher, or a property accessor.
+  function baseIteratee(value, context, argCount) {
+    if (value == null) return identity;
+    if (isFunction$1(value)) return optimizeCb(value, context, argCount);
+    if (isObject(value) && !isArray(value)) return matcher(value);
+    return property(value);
+  }
+
+  // External wrapper for our callback generator. Users may customize
+  // `_.iteratee` if they want additional predicate/iteratee shorthand styles.
+  // This abstraction hides the internal-only `argCount` argument.
+  function iteratee(value, context) {
+    return baseIteratee(value, context, Infinity);
+  }
+  _.iteratee = iteratee;
+
+  // The function we call internally to generate a callback. It invokes
+  // `_.iteratee` if overridden, otherwise `baseIteratee`.
+  function cb(value, context, argCount) {
+    if (_.iteratee !== iteratee) return _.iteratee(value, context);
+    return baseIteratee(value, context, argCount);
+  }
+
+  // Returns the results of applying the `iteratee` to each element of `obj`.
+  // In contrast to `_.map` it returns an object.
+  function mapObject(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    var _keys = keys(obj),
+        length = _keys.length,
+        results = {};
+    for (var index = 0; index < length; index++) {
+      var currentKey = _keys[index];
+      results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  }
+
+  // Predicate-generating function. Often useful outside of Underscore.
+  function noop(){}
+
+  // Generates a function for a given object that returns a given property.
+  function propertyOf(obj) {
+    if (obj == null) return noop;
+    return function(path) {
+      return get(obj, path);
+    };
+  }
+
+  // Run a function **n** times.
+  function times(n, iteratee, context) {
+    var accum = Array(Math.max(0, n));
+    iteratee = optimizeCb(iteratee, context, 1);
+    for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+    return accum;
+  }
+
+  // Return a random integer between `min` and `max` (inclusive).
+  function random(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + Math.floor(Math.random() * (max - min + 1));
+  }
+
+  // A (possibly faster) way to get the current timestamp as an integer.
+  var now = Date.now || function() {
+    return new Date().getTime();
+  };
+
+  // Internal helper to generate functions for escaping and unescaping strings
+  // to/from HTML interpolation.
+  function createEscaper(map) {
+    var escaper = function(match) {
+      return map[match];
+    };
+    // Regexes for identifying a key that needs to be escaped.
+    var source = '(?:' + keys(map).join('|') + ')';
+    var testRegexp = RegExp(source);
+    var replaceRegexp = RegExp(source, 'g');
+    return function(string) {
+      string = string == null ? '' : '' + string;
+      return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+    };
+  }
+
+  // Internal list of HTML entities for escaping.
+  var escapeMap = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#x27;',
+    '`': '&#x60;'
+  };
+
+  // Function for escaping strings to HTML interpolation.
+  var _escape = createEscaper(escapeMap);
+
+  // Internal list of HTML entities for unescaping.
+  var unescapeMap = invert(escapeMap);
+
+  // Function for unescaping strings from HTML interpolation.
+  var _unescape = createEscaper(unescapeMap);
+
+  // By default, Underscore uses ERB-style template delimiters. Change the
+  // following template settings to use alternative delimiters.
+  var templateSettings = _.templateSettings = {
+    evaluate: /<%([\s\S]+?)%>/g,
+    interpolate: /<%=([\s\S]+?)%>/g,
+    escape: /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `_.templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'": "'",
+    '\\': '\\',
+    '\r': 'r',
+    '\n': 'n',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
+
+  function escapeChar(match) {
+    return '\\' + escapes[match];
+  }
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  // NB: `oldSettings` only exists for backwards compatibility.
+  function template(text, settings, oldSettings) {
+    if (!settings && oldSettings) settings = oldSettings;
+    settings = defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
+      index = offset + match.length;
+
+      if (escape) {
+        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+      } else if (interpolate) {
+        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+      } else if (evaluate) {
+        source += "';\n" + evaluate + "\n__p+='";
+      }
+
+      // Adobe VMs need the match returned to produce the correct offset.
+      return match;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + 'return __p;\n';
+
+    var render;
+    try {
+      render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled source as a convenience for precompilation.
+    var argument = settings.variable || 'obj';
+    template.source = 'function(' + argument + '){\n' + source + '}';
+
+    return template;
+  }
+
+  // Traverses the children of `obj` along `path`. If a child is a function, it
+  // is invoked with its parent as context. Returns the value of the final
+  // child, or `fallback` if any child is undefined.
+  function result(obj, path, fallback) {
+    path = toPath$1(path);
+    var length = path.length;
+    if (!length) {
+      return isFunction$1(fallback) ? fallback.call(obj) : fallback;
+    }
+    for (var i = 0; i < length; i++) {
+      var prop = obj == null ? void 0 : obj[path[i]];
+      if (prop === void 0) {
+        prop = fallback;
+        i = length; // Ensure we don't continue iterating.
+      }
+      obj = isFunction$1(prop) ? prop.call(obj) : prop;
+    }
+    return obj;
+  }
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  function uniqueId(prefix) {
+    var id = ++idCounter + '';
+    return prefix ? prefix + id : id;
+  }
+
+  // Start chaining a wrapped Underscore object.
+  function chain(obj) {
+    var instance = _(obj);
+    instance._chain = true;
+    return instance;
+  }
+
+  // Internal function to execute `sourceFunc` bound to `context` with optional
+  // `args`. Determines whether to execute a function as a constructor or as a
+  // normal function.
+  function executeBound(sourceFunc, boundFunc, context, callingContext, args) {
+    if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
+    var self = baseCreate(sourceFunc.prototype);
+    var result = sourceFunc.apply(self, args);
+    if (isObject(result)) return result;
+    return self;
+  }
+
+  // Partially apply a function by creating a version that has had some of its
+  // arguments pre-filled, without changing its dynamic `this` context. `_` acts
+  // as a placeholder by default, allowing any combination of arguments to be
+  // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument.
+  var partial = restArguments(function(func, boundArgs) {
+    var placeholder = partial.placeholder;
+    var bound = function() {
+      var position = 0, length = boundArgs.length;
+      var args = Array(length);
+      for (var i = 0; i < length; i++) {
+        args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
+      }
+      while (position < arguments.length) args.push(arguments[position++]);
+      return executeBound(func, bound, this, this, args);
+    };
+    return bound;
+  });
+
+  partial.placeholder = _;
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally).
+  var bind = restArguments(function(func, context, args) {
+    if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function');
+    var bound = restArguments(function(callArgs) {
+      return executeBound(func, bound, context, this, args.concat(callArgs));
+    });
+    return bound;
+  });
+
+  // Internal helper for collection methods to determine whether a collection
+  // should be iterated as an array or as an object.
+  // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
+  // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
+  var isArrayLike = createSizePropertyCheck(getLength);
+
+  // Internal implementation of a recursive `flatten` function.
+  function flatten(input, depth, strict, output) {
+    output = output || [];
+    if (!depth && depth !== 0) {
+      depth = Infinity;
+    } else if (depth <= 0) {
+      return output.concat(input);
+    }
+    var idx = output.length;
+    for (var i = 0, length = getLength(input); i < length; i++) {
+      var value = input[i];
+      if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
+        // Flatten current level of array or arguments object.
+        if (depth > 1) {
+          flatten(value, depth - 1, strict, output);
+          idx = output.length;
+        } else {
+          var j = 0, len = value.length;
+          while (j < len) output[idx++] = value[j++];
+        }
+      } else if (!strict) {
+        output[idx++] = value;
+      }
+    }
+    return output;
+  }
+
+  // Bind a number of an object's methods to that object. Remaining arguments
+  // are the method names to be bound. Useful for ensuring that all callbacks
+  // defined on an object belong to it.
+  var bindAll = restArguments(function(obj, keys) {
+    keys = flatten(keys, false, false);
+    var index = keys.length;
+    if (index < 1) throw new Error('bindAll must be passed function names');
+    while (index--) {
+      var key = keys[index];
+      obj[key] = bind(obj[key], obj);
+    }
+    return obj;
+  });
+
+  // Memoize an expensive function by storing its results.
+  function memoize(func, hasher) {
+    var memoize = function(key) {
+      var cache = memoize.cache;
+      var address = '' + (hasher ? hasher.apply(this, arguments) : key);
+      if (!has(cache, address)) cache[address] = func.apply(this, arguments);
+      return cache[address];
+    };
+    memoize.cache = {};
+    return memoize;
+  }
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  var delay = restArguments(function(func, wait, args) {
+    return setTimeout(function() {
+      return func.apply(null, args);
+    }, wait);
+  });
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  var defer = partial(delay, _, 1);
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time. Normally, the throttled function will run
+  // as much as it can, without ever going more than once per `wait` duration;
+  // but if you'd like to disable the execution on the leading edge, pass
+  // `{leading: false}`. To disable execution on the trailing edge, ditto.
+  function throttle(func, wait, options) {
+    var timeout, context, args, result;
+    var previous = 0;
+    if (!options) options = {};
+
+    var later = function() {
+      previous = options.leading === false ? 0 : now();
+      timeout = null;
+      result = func.apply(context, args);
+      if (!timeout) context = args = null;
+    };
+
+    var throttled = function() {
+      var _now = now();
+      if (!previous && options.leading === false) previous = _now;
+      var remaining = wait - (_now - previous);
+      context = this;
+      args = arguments;
+      if (remaining <= 0 || remaining > wait) {
+        if (timeout) {
+          clearTimeout(timeout);
+          timeout = null;
+        }
+        previous = _now;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      } else if (!timeout && options.trailing !== false) {
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+
+    throttled.cancel = function() {
+      clearTimeout(timeout);
+      previous = 0;
+      timeout = context = args = null;
+    };
+
+    return throttled;
+  }
+
+  // When a sequence of calls of the returned function ends, the argument
+  // function is triggered. The end of a sequence is defined by the `wait`
+  // parameter. If `immediate` is passed, the argument function will be
+  // triggered at the beginning of the sequence instead of at the end.
+  function debounce(func, wait, immediate) {
+    var timeout, result;
+
+    var later = function(context, args) {
+      timeout = null;
+      if (args) result = func.apply(context, args);
+    };
+
+    var debounced = restArguments(function(args) {
+      if (timeout) clearTimeout(timeout);
+      if (immediate) {
+        var callNow = !timeout;
+        timeout = setTimeout(later, wait);
+        if (callNow) result = func.apply(this, args);
+      } else {
+        timeout = delay(later, wait, this, args);
+      }
+
+      return result;
+    });
+
+    debounced.cancel = function() {
+      clearTimeout(timeout);
+      timeout = null;
+    };
+
+    return debounced;
+  }
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  function wrap(func, wrapper) {
+    return partial(wrapper, func);
+  }
+
+  // Returns a negated version of the passed-in predicate.
+  function negate(predicate) {
+    return function() {
+      return !predicate.apply(this, arguments);
+    };
+  }
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  function compose() {
+    var args = arguments;
+    var start = args.length - 1;
+    return function() {
+      var i = start;
+      var result = args[start].apply(this, arguments);
+      while (i--) result = args[i].call(this, result);
+      return result;
+    };
+  }
+
+  // Returns a function that will only be executed on and after the Nth call.
+  function after(times, func) {
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  }
+
+  // Returns a function that will only be executed up to (but not including) the
+  // Nth call.
+  function before(times, func) {
+    var memo;
+    return function() {
+      if (--times > 0) {
+        memo = func.apply(this, arguments);
+      }
+      if (times <= 1) func = null;
+      return memo;
+    };
+  }
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  var once = partial(before, 2);
+
+  // Returns the first key on an object that passes a truth test.
+  function findKey(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var _keys = keys(obj), key;
+    for (var i = 0, length = _keys.length; i < length; i++) {
+      key = _keys[i];
+      if (predicate(obj[key], key, obj)) return key;
+    }
+  }
+
+  // Internal function to generate `_.findIndex` and `_.findLastIndex`.
+  function createPredicateIndexFinder(dir) {
+    return function(array, predicate, context) {
+      predicate = cb(predicate, context);
+      var length = getLength(array);
+      var index = dir > 0 ? 0 : length - 1;
+      for (; index >= 0 && index < length; index += dir) {
+        if (predicate(array[index], index, array)) return index;
+      }
+      return -1;
+    };
+  }
+
+  // Returns the first index on an array-like that passes a truth test.
+  var findIndex = createPredicateIndexFinder(1);
+
+  // Returns the last index on an array-like that passes a truth test.
+  var findLastIndex = createPredicateIndexFinder(-1);
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  function sortedIndex(array, obj, iteratee, context) {
+    iteratee = cb(iteratee, context, 1);
+    var value = iteratee(obj);
+    var low = 0, high = getLength(array);
+    while (low < high) {
+      var mid = Math.floor((low + high) / 2);
+      if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+    }
+    return low;
+  }
+
+  // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions.
+  function createIndexFinder(dir, predicateFind, sortedIndex) {
+    return function(array, item, idx) {
+      var i = 0, length = getLength(array);
+      if (typeof idx == 'number') {
+        if (dir > 0) {
+          i = idx >= 0 ? idx : Math.max(idx + length, i);
+        } else {
+          length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
+        }
+      } else if (sortedIndex && idx && length) {
+        idx = sortedIndex(array, item);
+        return array[idx] === item ? idx : -1;
+      }
+      if (item !== item) {
+        idx = predicateFind(slice.call(array, i, length), isNaN$1);
+        return idx >= 0 ? idx + i : -1;
+      }
+      for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
+        if (array[idx] === item) return idx;
+      }
+      return -1;
+    };
+  }
+
+  // Return the position of the first occurrence of an item in an array,
+  // or -1 if the item is not included in the array.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  var indexOf = createIndexFinder(1, findIndex, sortedIndex);
+
+  // Return the position of the last occurrence of an item in an array,
+  // or -1 if the item is not included in the array.
+  var lastIndexOf = createIndexFinder(-1, findLastIndex);
+
+  // Return the first value which passes a truth test.
+  function find(obj, predicate, context) {
+    var keyFinder = isArrayLike(obj) ? findIndex : findKey;
+    var key = keyFinder(obj, predicate, context);
+    if (key !== void 0 && key !== -1) return obj[key];
+  }
+
+  // Convenience version of a common use case of `_.find`: getting the first
+  // object containing specific `key:value` pairs.
+  function findWhere(obj, attrs) {
+    return find(obj, matcher(attrs));
+  }
+
+  // The cornerstone for collection functions, an `each`
+  // implementation, aka `forEach`.
+  // Handles raw objects in addition to array-likes. Treats all
+  // sparse array-likes as if they were dense.
+  function each(obj, iteratee, context) {
+    iteratee = optimizeCb(iteratee, context);
+    var i, length;
+    if (isArrayLike(obj)) {
+      for (i = 0, length = obj.length; i < length; i++) {
+        iteratee(obj[i], i, obj);
+      }
+    } else {
+      var _keys = keys(obj);
+      for (i = 0, length = _keys.length; i < length; i++) {
+        iteratee(obj[_keys[i]], _keys[i], obj);
+      }
+    }
+    return obj;
+  }
+
+  // Return the results of applying the iteratee to each element.
+  function map(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    var _keys = !isArrayLike(obj) && keys(obj),
+        length = (_keys || obj).length,
+        results = Array(length);
+    for (var index = 0; index < length; index++) {
+      var currentKey = _keys ? _keys[index] : index;
+      results[index] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  }
+
+  // Internal helper to create a reducing function, iterating left or right.
+  function createReduce(dir) {
+    // Wrap code that reassigns argument variables in a separate function than
+    // the one that accesses `arguments.length` to avoid a perf hit. (#1991)
+    var reducer = function(obj, iteratee, memo, initial) {
+      var _keys = !isArrayLike(obj) && keys(obj),
+          length = (_keys || obj).length,
+          index = dir > 0 ? 0 : length - 1;
+      if (!initial) {
+        memo = obj[_keys ? _keys[index] : index];
+        index += dir;
+      }
+      for (; index >= 0 && index < length; index += dir) {
+        var currentKey = _keys ? _keys[index] : index;
+        memo = iteratee(memo, obj[currentKey], currentKey, obj);
+      }
+      return memo;
+    };
+
+    return function(obj, iteratee, memo, context) {
+      var initial = arguments.length >= 3;
+      return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
+    };
+  }
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`.
+  var reduce = createReduce(1);
+
+  // The right-associative version of reduce, also known as `foldr`.
+  var reduceRight = createReduce(-1);
+
+  // Return all the elements that pass a truth test.
+  function filter(obj, predicate, context) {
+    var results = [];
+    predicate = cb(predicate, context);
+    each(obj, function(value, index, list) {
+      if (predicate(value, index, list)) results.push(value);
+    });
+    return results;
+  }
+
+  // Return all the elements for which a truth test fails.
+  function reject(obj, predicate, context) {
+    return filter(obj, negate(cb(predicate)), context);
+  }
+
+  // Determine whether all of the elements pass a truth test.
+  function every(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var _keys = !isArrayLike(obj) && keys(obj),
+        length = (_keys || obj).length;
+    for (var index = 0; index < length; index++) {
+      var currentKey = _keys ? _keys[index] : index;
+      if (!predicate(obj[currentKey], currentKey, obj)) return false;
+    }
+    return true;
+  }
+
+  // Determine if at least one element in the object passes a truth test.
+  function some(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var _keys = !isArrayLike(obj) && keys(obj),
+        length = (_keys || obj).length;
+    for (var index = 0; index < length; index++) {
+      var currentKey = _keys ? _keys[index] : index;
+      if (predicate(obj[currentKey], currentKey, obj)) return true;
+    }
+    return false;
+  }
+
+  // Determine if the array or object contains a given item (using `===`).
+  function contains(obj, item, fromIndex, guard) {
+    if (!isArrayLike(obj)) obj = values(obj);
+    if (typeof fromIndex != 'number' || guard) fromIndex = 0;
+    return indexOf(obj, item, fromIndex) >= 0;
+  }
+
+  // Invoke a method (with arguments) on every item in a collection.
+  var invoke = restArguments(function(obj, path, args) {
+    var contextPath, func;
+    if (isFunction$1(path)) {
+      func = path;
+    } else {
+      path = toPath$1(path);
+      contextPath = path.slice(0, -1);
+      path = path[path.length - 1];
+    }
+    return map(obj, function(context) {
+      var method = func;
+      if (!method) {
+        if (contextPath && contextPath.length) {
+          context = deepGet(context, contextPath);
+        }
+        if (context == null) return void 0;
+        method = context[path];
+      }
+      return method == null ? method : method.apply(context, args);
+    });
+  });
+
+  // Convenience version of a common use case of `_.map`: fetching a property.
+  function pluck(obj, key) {
+    return map(obj, property(key));
+  }
+
+  // Convenience version of a common use case of `_.filter`: selecting only
+  // objects containing specific `key:value` pairs.
+  function where(obj, attrs) {
+    return filter(obj, matcher(attrs));
+  }
+
+  // Return the maximum element (or element-based computation).
+  function max(obj, iteratee, context) {
+    var result = -Infinity, lastComputed = -Infinity,
+        value, computed;
+    if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
+      obj = isArrayLike(obj) ? obj : values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value != null && value > result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = cb(iteratee, context);
+      each(obj, function(v, index, list) {
+        computed = iteratee(v, index, list);
+        if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+          result = v;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  }
+
+  // Return the minimum element (or element-based computation).
+  function min(obj, iteratee, context) {
+    var result = Infinity, lastComputed = Infinity,
+        value, computed;
+    if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
+      obj = isArrayLike(obj) ? obj : values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value != null && value < result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = cb(iteratee, context);
+      each(obj, function(v, index, list) {
+        computed = iteratee(v, index, list);
+        if (computed < lastComputed || computed === Infinity && result === Infinity) {
+          result = v;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  }
+
+  // Sample **n** random values from a collection using the modern version of the
+  // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
+  // If **n** is not specified, returns a single random element.
+  // The internal `guard` argument allows it to work with `_.map`.
+  function sample(obj, n, guard) {
+    if (n == null || guard) {
+      if (!isArrayLike(obj)) obj = values(obj);
+      return obj[random(obj.length - 1)];
+    }
+    var sample = isArrayLike(obj) ? clone(obj) : values(obj);
+    var length = getLength(sample);
+    n = Math.max(Math.min(n, length), 0);
+    var last = length - 1;
+    for (var index = 0; index < n; index++) {
+      var rand = random(index, last);
+      var temp = sample[index];
+      sample[index] = sample[rand];
+      sample[rand] = temp;
+    }
+    return sample.slice(0, n);
+  }
+
+  // Shuffle a collection.
+  function shuffle(obj) {
+    return sample(obj, Infinity);
+  }
+
+  // Sort the object's values by a criterion produced by an iteratee.
+  function sortBy(obj, iteratee, context) {
+    var index = 0;
+    iteratee = cb(iteratee, context);
+    return pluck(map(obj, function(value, key, list) {
+      return {
+        value: value,
+        index: index++,
+        criteria: iteratee(value, key, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index - right.index;
+    }), 'value');
+  }
+
+  // An internal function used for aggregate "group by" operations.
+  function group(behavior, partition) {
+    return function(obj, iteratee, context) {
+      var result = partition ? [[], []] : {};
+      iteratee = cb(iteratee, context);
+      each(obj, function(value, index) {
+        var key = iteratee(value, index, obj);
+        behavior(result, value, key);
+      });
+      return result;
+    };
+  }
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  var groupBy = group(function(result, value, key) {
+    if (has(result, key)) result[key].push(value); else result[key] = [value];
+  });
+
+  // Indexes the object's values by a criterion, similar to `_.groupBy`, but for
+  // when you know that your index values will be unique.
+  var indexBy = group(function(result, value, key) {
+    result[key] = value;
+  });
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  var countBy = group(function(result, value, key) {
+    if (has(result, key)) result[key]++; else result[key] = 1;
+  });
+
+  // Split a collection into two arrays: one whose elements all pass the given
+  // truth test, and one whose elements all do not pass the truth test.
+  var partition = group(function(result, value, pass) {
+    result[pass ? 0 : 1].push(value);
+  }, true);
+
+  // Safely create a real, live array from anything iterable.
+  var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
+  function toArray(obj) {
+    if (!obj) return [];
+    if (isArray(obj)) return slice.call(obj);
+    if (isString(obj)) {
+      // Keep surrogate pair characters together.
+      return obj.match(reStrSymbol);
+    }
+    if (isArrayLike(obj)) return map(obj, identity);
+    return values(obj);
+  }
+
+  // Return the number of elements in a collection.
+  function size(obj) {
+    if (obj == null) return 0;
+    return isArrayLike(obj) ? obj.length : keys(obj).length;
+  }
+
+  // Internal `_.pick` helper function to determine whether `key` is an enumerable
+  // property name of `obj`.
+  function keyInObj(value, key, obj) {
+    return key in obj;
+  }
+
+  // Return a copy of the object only containing the allowed properties.
+  var pick = restArguments(function(obj, keys) {
+    var result = {}, iteratee = keys[0];
+    if (obj == null) return result;
+    if (isFunction$1(iteratee)) {
+      if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]);
+      keys = allKeys(obj);
+    } else {
+      iteratee = keyInObj;
+      keys = flatten(keys, false, false);
+      obj = Object(obj);
+    }
+    for (var i = 0, length = keys.length; i < length; i++) {
+      var key = keys[i];
+      var value = obj[key];
+      if (iteratee(value, key, obj)) result[key] = value;
+    }
+    return result;
+  });
+
+  // Return a copy of the object without the disallowed properties.
+  var omit = restArguments(function(obj, keys) {
+    var iteratee = keys[0], context;
+    if (isFunction$1(iteratee)) {
+      iteratee = negate(iteratee);
+      if (keys.length > 1) context = keys[1];
+    } else {
+      keys = map(flatten(keys, false, false), String);
+      iteratee = function(value, key) {
+        return !contains(keys, key);
+      };
+    }
+    return pick(obj, iteratee, context);
+  });
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N.
+  function initial(array, n, guard) {
+    return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+  }
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. The **guard** check allows it to work with `_.map`.
+  function first(array, n, guard) {
+    if (array == null || array.length < 1) return n == null || guard ? void 0 : [];
+    if (n == null || guard) return array[0];
+    return initial(array, array.length - n);
+  }
+
+  // Returns everything but the first entry of the `array`. Especially useful on
+  // the `arguments` object. Passing an **n** will return the rest N values in the
+  // `array`.
+  function rest(array, n, guard) {
+    return slice.call(array, n == null || guard ? 1 : n);
+  }
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array.
+  function last(array, n, guard) {
+    if (array == null || array.length < 1) return n == null || guard ? void 0 : [];
+    if (n == null || guard) return array[array.length - 1];
+    return rest(array, Math.max(0, array.length - n));
+  }
+
+  // Trim out all falsy values from an array.
+  function compact(array) {
+    return filter(array, Boolean);
+  }
+
+  // Flatten out an array, either recursively (by default), or up to `depth`.
+  // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively.
+  function flatten$1(array, depth) {
+    return flatten(array, depth, false);
+  }
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  var difference = restArguments(function(array, rest) {
+    rest = flatten(rest, true, true);
+    return filter(array, function(value){
+      return !contains(rest, value);
+    });
+  });
+
+  // Return a version of the array that does not contain the specified value(s).
+  var without = restArguments(function(array, otherArrays) {
+    return difference(array, otherArrays);
+  });
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // The faster algorithm will not work with an iteratee if the iteratee
+  // is not a one-to-one function, so providing an iteratee will disable
+  // the faster algorithm.
+  function uniq(array, isSorted, iteratee, context) {
+    if (!isBoolean(isSorted)) {
+      context = iteratee;
+      iteratee = isSorted;
+      isSorted = false;
+    }
+    if (iteratee != null) iteratee = cb(iteratee, context);
+    var result = [];
+    var seen = [];
+    for (var i = 0, length = getLength(array); i < length; i++) {
+      var value = array[i],
+          computed = iteratee ? iteratee(value, i, array) : value;
+      if (isSorted && !iteratee) {
+        if (!i || seen !== computed) result.push(value);
+        seen = computed;
+      } else if (iteratee) {
+        if (!contains(seen, computed)) {
+          seen.push(computed);
+          result.push(value);
+        }
+      } else if (!contains(result, value)) {
+        result.push(value);
+      }
+    }
+    return result;
+  }
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  var union = restArguments(function(arrays) {
+    return uniq(flatten(arrays, true, true));
+  });
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  function intersection(array) {
+    var result = [];
+    var argsLength = arguments.length;
+    for (var i = 0, length = getLength(array); i < length; i++) {
+      var item = array[i];
+      if (contains(result, item)) continue;
+      var j;
+      for (j = 1; j < argsLength; j++) {
+        if (!contains(arguments[j], item)) break;
+      }
+      if (j === argsLength) result.push(item);
+    }
+    return result;
+  }
+
+  // Complement of zip. Unzip accepts an array of arrays and groups
+  // each array's elements on shared indices.
+  function unzip(array) {
+    var length = array && max(array, getLength).length || 0;
+    var result = Array(length);
+
+    for (var index = 0; index < length; index++) {
+      result[index] = pluck(array, index);
+    }
+    return result;
+  }
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  var zip = restArguments(unzip);
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values. Passing by pairs is the reverse of `_.pairs`.
+  function object(list, values) {
+    var result = {};
+    for (var i = 0, length = getLength(list); i < length; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  }
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](https://docs.python.org/library/functions.html#range).
+  function range(start, stop, step) {
+    if (stop == null) {
+      stop = start || 0;
+      start = 0;
+    }
+    if (!step) {
+      step = stop < start ? -1 : 1;
+    }
+
+    var length = Math.max(Math.ceil((stop - start) / step), 0);
+    var range = Array(length);
+
+    for (var idx = 0; idx < length; idx++, start += step) {
+      range[idx] = start;
+    }
+
+    return range;
+  }
+
+  // Chunk a single array into multiple arrays, each containing `count` or fewer
+  // items.
+  function chunk(array, count) {
+    if (count == null || count < 1) return [];
+    var result = [];
+    var i = 0, length = array.length;
+    while (i < length) {
+      result.push(slice.call(array, i, i += count));
+    }
+    return result;
+  }
+
+  // Helper function to continue chaining intermediate results.
+  function chainResult(instance, obj) {
+    return instance._chain ? _(obj).chain() : obj;
+  }
+
+  // Add your own custom functions to the Underscore object.
+  function mixin(obj) {
+    each(functions(obj), function(name) {
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return chainResult(this, func.apply(_, args));
+      };
+    });
+    return _;
+  }
+
+  // Add all mutator `Array` functions to the wrapper.
+  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      if (obj != null) {
+        method.apply(obj, arguments);
+        if ((name === 'shift' || name === 'splice') && obj.length === 0) {
+          delete obj[0];
+        }
+      }
+      return chainResult(this, obj);
+    };
+  });
+
+  // Add all accessor `Array` functions to the wrapper.
+  each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      if (obj != null) obj = method.apply(obj, arguments);
+      return chainResult(this, obj);
+    };
+  });
+
+  // Named Exports
+
+  var allExports = {
+    __proto__: null,
+    VERSION: VERSION,
+    restArguments: restArguments,
+    isObject: isObject,
+    isNull: isNull,
+    isUndefined: isUndefined,
+    isBoolean: isBoolean,
+    isElement: isElement,
+    isString: isString,
+    isNumber: isNumber,
+    isDate: isDate,
+    isRegExp: isRegExp,
+    isError: isError,
+    isSymbol: isSymbol,
+    isArrayBuffer: isArrayBuffer,
+    isDataView: isDataView$1,
+    isArray: isArray,
+    isFunction: isFunction$1,
+    isArguments: isArguments$1,
+    isFinite: isFinite$1,
+    isNaN: isNaN$1,
+    isTypedArray: isTypedArray$1,
+    isEmpty: isEmpty,
+    isMatch: isMatch,
+    isEqual: isEqual,
+    isMap: isMap,
+    isWeakMap: isWeakMap,
+    isSet: isSet,
+    isWeakSet: isWeakSet,
+    keys: keys,
+    allKeys: allKeys,
+    values: values,
+    pairs: pairs,
+    invert: invert,
+    functions: functions,
+    methods: functions,
+    extend: extend,
+    extendOwn: extendOwn,
+    assign: extendOwn,
+    defaults: defaults,
+    create: create,
+    clone: clone,
+    tap: tap,
+    get: get,
+    has: has$1,
+    mapObject: mapObject,
+    identity: identity,
+    constant: constant,
+    noop: noop,
+    toPath: toPath,
+    property: property,
+    propertyOf: propertyOf,
+    matcher: matcher,
+    matches: matcher,
+    times: times,
+    random: random,
+    now: now,
+    escape: _escape,
+    unescape: _unescape,
+    templateSettings: templateSettings,
+    template: template,
+    result: result,
+    uniqueId: uniqueId,
+    chain: chain,
+    iteratee: iteratee,
+    partial: partial,
+    bind: bind,
+    bindAll: bindAll,
+    memoize: memoize,
+    delay: delay,
+    defer: defer,
+    throttle: throttle,
+    debounce: debounce,
+    wrap: wrap,
+    negate: negate,
+    compose: compose,
+    after: after,
+    before: before,
+    once: once,
+    findKey: findKey,
+    findIndex: findIndex,
+    findLastIndex: findLastIndex,
+    sortedIndex: sortedIndex,
+    indexOf: indexOf,
+    lastIndexOf: lastIndexOf,
+    find: find,
+    detect: find,
+    findWhere: findWhere,
+    each: each,
+    forEach: each,
+    map: map,
+    collect: map,
+    reduce: reduce,
+    foldl: reduce,
+    inject: reduce,
+    reduceRight: reduceRight,
+    foldr: reduceRight,
+    filter: filter,
+    select: filter,
+    reject: reject,
+    every: every,
+    all: every,
+    some: some,
+    any: some,
+    contains: contains,
+    includes: contains,
+    include: contains,
+    invoke: invoke,
+    pluck: pluck,
+    where: where,
+    max: max,
+    min: min,
+    shuffle: shuffle,
+    sample: sample,
+    sortBy: sortBy,
+    groupBy: groupBy,
+    indexBy: indexBy,
+    countBy: countBy,
+    partition: partition,
+    toArray: toArray,
+    size: size,
+    pick: pick,
+    omit: omit,
+    first: first,
+    head: first,
+    take: first,
+    initial: initial,
+    last: last,
+    rest: rest,
+    tail: rest,
+    drop: rest,
+    compact: compact,
+    flatten: flatten$1,
+    without: without,
+    uniq: uniq,
+    unique: uniq,
+    union: union,
+    intersection: intersection,
+    difference: difference,
+    unzip: unzip,
+    transpose: unzip,
+    zip: zip,
+    object: object,
+    range: range,
+    chunk: chunk,
+    mixin: mixin,
+    'default': _
+  };
+
+  // Default Export
+
+  // Add all of the Underscore functions to the wrapper object.
+  var _$1 = mixin(allExports);
+  // Legacy Node.js API.
+  _$1._ = _$1;
+
+  return _$1;
+
+})));
+
+
+}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],3:[function(require,module,exports){
+window.jsonform = require('jsonform');
+
+},{"jsonform":1}]},{},[3]);

File diff suppressed because it is too large
+ 11212 - 12737
bundle/mssql.js


File diff suppressed because it is too large
+ 497 - 38
package-lock.json


+ 30 - 0
package.json

@@ -0,0 +1,30 @@
+{
+  "name": "browserified",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "dependencies": {
+    "foreach-cli": "^1.8.1",
+    "jsonform": "^2.2.1",
+    "node-mssql": "^0.0.1"
+  },
+  "devDependencies": {
+    "browserify": "^17.0.0",
+    "js-yaml": "^3.14.0",
+    "json-2-csv": "^3.8.0",
+    "json2csv": "^5.0.5",
+    "minify": "^6.0.1",
+    "mssql": "^6.2.3",
+    "tinyify": "^3.0.0"
+  },
+  "scripts": {
+    "build": "./node_modules/.bin/foreach -g \"src/*\" -x \"./node_modules/.bin/browserify #{path} -o bundle/#{base}\"",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "ssh://git@git.tsimnet.eu:24/dasimmet/browserified.git"
+  },
+  "author": "",
+  "license": "ISC"
+}

+ 1 - 0
src/jsonform.js

@@ -0,0 +1 @@
+window.jsonform = require('jsonform');

Some files were not shown because too many files changed in this diff