Skip to main content

JSONFORM_JS

Constant JSONFORM_JS 

Source
pub const JSONFORM_JS: &[u8] = b"/* Copyright (c) 2012 Joshfire - MIT license */\n/**\n * @fileoverview Core of the JSON Form client-side library.\n *\n * Generates an HTML form from a structured data model and a layout description.\n *\n * The library may also validate inputs entered by the user against the data model\n * upon form submission and create the structured data object initialized with the\n * values that were submitted.\n *\n * The library depends on:\n *  - jQuery\n *  - the underscore library\n *  - a JSON parser/serializer. Nothing to worry about in modern browsers.\n *  - the JSONFormValidation library (in jsv.js) for validation purpose\n *\n * See documentation at:\n * http://developer.joshfire.com/doc/dev/ref/jsonform\n *\n * The library creates and maintains an internal data tree along with the DOM.\n * That structure is necessary to handle arrays (and nested arrays!) that are\n * dynamic by essence.\n */\n\n /*global window*/\n\n(function(serverside, global, $, _, JSON) {\n  if (serverside && !_) {\n    _ = require(\'underscore\');\n  }\n\n  /**\n   * Regular expressions used to extract array indexes in input field names\n   */\n  var reArray = /\\[([0-9]*)\\](?=\\[|\\.|$)/g;\n\n  /**\n   * Template settings for form views\n   */\n  var fieldTemplateSettings = {\n    evaluate    : /<%([\\s\\S]+?)%>/g,\n    interpolate : /<%=([\\s\\S]+?)%>/g\n  };\n\n  /**\n   * Template settings for value replacement\n   */\n  var valueTemplateSettings = {\n    evaluate    : /\\{\\[([\\s\\S]+?)\\]\\}/g,\n    interpolate : /\\{\\{([\\s\\S]+?)\\}\\}/g\n  };\n\n  /**\n   * Returns true if given value is neither \"undefined\" nor null\n   */\n  var isSet = function (value) {\n    return !(_.isUndefined(value) || _.isNull(value));\n  };\n\n  /**\n   * Returns true if given property is directly property of an object\n   */\n  var hasOwnProperty = function (obj, prop) {\n    return typeof obj === \'object\' && obj.hasOwnProperty(prop);\n  }\n\n  /**\n   * The jsonform object whose methods will be exposed to the window object\n   */\n  var jsonform = {util:{}};\n\n\n  // From backbonejs\n  var escapeHTML = function (string) {\n    if (!isSet(string)) {\n      return \'\';\n    }\n    string = \'\' + string;\n    if (!string) {\n      return \'\';\n    }\n    return string\n      .replace(/&(?!\\w+;|#\\d+;|#x[\\da-f]+;)/gi, \'&amp;\')\n      .replace(/</g, \'&lt;\')\n      .replace(/>/g, \'&gt;\')\n      .replace(/\"/g, \'&quot;\')\n      .replace(/\'/g, \'&#x27;\')\n      .replace(/\\//g, \'&#x2F;\');\n  };\n\n/**\n * Escapes selector name for use with jQuery\n *\n * All meta-characters listed in jQuery doc are escaped:\n * http://api.jquery.com/category/selectors/\n *\n * @function\n * @param {String} selector The jQuery selector to escape\n * @return {String} The escaped selector.\n */\nvar escapeSelector = function (selector) {\n  return selector.replace(/([ \\!\\\"\\#\\$\\%\\&\\\'\\(\\)\\*\\+\\,\\.\\/\\:\\;<\\=\\>\\?\\@\\[\\\\\\]\\^\\`\\{\\|\\}\\~])/g, \'\\\\$1\');\n};\n\n/**\n *\n * Slugifies a string by replacing spaces with _. Used to create\n * valid classnames and ids for the form.\n *\n * @function\n * @param {String} str The string to slugify\n * @return {String} The slugified string.\n */\nvar slugify = function(str) {\n  return str.replace(/\\ /g, \'_\');\n}\n\n/**\n * Initializes tabular sections in forms. Such sections are generated by the\n * \'selectfieldset\' type of elements in JSON Form.\n *\n * Input fields that are not visible are automatically disabled\n * not to appear in the submitted form. That\'s on purpose, as tabs\n * are meant to convey an alternative (and not a sequence of steps).\n *\n * The tabs menu is not rendered as tabs but rather as a select field because\n * it\'s easier to grasp that it\'s an alternative.\n *\n * Code based on bootstrap-tabs.js, updated to:\n * - react to option selection instead of tab click\n * - disable input fields in non visible tabs\n * - disable the possibility to have dropdown menus (no meaning here)\n * - act as a regular function instead of as a jQuery plug-in.\n *\n * @function\n * @param {Object} tabs jQuery object that contains the tabular sections\n *  to initialize. The object may reference more than one element.\n */\nvar initializeTabs = function (tabs) {\n  var activate = function (element, container) {\n    container\n      .find(\'> .active\')\n      .removeClass(\'active\');\n    element.addClass(\'active\');\n  };\n\n  var enableFields = function ($target, targetIndex) {\n    // Enable all fields in the targeted tab\n    $target.find(\'input, textarea, select\').removeAttr(\'disabled\');\n\n    // Disable all fields in other tabs\n    $target.parent()\n      .children(\':not([data-idx=\' + targetIndex + \'])\')\n      .find(\'input, textarea, select\')\n      .attr(\'disabled\', \'disabled\');\n  };\n\n  var optionSelected = function (e) {\n    var $option = $(\"option:selected\", $(this)),\n      $select = $(this),\n      // do not use .attr() as it sometimes unexplicably fails\n      targetIdx = $option.get(0).getAttribute(\'data-idx\') || $option.attr(\'value\'),\n      $target;\n\n    e.preventDefault();\n    if ($option.hasClass(\'active\')) {\n      return;\n    }\n\n    $target = $(this).parents(\'.tabbable\').eq(0).find(\'> .tab-content > [data-idx=\' + targetIdx + \']\');\n\n    activate($option, $select);\n    activate($target, $target.parent());\n    enableFields($target, targetIdx);\n  };\n\n  var tabClicked = function (e) {\n    var $a = $(\'a\', $(this));\n    var $content = $(this).parents(\'.tabbable\').first()\n      .find(\'.tab-content\').first();\n    var targetIdx = $(this).index();\n    // The `>` here is to prevent activating selectfieldsets inside a tabarray\n    var $target = $content.find(\'> [data-idx=\' + targetIdx + \']\');\n\n    e.preventDefault();\n    activate($(this), $(this).parent());\n    activate($target, $target.parent());\n    if ($(this).parent().hasClass(\'jsonform-alternative\')) {\n      enableFields($target, targetIdx);\n    }\n  };\n\n  tabs.each(function () {\n    $(this).delegate(\'select.nav\', \'change\', optionSelected);\n    $(this).find(\'select.nav\').each(function () {\n      $(this).val($(this).find(\'.active\').attr(\'value\'));\n      // do not use .attr() as it sometimes unexplicably fails\n      var targetIdx = $(this).find(\'option:selected\').get(0).getAttribute(\'data-idx\') ||\n        $(this).find(\'option:selected\').attr(\'value\');\n      var $target = $(this).parents(\'.tabbable\').eq(0).find(\'> .tab-content > [data-idx=\' + targetIdx + \']\');\n      enableFields($target, targetIdx);\n    });\n\n    $(this).delegate(\'ul.nav li\', \'click\', tabClicked);\n    $(this).find(\'ul.nav li.active\').click();\n  });\n};\n\n\n// Twitter bootstrap-friendly HTML boilerplate for standard inputs\njsonform.fieldTemplate = function(inner) {\n  return \'<div \' +\n    \'<% for(var key in elt.htmlMetaData) {%>\' +\n      \'<%= key %>=\"<%= elt.htmlMetaData[key] %>\" \' +\n    \'<% }%>\' +\n    \'class=\"form-group jsonform-error-<%= keydash %>\' +\n    \'<%= elt.htmlClass ? \" \" + elt.htmlClass : \"\" %>\' +\n    \'<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== \"boolean\") ? \" jsonform-required\" : \"\") %>\' +\n    \'<%= (node.readOnly ? \" jsonform-readonly\" : \"\") %>\' +\n    \'<%= (node.disabled ? \" jsonform-disabled\" : \"\") %>\' +\n    \'\">\' +\n    \'<% if (!elt.notitle) { %>\' +\n      \'<label for=\"<%= node.id %>\"><%= node.title ? node.title : node.name %></label>\' +\n    \'<% } %>\' +\n    \'<div class=\"controls\">\' +\n      \'<% if (node.prepend || node.append) { %>\' +\n      \'<div class=\"<% if (node.prepend) { %>input-group<% } %>\' +\n        \'<% if (node.append) { %> input-group<% } %>\">\' +\n        \'<% if (node.prepend) { %>\' +\n          \'<span class=\"input-group-addon\"><%= node.prepend %></span>\' +\n        \'<% } %>\' +\n      \'<% } %>\' +\n      inner +\n      \'<% if (node.append) { %>\' +\n        \'<span class=\"input-group-addon\"><%= node.append %></span>\' +\n      \'<% } %>\' +\n      \'<% if (node.prepend || node.append) { %>\' +\n        \'</div>\' +\n      \'<% } %>\' +\n      \'<% if (node.description) { %>\' +\n        \'<span class=\"help-block\"><%= node.description %></span>\' +\n      \'<% } %>\' +\n      \'<span class=\"help-block jsonform-errortext\" style=\"display:none;\"></span>\' +\n    \'</div></div>\';\n};\n\nvar fileDisplayTemplate = \'<div class=\"_jsonform-preview\">\' +\n  \'<% if (value.type==\"image\") { %>\' +\n  \'<img class=\"jsonform-preview\" id=\"jsonformpreview-<%= id %>\" src=\"<%= value.url %>\" />\' +\n  \'<% } else { %>\' +\n  \'<a href=\"<%= value.url %>\"><%= value.name %></a> (<%= Math.ceil(value.size/1024) %>kB)\' +\n  \'<% } %>\' +\n  \'</div>\' +\n  \'<a href=\"#\" class=\"btn btn-default _jsonform-delete\"><i class=\"glyphicon glyphicon-remove\" title=\"Remove\"></i></a> \';\n\nvar inputFieldTemplate = function (type) {\n  return {\n    \'template\': \'<input type=\"\' + type + \'\" \' +\n      \'class=\\\'form-control<%= (fieldHtmlClass ? \" \" + fieldHtmlClass : \"\") %>\\\'\' +\n      \'name=\"<%= node.name %>\" value=\"<%= escape(value) %>\" id=\"<%= id %>\"\' +\n      \' aria-label=\"<%= node.title ? escape(node.title) : node.name %>\"\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.readOnly ? \" readonly=\\\'readonly\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && (node.schemaElement.step > 0 || node.schemaElement.step == \"any\") ? \" step=\\\'\" + node.schemaElement.step + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.minLength ? \" minlength=\\\'\" + node.schemaElement.minLength + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.maxLength ? \" maxlength=\\\'\" + node.schemaElement.maxLength + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== \"boolean\") ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \'<%= (node.placeholder? \" placeholder=\" + \\\'\"\\\' + escape(node.placeholder) + \\\'\"\\\' : \"\")%>\' +\n      \' />\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true\n  }\n};\n\njsonform.elementTypes = {\n  \'none\': {\n    \'template\': \'\'\n  },\n  \'root\': {\n    \'template\': \'<div><%= children %></div>\'\n  },\n  \'text\': inputFieldTemplate(\'text\'),\n  \'password\': inputFieldTemplate(\'password\'),\n  \'date\': inputFieldTemplate(\'date\'),\n  \'datetime\': inputFieldTemplate(\'datetime\'),\n  \'datetime-local\': inputFieldTemplate(\'datetime-local\'),\n  \'email\': inputFieldTemplate(\'email\'),\n  \'month\': inputFieldTemplate(\'month\'),\n  \'number\': inputFieldTemplate(\'number\'),\n  \'search\': inputFieldTemplate(\'search\'),\n  \'tel\': inputFieldTemplate(\'tel\'),\n  \'time\': inputFieldTemplate(\'time\'),\n  \'url\': inputFieldTemplate(\'url\'),\n  \'week\': inputFieldTemplate(\'week\'),\n  \'range\': {\n    \'template\': \'<div class=\"range\"><input type=\"range\" \' +\n      \'<%= (fieldHtmlClass ? \"class=\\\'\" + fieldHtmlClass + \"\\\' \" : \"\") %>\' +\n      \'name=\"<%= node.name %>\" value=\"<%= escape(value) %>\" id=\"<%= id %>\"\' +\n      \' aria-label=\"<%= node.title ? escape(node.title) : node.name %>\"\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \' min=<%= range.min %>\' +\n      \' max=<%= range.max %>\' +\n      \' step=<%= range.step %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \' /><% if (range.indicator) { %><span class=\"range-value\" rel=\"<%= id %>\"><%= escape(value) %></span><% } %></div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onInput\': function(evt, elt) {\n      const valueIndicator = document.querySelector(\'span.range-value[rel=\"\' + elt.id + \'\"]\');\n      if (valueIndicator) {\n        valueIndicator.innerText = evt.target.value;\n      }\n    },\n    \'onBeforeRender\': function (data, node) {\n      data.range = {\n        min: 1,\n        max: 100,\n        step: 1,\n        indicator: false\n      };\n      if (!node || !node.schemaElement) return;\n      if (node.formElement && node.formElement.step) {\n        data.range.step = node.formElement.step;\n      }\n      if (node.formElement && node.formElement.indicator) {\n        data.range.indicator = node.formElement.indicator;\n      }\n      if (typeof node.schemaElement.minimum !== \'undefined\') {\n        if (node.schemaElement.exclusiveMinimum) {\n          data.range.min = node.schemaElement.minimum + data.range.step;\n        }\n        else {\n          data.range.min = node.schemaElement.minimum;\n        }\n      }\n      if (typeof node.schemaElement.maximum !== \'undefined\') {\n        if (node.schemaElement.exclusiveMaximum) {\n          data.range.max = node.schemaElement.maximum - data.range.step;\n        }\n        else {\n          data.range.max = node.schemaElement.maximum;\n        }\n      }\n    }\n  },\n  \'color\':{\n    \'template\':\'<input type=\"text\" \' +\n      \'<%= (fieldHtmlClass ? \"class=\\\'\" + fieldHtmlClass + \"\\\' \" : \"\") %>\' +\n      \'name=\"<%= node.name %>\" value=\"<%= escape(value) %>\" id=\"<%= id %>\"\' +\n      \' aria-label=\"<%= node.title ? escape(node.title) : node.name %>\"\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \' />\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onInsert\': function(evt, node) {\n      $(node.el).find(\'#\' + escapeSelector(node.id)).spectrum({\n        preferredFormat: \"hex\",\n        showInput: true\n      });\n    }\n  },\n  \'textarea\':{\n    \'template\':\'<textarea id=\"<%= id %>\" name=\"<%= node.name %>\" \' +\n      \'<%= (fieldHtmlClass ? \"class=\\\'\" + fieldHtmlClass + \"\\\' \" : \"\") %>\' +\n      \'style=\"height:<%= elt.height || \"150px\" %>;width:<%= elt.width || \"100%\" %>;\"\' +\n      \' aria-label=\"<%= node.title ? escape(node.title) : node.name %>\"\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.readOnly ? \" readonly=\\\'readonly\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.minLength ? \" minlength=\\\'\" + node.schemaElement.minLength + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.maxLength ? \" maxlength=\\\'\" + node.schemaElement.maxLength + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \'<%= (node.placeholder? \" placeholder=\" + \\\'\"\\\' + escape(node.placeholder) + \\\'\"\\\' : \"\")%>\' +\n      \'><%= value %></textarea>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true\n  },\n  \'wysihtml5\':{\n    \'template\':\'<textarea id=\"<%= id %>\" name=\"<%= node.name %>\" style=\"height:<%= elt.height || \"300px\" %>;width:<%= elt.width || \"100%\" %>;\"\' +\n      \' aria-label=\"<%= node.title ? escape(node.title) : node.name %>\"\' +\n      \'<%= (fieldHtmlClass ? \"class=\\\'\" + fieldHtmlClass + \"\\\' \" : \"\") %>\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.readOnly ? \" readonly=\\\'readonly\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.minLength ? \" minlength=\\\'\" + node.schemaElement.minLength + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.maxLength ? \" maxlength=\\\'\" + node.schemaElement.maxLength + \"\\\'\" : \"\") %>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \'<%= (node.placeholder? \" placeholder=\" + \\\'\"\\\' + escape(node.placeholder) + \\\'\"\\\' : \"\")%>\' +\n      \'><%= value %></textarea>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onInsert\': function (evt, node) {\n      var setup = function () {\n        //protect from double init\n        if ($(node.el).data(\"wysihtml5\")) return;\n        $(node.el).data(\"wysihtml5_loaded\",true);\n\n        $(node.el).find(\'#\' + escapeSelector(node.id)).wysihtml5({\n          \"html\": true,\n          \"link\": true,\n          \"font-styles\":true,\n          \"image\": false,\n          \"events\": {\n            \"load\": function () {\n              // In chrome, if an element is required and hidden, it leads to\n              // the error \'An invalid form control with name=\'\' is not focusable\'\n              // See http://stackoverflow.com/questions/7168645/invalid-form-control-only-in-google-chrome\n              $(this.textareaElement).removeAttr(\'required\');\n            }\n          }\n        });\n      };\n\n      // Is there a setup hook?\n      if (window.jsonform_wysihtml5_setup) {\n        window.jsonform_wysihtml5_setup(setup);\n        return;\n      }\n\n      // Wait until wysihtml5 is loaded\n      var itv = window.setInterval(function() {\n        if (window.wysihtml5) {\n          window.clearInterval(itv);\n          setup();\n        }\n      },1000);\n    }\n  },\n  \'ace\':{\n    \'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>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onInsert\': function (evt, node) {\n      var setup = function () {\n        var formElement = node.formElement || {};\n        var ace = window.ace;\n        var editor = ace.edit($(node.el).find(\'#\' + escapeSelector(node.id) + \'__ace\').get(0));\n        var idSelector = \'#\' + escapeSelector(node.id) + \'__hidden\';\n        // Force editor to use \"\\n\" for new lines, not to bump into ACE \"\\r\" conversion issue\n        // (ACE is ok with \"\\r\" on pasting but fails to return \"\\r\" when value is extracted)\n        editor.getSession().setNewLineMode(\'unix\');\n        editor.renderer.setShowPrintMargin(false);\n        editor.setTheme(\"ace/theme/\"+(formElement.aceTheme||\"twilight\"));\n\n        if (formElement.aceMode) {\n          editor.getSession().setMode(\"ace/mode/\"+formElement.aceMode);\n        }\n        editor.getSession().setTabSize(2);\n\n        // Set the contents of the initial manifest file\n        editor.getSession().setValue(node.value||\"\");\n\n        //TODO: this is clearly sub-optimal\n        // \'Lazily\' bind to the onchange \'ace\' event to give\n        // priority to user edits\n        var lazyChanged = _.debounce(function () {\n          $(node.el).find(idSelector).val(editor.getSession().getValue());\n          $(node.el).find(idSelector).change();\n        }, 600);\n        editor.getSession().on(\'change\', lazyChanged);\n\n        editor.on(\'blur\', function() {\n          $(node.el).find(idSelector).change();\n          $(node.el).find(idSelector).trigger(\"blur\");\n        });\n        editor.on(\'focus\', function() {\n          $(node.el).find(idSelector).trigger(\"focus\");\n        });\n      };\n\n      // Is there a setup hook?\n      if (window.jsonform_ace_setup) {\n        window.jsonform_ace_setup(setup);\n        return;\n      }\n\n      // Wait until ACE is loaded\n      var itv = window.setInterval(function() {\n        if (window.ace) {\n          window.clearInterval(itv);\n          setup();\n        }\n      },1000);\n    }\n  },\n  \'checkbox\':{\n    \'template\': \'<div class=\"checkbox\"><label><input type=\"checkbox\" id=\"<%= id %>\" \' +\n      \'<%= (fieldHtmlClass ? \" class=\\\'\" + fieldHtmlClass + \"\\\'\": \"\") %>\' +\n      \'name=\"<%= node.name %>\" value=\"1\" <% if (value) {%>checked<% } %>\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== \"boolean\") ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \' /><%= node.inlinetitle || \"\" %>\' +\n      \'</label></div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    }\n  },\n  \'file\':{\n    \'template\':\'<input class=\"input-file\" id=\"<%= id %>\" name=\"<%= node.name %>\" type=\"file\" \' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \'<%= (node.formElement && node.formElement.accept ? (\" accept=\\\'\" + node.formElement.accept + \"\\\'\") : \"\") %>\' +\n      \'/>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true\n  },\n  \'file-hosted-public\':{\n    \'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>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'onBeforeRender\': function (data, node) {\n\n      if (!node.ownerTree._transloadit_generic_public_index) {\n        node.ownerTree._transloadit_generic_public_index=1;\n      } else {\n        node.ownerTree._transloadit_generic_public_index++;\n      }\n\n      data.transloaditname = \"_transloadit_jsonform_genericupload_public_\"+node.ownerTree._transloadit_generic_public_index;\n\n      if (!node.ownerTree._transloadit_generic_elts) node.ownerTree._transloadit_generic_elts = {};\n      node.ownerTree._transloadit_generic_elts[data.transloaditname] = node;\n    },\n    \'onChange\': function(evt,elt) {\n      // The \"transloadit\" function should be called only once to enable\n      // the service when the form is submitted. Has it already been done?\n      if (elt.ownerTree._transloadit_bound) {\n        return false;\n      }\n      elt.ownerTree._transloadit_bound = true;\n\n      // Call the \"transloadit\" function on the form element\n      var formElt = $(elt.ownerTree.domRoot);\n      formElt.transloadit({\n        autoSubmit: false,\n        wait: true,\n        onSuccess: function (assembly) {\n          // Image has been uploaded. Check the \"results\" property that\n          // contains the list of files that Transloadit produced. There\n          // should be one image per file input in the form at most.\n          var results = _.values(assembly.results);\n          results = _.flatten(results);\n          _.each(results, function (result) {\n            // Save the assembly result in the right hidden input field\n            var id = elt.ownerTree._transloadit_generic_elts[result.field].id;\n            var input = formElt.find(\'#\' + escapeSelector(id));\n            var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {\n              return !!isSet(result.meta[key]);\n            });\n            result.meta = _.pick(result.meta, nonEmptyKeys);\n            input.val(JSON.stringify(result));\n          });\n\n          // Unbind transloadit from the form\n          elt.ownerTree._transloadit_bound = false;\n          formElt.unbind(\'submit.transloadit\');\n\n          // Submit the form on next tick\n          _.delay(function () {\n            elt.ownerTree.submit();\n          }, 10);\n        },\n        onError: function (assembly) {\n          // TODO: report the error to the user\n          console.log(\'assembly error\', assembly);\n        }\n      });\n    },\n    \'onInsert\': function (evt, node) {\n      $(node.el).find(\'a._jsonform-delete\').on(\'click\', function (evt) {\n        $(node.el).find(\'._jsonform-preview\').remove();\n        $(node.el).find(\'a._jsonform-delete\').remove();\n        $(node.el).find(\'#\' + escapeSelector(node.id)).val(\'\');\n        evt.preventDefault();\n        return false;\n      });\n    },\n    \'onSubmit\':function(evt, elt) {\n      if (elt.ownerTree._transloadit_bound) {\n        return false;\n      }\n      return true;\n    }\n\n  },\n  \'file-transloadit\': {\n    \'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>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'onChange\': function (evt, elt) {\n      // The \"transloadit\" function should be called only once to enable\n      // the service when the form is submitted. Has it already been done?\n      if (elt.ownerTree._transloadit_bound) {\n        return false;\n      }\n      elt.ownerTree._transloadit_bound = true;\n\n      // Call the \"transloadit\" function on the form element\n      var formElt = $(elt.ownerTree.domRoot);\n      formElt.transloadit({\n        autoSubmit: false,\n        wait: true,\n        onSuccess: function (assembly) {\n          // Image has been uploaded. Check the \"results\" property that\n          // contains the list of files that Transloadit produced. Note\n          // JSONForm only supports 1-to-1 associations, meaning it\n          // expects the \"results\" property to contain only one image\n          // per file input in the form.\n          var results = _.values(assembly.results);\n          results = _.flatten(results);\n          _.each(results, function (result) {\n            // Save the assembly result in the right hidden input field\n            var input = formElt.find(\'input[name=\"\' +\n              result.field.replace(/^_transloadit_/, \'\') +\n              \'\"]\');\n            var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {\n              return !!isSet(result.meta[key]);\n            });\n            result.meta = _.pick(result.meta, nonEmptyKeys);\n            input.val(JSON.stringify(result));\n          });\n\n          // Unbind transloadit from the form\n          elt.ownerTree._transloadit_bound = false;\n          formElt.unbind(\'submit.transloadit\');\n\n          // Submit the form on next tick\n          _.delay(function () {\n            elt.ownerTree.submit();\n          }, 10);\n        },\n        onError: function (assembly) {\n          // TODO: report the error to the user\n          console.log(\'assembly error\', assembly);\n        }\n      });\n    },\n    \'onInsert\': function (evt, node) {\n      $(node.el).find(\'a._jsonform-delete\').on(\'click\', function (evt) {\n        $(node.el).find(\'._jsonform-preview\').remove();\n        $(node.el).find(\'a._jsonform-delete\').remove();\n        $(node.el).find(\'#\' + escapeSelector(node.id)).val(\'\');\n        evt.preventDefault();\n        return false;\n      });\n    },\n    \'onSubmit\': function (evt, elt) {\n      if (elt.ownerTree._transloadit_bound) {\n        return false;\n      }\n      return true;\n    }\n  },\n  \'select\':{\n    \'template\':\'<select name=\"<%= node.name %>\" id=\"<%= id %>\"\' +\n      \'class=\\\'form-control<%= (fieldHtmlClass ? \" \" + fieldHtmlClass : \"\") %>\\\'\' +\n      \'<%= (node.schemaElement && node.schemaElement.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \'> \' +\n      \'<% _.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> <% }}}); %> \' +\n      \'</select>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true\n  },\n  \'imageselect\': {\n    \'template\': \'<div>\' +\n      \'<input type=\"hidden\" name=\"<%= node.name %>\" id=\"<%= node.id %>\" value=\"<%= value %>\" />\' +\n      \'<div class=\"dropdown\">\' +\n      \'<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\"<% } %>>\' +\n        \'<% if (node.value) { %><img src=\"<% if (!node.value.match(/^https?:/)) { %><%= prefix %><% } %><%= node.value %><%= suffix %>\" alt=\"\" /><% } else { %><%= buttonTitle %><% } %>\' +\n      \'</a>\' +\n      \'<div class=\"dropdown-menu navbar\" id=\"<%= node.id %>_dropdown\">\' +\n        \'<div>\' +\n        \'<% _.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> <% }); %>\' +\n        \'</div>\' +\n        \'<div class=\"pagination-right\"><a class=\"btn btn-default\">Reset</a></div>\' +\n      \'</div>\' +\n      \'</div>\' +\n      \'</div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onBeforeRender\': function (data, node) {\n      var elt = node.formElement || {};\n      var nbRows = null;\n      var maxColumns = elt.imageSelectorColumns || 5;\n      data.buttonTitle = elt.imageSelectorTitle || \'Select...\';\n      data.prefix = elt.imagePrefix || \'\';\n      data.suffix = elt.imageSuffix || \'\';\n      data.width = elt.imageWidth || 32;\n      data.height = elt.imageHeight || 32;\n      data.buttonClass = elt.imageButtonClass || false;\n      if (node.options.length > maxColumns) {\n        nbRows = Math.ceil(node.options.length / maxColumns);\n        data.columns = Math.ceil(node.options.length / nbRows);\n      }\n      else {\n        data.columns = maxColumns;\n      }\n    },\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'onInsert\': function (evt, node) {\n      $(node.el).on(\'click\', \'.dropdown-menu a\', function (evt) {\n        evt.preventDefault();\n        evt.stopPropagation();\n        var img = (evt.target.nodeName.toLowerCase() === \'img\') ?\n          $(evt.target) :\n          $(evt.target).find(\'img\');\n        var value = img.attr(\'src\');\n        var elt = node.formElement || {};\n        var prefix = elt.imagePrefix || \'\';\n        var suffix = elt.imageSuffix || \'\';\n        var width = elt.imageWidth || 32;\n        var height = elt.imageHeight || 32;\n        if (value) {\n          if (value.indexOf(prefix) === 0) {\n            value = value.substring(prefix.length);\n          }\n          value = value.substring(0, value.length - suffix.length);\n          $(node.el).find(\'input\').attr(\'value\', value);\n          $(node.el).find(\'a[data-toggle=\"dropdown\"]\')\n            .addClass(elt.imageButtonClass)\n            .attr(\'style\', \'max-width:\' + width + \'px;max-height:\' + height + \'px\')\n            .html(\'<img src=\"\' + (!value.match(/^https?:/) ? prefix : \'\') + value + suffix + \'\" alt=\"\" />\');\n        }\n        else {\n          $(node.el).find(\'input\').attr(\'value\', \'\');\n          $(node.el).find(\'a[data-toggle=\"dropdown\"]\')\n            .removeClass(elt.imageButtonClass)\n            .removeAttr(\'style\')\n            .html(elt.imageSelectorTitle || \'Select...\');\n        }\n      });\n    }\n  },\n  \'iconselect\': {\n    \'template\': \'<div>\' +\n      \'<input type=\"hidden\" name=\"<%= node.name %>\" id=\"<%= node.id %>\" value=\"<%= value %>\" />\' +\n      \'<div class=\"dropdown\">\' +\n      \'<a class=\"btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } %>\" data-toggle=\"dropdown\" href=\"#\"<% if (node.value) { %> style=\"max-width:<%= width %>px;max-height:<%= height %>px\"<% } %>>\' +\n        \'<% if (node.value) { %><i class=\"icon-<%= node.value %>\" /><% } else { %><%= buttonTitle %><% } %>\' +\n      \'</a>\' +\n      \'<div class=\"dropdown-menu navbar\" id=\"<%= node.id %>_dropdown\">\' +\n        \'<div>\' +\n        \'<% _.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> <% }); %>\' +\n        \'</div>\' +\n        \'<div class=\"pagination-right\"><a class=\"btn\">Reset</a></div>\' +\n      \'</div>\' +\n      \'</div>\' +\n      \'</div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onBeforeRender\': function (data, node) {\n      var elt = node.formElement || {};\n      var nbRows = null;\n      var maxColumns = elt.imageSelectorColumns || 5;\n      data.buttonTitle = elt.imageSelectorTitle || \'Select...\';\n      data.buttonClass = elt.imageButtonClass || false;\n      if (node.options.length > maxColumns) {\n        nbRows = Math.ceil(node.options.length / maxColumns);\n        data.columns = Math.ceil(node.options.length / nbRows);\n      }\n      else {\n        data.columns = maxColumns;\n      }\n    },\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'onInsert\': function (evt, node) {\n      $(node.el).on(\'click\', \'.dropdown-menu a\', function (evt) {\n        evt.preventDefault();\n        evt.stopPropagation();\n        var i = (evt.target.nodeName.toLowerCase() === \'i\') ?\n          $(evt.target) :\n          $(evt.target).find(\'i\');\n        var value = i.attr(\'class\');\n        var elt = node.formElement || {};\n        if (value) {\n          value = value;\n          $(node.el).find(\'input\').attr(\'value\', value);\n          $(node.el).find(\'a[data-toggle=\"dropdown\"]\')\n            .addClass(elt.imageButtonClass)\n            .html(\'<i class=\"\'+ value +\'\" alt=\"\" />\');\n        }\n        else {\n          $(node.el).find(\'input\').attr(\'value\', \'\');\n          $(node.el).find(\'a[data-toggle=\"dropdown\"]\')\n            .removeClass(elt.imageButtonClass)\n            .html(elt.imageSelectorTitle || \'Select...\');\n        }\n      });\n    }\n  },\n  \'radios\':{\n    \'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) %>\"\' +\n      \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n      \'<%= (node.schemaElement && node.schemaElement.required ? \" required=\\\'required\\\'\" : \"\") %>\' +\n      \'/><%= (key instanceof Object ? key.title : key) %></label></div> <% }); %></div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true\n  },\n  \'radiobuttons\': {\n    \'template\': \'<div id=\"<%= node.id %>\">\' +\n      \'<% _.each(node.options, function(key, val) { %>\' +\n        \'<label class=\"btn btn-default\">\' +\n        \'<input<%= (fieldHtmlClass ? \" class=\\\'\" + fieldHtmlClass + \"\\\'\": \"\") %> type=\"radio\" style=\"position:absolute;left:-9999px;\" \' +\n        \'<% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked=\"checked\" <% } %> name=\"<%= node.name %>\" value=\"<%= (key instanceof Object ? key.value : key) %>\" />\' +\n        \'<span><%= (key instanceof Object ? key.title : key) %></span></label> \' +\n        \'<% }); %>\' +\n      \'</div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onInsert\': function (evt, node) {\n      var activeClass = \'active\';\n      var elt = node.formElement || {};\n      if (elt.activeClass) {\n        activeClass += \' \' + elt.activeClass;\n      }\n      $(node.el).find(\'label\').on(\'click\', function () {\n        $(this).parent().find(\'label\').removeClass(activeClass);\n        $(this).addClass(activeClass);\n      });\n      // Set active on insert\n      $(node.el).find(\'input:checked\').parent().addClass(activeClass)\n    }\n  },\n  \'checkboxes\':{\n    \'template\': \'<div><%= choiceshtml %></div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'onBeforeRender\': function (data, node) {\n      // Build up choices from the enumeration list\n      var choices = null;\n      var choiceshtml = null;\n      var template = \'<div class=\"checkbox\"><label>\' +\n        \'<input type=\"checkbox\" <% if (value) { %> checked=\"checked\" <% } %> name=\"<%= name %>\" value=\"1\"\' +\n        \'<%= (node.disabled? \" disabled\" : \"\")%>\' +\n        \'/><%= title %></label></div>\';\n      if (!node || !node.schemaElement) return;\n\n      if (node.schemaElement.items) {\n        choices =\n          node.schemaElement.items[\"enum\"] ||\n          node.schemaElement.items[0][\"enum\"];\n      } else {\n        choices = node.schemaElement[\"enum\"];\n      }\n      if (!choices) return;\n\n      choiceshtml = \'\';\n      _.each(choices, function (choice, idx) {\n        choiceshtml += _.template(template, fieldTemplateSettings)({\n          name: node.key + \'[\' + idx + \']\',\n          value: _.include(node.value, choice),\n          title: hasOwnProperty(node.formElement.titleMap, choice) ? node.formElement.titleMap[choice] : choice,\n          node: node\n        });\n      });\n\n      data.choiceshtml = choiceshtml;\n    }\n  },\n  \'array\': {\n    \'template\': \'<div id=\"<%= id %>\"><ul class=\"_jsonform-array-ul\" style=\"list-style-type:none;\"><%= children %></ul>\' +\n      \'<span class=\"_jsonform-array-buttons\">\' +\n        \'<a href=\"#\" class=\"btn btn-default _jsonform-array-addmore\"><i class=\"glyphicon glyphicon-plus-sign\" title=\"Add new\"></i></a> \' +\n        \'<a href=\"#\" class=\"btn btn-default _jsonform-array-deletelast\"><i class=\"glyphicon glyphicon-minus-sign\" title=\"Delete last\"></i></a>\' +\n      \'</span>\' +\n      \'</div>\',\n    \'fieldtemplate\': true,\n    \'array\': true,\n    \'childTemplate\': function (inner, enableDrag) {\n      if ($(\'\').sortable) {\n        // Insert a \"draggable\" icon\n        // floating to the left of the main element\n        return \'<li data-idx=\"<%= node.childPos %>\">\' +\n          // only allow drag of children if enabled\n          (enableDrag ? \'<span class=\"draggable line\"><i class=\"glyphicon glyphicon-list\" title=\"Move item\"></i></span>\' : \'\') +\n          inner +\n          \'<span class=\"_jsonform-array-buttons\">\' +\n            \'<a href=\"#\" class=\"btn btn-default _jsonform-array-deletecurrent\"><i class=\"glyphicon glyphicon-minus-sign\" title=\"Delete current\"></i></a>\' +\n          \'</span>\' +\n          \'</li>\';\n      }\n      else {\n        return \'<li data-idx=\"<%= node.childPos %>\">\' +\n          inner +\n          \'<span class=\"_jsonform-array-buttons\">\' +\n            \'<a href=\"#\" class=\"btn btn-default _jsonform-array-deletecurrent\"><i class=\"glyphicon glyphicon-minus-sign\" title=\"Delete current\"></i></a>\' +\n          \'</span>\' +\n          \'</li>\';\n      }\n    },\n    \'onInsert\': function (evt, node) {\n      var $nodeid = $(node.el).find(\'#\' + escapeSelector(node.id));\n      var boundaries = node.getArrayBoundaries();\n\n      // Switch two nodes in an array\n      var moveNodeTo = function (fromIdx, toIdx) {\n        // Note \"switchValuesWith\" extracts values from the DOM since field\n        // values are not synchronized with the tree data structure, so calls\n        // to render are needed at each step to force values down to the DOM\n        // before next move.\n        // TODO: synchronize field values and data structure completely and\n        // call render only once to improve efficiency.\n        if (fromIdx === toIdx) return;\n        var incr = (fromIdx < toIdx) ? 1: -1;\n        var i = 0;\n        var parentEl = $(\'> ul\', $nodeid);\n        for (i = fromIdx; i !== toIdx; i += incr) {\n          node.children[i].switchValuesWith(node.children[i + incr]);\n          node.children[i].render(parentEl.get(0));\n          node.children[i + incr].render(parentEl.get(0));\n        }\n\n        // No simple way to prevent DOM reordering with jQuery UI Sortable,\n        // so we\'re going to need to move sorted DOM elements back to their\n        // origin position in the DOM ourselves (we switched values but not\n        // DOM elements)\n        var fromEl = $(node.children[fromIdx].el);\n        var toEl = $(node.children[toIdx].el);\n        fromEl.detach();\n        toEl.detach();\n        if (fromIdx < toIdx) {\n          if (fromIdx === 0) parentEl.prepend(fromEl);\n          else $(node.children[fromIdx-1].el).after(fromEl);\n          $(node.children[toIdx-1].el).after(toEl);\n        }\n        else {\n          if (toIdx === 0) parentEl.prepend(toEl);\n          else $(node.children[toIdx-1].el).after(toEl);\n          $(node.children[fromIdx-1].el).after(fromEl);\n        }\n      };\n\n      $(\'> span > a._jsonform-array-addmore\', $nodeid).click(function (evt) {\n        evt.preventDefault();\n        evt.stopPropagation();\n        var idx = node.children.length;\n        if (boundaries.maxItems >= 0) {\n          if (node.children.length > boundaries.maxItems - 2) {\n            $nodeid.find(\'> span > a._jsonform-array-addmore\')\n              .addClass(\'disabled\');\n          }\n          if (node.children.length > boundaries.maxItems - 1) {\n            return false;\n          }\n        }\n        node.insertArrayItem(idx, $(\'> ul\', $nodeid).get(0));\n        if ((boundaries.minItems <= 0) ||\n            ((boundaries.minItems > 0) &&\n              (node.children.length > boundaries.minItems - 1))) {\n          $nodeid.find(\'a._jsonform-array-deletecurrent\')\n            .removeClass(\'disabled\');\n          $nodeid.find(\'> span > a._jsonform-array-deletelast\')\n            .removeClass(\'disabled\');\n        }\n      });\n\n      //Simulate Users click to setup the form with its minItems\n      var curItems = $(\'> ul > li\', $nodeid).length;\n      if ((boundaries.minItems > 0) &&\n          (curItems < boundaries.minItems)) {\n        for (var i = 0; i < (boundaries.minItems - 1) && ($nodeid.find(\'> ul > li\').length < boundaries.minItems); i++) {\n          node.insertArrayItem(curItems, $nodeid.find(\'> ul\').get(0));\n        }\n      }\n      if ((boundaries.minItems > 0) &&\n          (node.children.length <= boundaries.minItems)) {\n        $nodeid.find(\'a._jsonform-array-deletecurrent\')\n          .addClass(\'disabled\');\n        $nodeid.find(\'> span > a._jsonform-array-deletelast\')\n          .addClass(\'disabled\');\n      }\n\n      function deleteArrayItem (idx) {\n        if (boundaries.minItems > 0) {\n          if (node.children.length < boundaries.minItems + 2) {\n            $nodeid.find(\'> span > a._jsonform-array-deletelast\')\n              .addClass(\'disabled\');\n          }\n          if (node.children.length <= boundaries.minItems) {\n            return false;\n          }\n        }\n        else if (node.children.length === 1) {\n          $nodeid.find(\'a._jsonform-array-deletecurrent\')\n            .addClass(\'disabled\');\n          $nodeid.find(\'> span > a._jsonform-array-deletelast\')\n            .addClass(\'disabled\');\n        }\n        node.deleteArrayItem(idx);\n        if (boundaries.minItems > 0) {\n          if (node.children.length < boundaries.minItems + 1) {\n            $nodeid.find(\'a._jsonform-array-deletecurrent\')\n              .addClass(\'disabled\');\n          }\n        }\n        if ((boundaries.maxItems >= 0) && (idx <= boundaries.maxItems - 1)) {\n          $nodeid.find(\'> span > a._jsonform-array-addmore\')\n            .removeClass(\'disabled\');\n        }\n      }\n\n      $($nodeid).on(\'click\', \'a._jsonform-array-deletecurrent\', function (evt) {\n        var idx = $(this).closest(\'[data-idx]\').data(\'idx\')\n        evt.preventDefault();\n        evt.stopPropagation();\n        return deleteArrayItem(idx);\n      });\n\n      $(\'> span > a._jsonform-array-deletelast\', $nodeid).click(function (evt) {\n        var idx = node.children.length - 1;\n        evt.preventDefault();\n        evt.stopPropagation();\n        return deleteArrayItem(idx);\n      });\n\n      // only allow drag if default or enabled\n      if (!isSet(node.formElement.draggable) || node.formElement.draggable) {\n        if ($(node.el).sortable) {\n          $(\'> ul\', $nodeid).sortable();\n          $(\'> ul\', $nodeid).bind(\'sortstop\', function (event, ui) {\n            var idx = $(ui.item).data(\'idx\');\n            var newIdx = $(ui.item).index();\n            moveNodeTo(idx, newIdx);\n          });\n        }\n      }\n    }\n  },\n  \'tabarray\': {\n    \'template\': \'<div id=\"<%= id %>\"><div class=\"tabbable tabs-left\">\' +\n      \'<ul class=\"nav nav-tabs\">\' +\n        \'<%= tabs %>\' +\n      \'</ul>\' +\n      \'<div class=\"tab-content\">\' +\n        \'<%= children %>\' +\n      \'</div>\' +\n      \'</div>\' +\n      \'<a href=\"#\" class=\"btn btn-default _jsonform-array-addmore\"><i class=\"glyphicon glyphicon-plus-sign\" title=\"Add new\"></i></a> \' +\n      \'<a href=\"#\" class=\"btn btn-default _jsonform-array-deleteitem\"><i class=\"glyphicon glyphicon-minus-sign\" title=\"Delete item\"></i></a></div>\',\n    \'fieldtemplate\': true,\n    \'array\': true,\n    \'childTemplate\': function (inner) {\n      return \'<div data-idx=\"<%= node.childPos %>\" class=\"tab-pane\">\' +\n        inner +\n        \'</div>\';\n    },\n    \'onBeforeRender\': function (data, node) {\n      // Generate the initial \'tabs\' from the children\n      var tabs = \'\';\n      _.each(node.children, function (child, idx) {\n        var title = child.legend ||\n          child.title ||\n          (\'Item \' + (idx+1));\n        tabs += \'<li data-idx=\"\' + idx + \'\"\' +\n          ((idx === 0) ? \' class=\"active\"\' : \'\') +\n          \'><a class=\"draggable tab\" data-toggle=\"tab\" rel=\"\' + escape(title) + \'\">\' +\n          escapeHTML(title) +\n          \'</a></li>\';\n      });\n      data.tabs = tabs;\n    },\n    \'onInsert\': function (evt, node) {\n      var $nodeid = $(node.el).find(\'#\' + escapeSelector(node.id));\n      var boundaries = node.getArrayBoundaries();\n\n      var moveNodeTo = function (fromIdx, toIdx) {\n        // Note \"switchValuesWith\" extracts values from the DOM since field\n        // values are not synchronized with the tree data structure, so calls\n        // to render are needed at each step to force values down to the DOM\n        // before next move.\n        // TODO: synchronize field values and data structure completely and\n        // call render only once to improve efficiency.\n        if (fromIdx === toIdx) return;\n        var incr = (fromIdx < toIdx) ? 1: -1;\n        var i = 0;\n        var tabEl = $(\'> .tabbable > .tab-content\', $nodeid).get(0);\n        for (i = fromIdx; i !== toIdx; i += incr) {\n          node.children[i].switchValuesWith(node.children[i + incr]);\n          node.children[i].render(tabEl);\n          node.children[i + incr].render(tabEl);\n        }\n      };\n\n\n      // Refreshes the list of tabs\n      var updateTabs = function (selIdx) {\n        var tabs = \'\';\n        var activateFirstTab = false;\n        if (selIdx === undefined) {\n          selIdx = $(\'> .tabbable > .nav-tabs .active\', $nodeid).data(\'idx\');\n          if (selIdx) {\n            selIdx = parseInt(selIdx, 10);\n          }\n          else {\n            activateFirstTab = true;\n            selIdx = 0;\n          }\n        }\n        if (selIdx >= node.children.length) {\n          selIdx = node.children.length - 1;\n        }\n        _.each(node.children, function (child, idx) {\n          $(\'> .tabbable > .tab-content > [data-idx=\"\' + idx + \'\"] > fieldset > legend\', $nodeid).html(child.legend);\n          var title = child.legend || child.title || (\'Item \' + (idx+1));\n          tabs += \'<li data-idx=\"\' + idx + \'\">\' +\n                  \'<a class=\"draggable tab\" data-toggle=\"tab\" rel=\"\' + escape(title) + \'\">\' +\n                  escapeHTML(title) +\n                  \'</a></li>\';\n        });\n        $(\'> .tabbable > .nav-tabs\', $nodeid).html(tabs);\n        if (activateFirstTab) {\n          $(\'> .tabbable > .nav-tabs [data-idx=\"0\"]\', $nodeid).addClass(\'active\');\n        }\n        $(\'> .tabbable > .nav-tabs [data-toggle=\"tab\"]\', $nodeid).eq(selIdx).click();\n      };\n\n      $(\'> a._jsonform-array-deleteitem\', $nodeid).click(function (evt) {\n        var idx = $(\'> .tabbable > .nav-tabs .active\', $nodeid).data(\'idx\');\n        evt.preventDefault();\n        evt.stopPropagation();\n        if (boundaries.minItems > 0) {\n          if (node.children.length < boundaries.minItems + 1) {\n            $nodeid.find(\'> a._jsonform-array-deleteitem\')\n              .addClass(\'disabled\');\n          }\n          if (node.children.length <= boundaries.minItems) return false;\n        }\n        node.deleteArrayItem(idx);\n        updateTabs();\n        if ((node.children.length < boundaries.minItems + 1) ||\n            (node.children.length === 0)) {\n          $nodeid.find(\'> a._jsonform-array-deleteitem\').addClass(\'disabled\');\n        }\n        if ((boundaries.maxItems >= 0) &&\n            (node.children.length <= boundaries.maxItems)) {\n          $nodeid.find(\'> a._jsonform-array-addmore\').removeClass(\'disabled\');\n        }\n      });\n\n      $(\'> a._jsonform-array-addmore\', $nodeid).click(function (evt) {\n        var idx = node.children.length;\n        if (boundaries.maxItems>=0) {\n          if (node.children.length>boundaries.maxItems-2) {\n            $(\'> a._jsonform-array-addmore\', $nodeid).addClass(\"disabled\");\n          }\n          if (node.children.length > boundaries.maxItems - 1) {\n            return false;\n          }\n        }\n        evt.preventDefault();\n        evt.stopPropagation();\n        node.insertArrayItem(idx,\n          $nodeid.find(\'> .tabbable > .tab-content\').get(0));\n        updateTabs(idx);\n        if ((boundaries.minItems <= 0) ||\n            ((boundaries.minItems > 0) && (idx > boundaries.minItems - 1))) {\n          $nodeid.find(\'> a._jsonform-array-deleteitem\').removeClass(\'disabled\');\n        }\n      });\n\n      $(node.el).on(\'legendUpdated\', function (evt) {\n        updateTabs();\n        evt.preventDefault();\n        evt.stopPropagation();\n      });\n\n      // only allow drag if default or enabled\n      if (!isSet(node.formElement.draggable) || node.formElement.draggable) {\n        if ($(node.el).sortable) {\n          $(\'> .tabbable > .nav-tabs\', $nodeid).sortable({\n            containment: node.el,\n            tolerance: \'pointer\'\n          });\n          $(\'> .tabbable > .nav-tabs\', $nodeid).bind(\'sortstop\', function (event, ui) {\n            var idx = $(ui.item).data(\'idx\');\n            var newIdx = $(ui.item).index();\n            moveNodeTo(idx, newIdx);\n            updateTabs(newIdx);\n          });\n        }\n      }\n\n      // Simulate User\'s click to setup the form with its minItems\n      if ((boundaries.minItems >= 0)  &&\n          (node.children.length <= boundaries.minItems)) {\n        for (var i = 0; i < (boundaries.minItems - 1); i++) {\n          $nodeid.find(\'> a._jsonform-array-addmore\').click();\n        }\n        $nodeid.find(\'> a._jsonform-array-deleteitem\').addClass(\'disabled\');\n        updateTabs();\n      }\n\n      if ((boundaries.maxItems >= 0) &&\n          (node.children.length >= boundaries.maxItems)) {\n        $nodeid.find(\'> a._jsonform-array-addmore\').addClass(\'disabled\');\n      }\n      if ((boundaries.minItems >= 0) &&\n          (node.children.length <= boundaries.minItems)) {\n        $nodeid.find(\'> a._jsonform-array-deleteitem\').addClass(\'disabled\');\n      }\n    }\n  },\n  \'help\': {\n    \'template\':\'<span class=\"help-block\" style=\"padding-top:5px\"><%= elt.helpvalue %></span>\',\n    \'fieldtemplate\': true\n  },\n  \'msg\': {\n    \'template\': \'<%= elt.msg %>\'\n  },\n  \'fieldset\': {\n    \'template\': \'<fieldset class=\"form-group jsonform-error-<%= keydash %> <% if (elt.expandable) { %>expandable<% } %> <%= elt.htmlClass?elt.htmlClass:\"\" %>\" \' +\n      \'<% if (id) { %> id=\"<%= id %>\"<% } %>\' +\n      \'>\' +\n      \'<% if (node.title || node.legend) { %><legend role=\"treeitem\" aria-expanded=\"false\"><%= node.title || node.legend %></legend><% } %>\' +\n      \'<% if (elt.expandable) { %><div class=\"form-group\"><% } %>\' +\n      \'<%= children %>\' +\n      \'<% if (elt.expandable) { %></div><% } %>\' +\n      \'</fieldset>\',\n    onInsert: function (evt, node) {\n      if (node.el !== null) {\n        $(\'.expandable > div, .expandable > fieldset\', node.el).hide();\n        // See #233\n        $(\".expandable\", node.el).removeClass(\"expanded\");\n      }\n    }\n  },\n  \'advancedfieldset\': {\n    \'template\': \'<fieldset\' +\n      \'<% if (id) { %> id=\"<%= id %>\"<% } %>\' +\n      \' class=\"expandable <%= elt.htmlClass?elt.htmlClass:\"\" %>\">\' +\n      \'<legend role=\"treeitem\" aria-expanded=\"false\"><%= (node.title || node.legend) ? (node.title || node.legend) : \"Advanced options\" %></legend>\' +\n      \'<div class=\"form-group\">\' +\n      \'<%= children %>\' +\n      \'</div>\' +\n      \'</fieldset>\',\n    onInsert: function (evt, node) {\n      if (node.el !== null) {\n        $(\'.expandable > div, .expandable > fieldset\', node.el).hide();\n        // See #233\n        $(\".expandable\", node.el).removeClass(\"expanded\");\n      }\n    }\n  },\n  \'authfieldset\': {\n    \'template\': \'<fieldset\' +\n      \'<% if (id) { %> id=\"<%= id %>\"<% } %>\' +\n      \' class=\"expandable <%= elt.htmlClass?elt.htmlClass:\"\" %>\">\' +\n      \'<legend role=\"treeitem\" aria-expanded=\"false\"><%= (node.title || node.legend) ? (node.title || node.legend) : \"Authentication settings\" %></legend>\' +\n      \'<div class=\"form-group\">\' +\n      \'<%= children %>\' +\n      \'</div>\' +\n      \'</fieldset>\',\n    onInsert: function (evt, node) {\n      if (node.el !== null) {\n        $(\'.expandable > div, .expandable > fieldset\', node.el).hide();\n        // See #233\n        $(\".expandable\", node.el).removeClass(\"expanded\");\n      }\n    }\n  },\n  \'submit\':{\n    \'template\':\'<input type=\"submit\" <% if (id) { %> id=\"<%= id %>\" <% } %> class=\"btn btn-primary <%= elt.htmlClass?elt.htmlClass:\"\" %>\" value=\"<%= value || node.title %>\"<%= (node.disabled? \" disabled\" : \"\")%>/>\'\n  },\n  \'button\':{\n    \'template\':\' <button type=\"button\" <% if (id) { %> id=\"<%= id %>\" <% } %> class=\"btn btn-default <%= elt.htmlClass?elt.htmlClass:\"\" %>\"><%= node.title %></button> \'\n  },\n  \'actions\':{\n    \'template\':\'<div class=\"<%= elt.htmlClass?elt.htmlClass:\"\" %>\"><%= children %></div>\'\n  },\n  \'hidden\':{\n    \'template\':\'<input type=\"hidden\" id=\"<%= id %>\" name=\"<%= node.name %>\" value=\"<%= escape(value) %>\" />\',\n    \'inputfield\': true\n  },\n  \'tabs\':{\n    \'template\':\'<ul class=\"nav nav-tabs <%= elt.htmlClass?elt.htmlClass:\"\" %>\"\' +\n    \'<% if (elt.id) { %> id=\"<%= elt.id %>\"<% } %>\' +\n    \'><%=tab_list%></ul><div class=\"tab-content\" <% if (elt.id) { %> data-tabset=\"<%= elt.id %>\"<% } %>><%=children%></div>\',\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'onBeforeRender\': function (data, node) {\n      // Generate the initial \'tabs\' from the children\n      var parentID = escapeHTML(node.id ? node.id + \"-\" : \"\")\n      var tab_list = \'\';\n      _.each(node.children, function (child, idx) {\n        var title = escapeHTML(child.title || (\'Item \' + (idx+1)));\n        var title_escaped = title.replace(/ /g,\"_\");\n        tab_list += \'<li class=\"nav-item\' +\n          ((idx === 0) ? \' active\' : \'\') + \'\">\' +\n          \'<a href=\"#\'+ parentID + title_escaped +\'\" class=\"nav-link\"\' +\n          \' data-tab=\"\' + parentID + title_escaped + \'\"\' +\n          \' data-toggle=\"tab\">\' + title +\n          \'</a></li>\';\n      });\n      data.tab_list = tab_list;\n      return data;\n    },\n    \'onInsert\': function(evt, node){\n      $(\"#\"+node.id+\">li.nav-item\").on(\"click\", function(e){\n        e.preventDefault();\n        $(node.el).find(\"div[data-tabset=\'\"+node.id+\"\']>div.tab-pane.active\").each(function(){\n          $(this).removeClass(\"active\");\n        })\n        var tab_id = $(this).find(\'a\').attr(\'data-tab\');\n        $(\"#\"+tab_id).addClass(\"active\");\n      });\n    }\n  },\n  \'tab\':{\n    \'template\': \'<div class=\"tab-pane\' +\n    \'<% if (elt.htmlClass) { %> <%= elt.htmlClass %> <% } %>\' +\n        //Set the first tab as active\n    \'<% if (node.childPos === 0) { %> active<% } %>\' +\n    \'\"\' + //Finish end quote of class tag\n    \'<% if (node.title) { %> id=\"<%= node.parentNode.id %>-<%= node.title.replace(/ /g,\"_\") %>\"<% } %>\' +\n    \'><%= children %></div>\'\n  },\n  \'selectfieldset\': {\n    \'template\': \'<fieldset class=\"tab-container <%= elt.htmlClass?elt.htmlClass:\"\" %>\">\' +\n      \'<% if (node.legend) { %><legend role=\"treeitem\" aria-expanded=\"false\"><%= node.legend %></legend><% } %>\' +\n      \'<% if (node.formElement.key) { %><input type=\"hidden\" id=\"<%= node.id %>\" name=\"<%= node.name %>\" value=\"<%= escape(value) %>\" /><% } else { %>\' +\n        \'<a id=\"<%= node.id %>\"></a><% } %>\' +\n      \'<div class=\"tabbable\">\' +\n        \'<div class=\"form-group<%= node.formElement.hideMenu ? \" hide\" : \"\" %>\">\' +\n          \'<% if (!elt.notitle) { %><label for=\"<%= node.id %>\"><%= node.title ? node.title : node.name %></label><% } %>\' +\n          \'<div class=\"controls\"><%= tabs %></div>\' +\n        \'</div>\' +\n        \'<div class=\"tab-content\">\' +\n          \'<%= children %>\' +\n        \'</div>\' +\n      \'</div>\' +\n      \'</fieldset>\',\n    \'inputfield\': true,\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'childTemplate\': function (inner) {\n      return \'<div data-idx=\"<%= node.childPos %>\" class=\"tab-pane\' +\n        \'<% if (node.active) { %> active<% } %>\">\' +\n        inner +\n        \'</div>\';\n    },\n    \'onBeforeRender\': function (data, node) {\n      // Before rendering, this function ensures that:\n      // 1. direct children have IDs (used to show/hide the tabs contents)\n      // 2. the tab to active is flagged accordingly. The active tab is\n      // the first one, except if form values are available, in which case\n      // it\'s the first tab for which there is some value available (or back\n      // to the first one if there are none)\n      // 3. the HTML of the select field used to select tabs is exposed in the\n      // HTML template data as \"tabs\"\n\n      var children = null;\n      var choices = [];\n      if (node.schemaElement) {\n        choices = node.schemaElement[\'enum\'] || [];\n      }\n      if (node.options) {\n        children = _.map(node.options, function (option, idx) {\n          var child = node.children[idx];\n          child.childPos = idx; // When nested the childPos is always 0.\n          if (option instanceof Object) {\n            option = _.extend({ node: child }, option);\n            option.title = option.title ||\n              child.legend ||\n              child.title ||\n              (\'Option \' + (child.childPos+1));\n            option.value = isSet(option.value) ? option.value :\n              isSet(choices[idx]) ? choices[idx] : idx;\n            return option;\n          }\n          else {\n            return {\n              title: option,\n              value: isSet(choices[child.childPos]) ?\n                choices[child.childPos] :\n                child.childPos,\n              node: child\n            };\n          }\n        });\n      }\n      else {\n        children = _.map(node.children, function (child, idx) {\n          child.childPos = idx; // When nested the childPos is always 0.\n          return {\n            title: child.legend || child.title || (\'Option \' + (child.childPos+1)),\n            value: choices[child.childPos] || child.value || child.childPos,\n            node: child\n          };\n        });\n      }\n\n      // Reset each children to inactive so that they are not shown on insert\n      // The active one will then be shown later one. This is useful when sorting\n      // arrays with selectfieldset, otherwise both fields could be active at the\n      // same time.\n      _.each(children, function (child, idx) {\n        child.node.active = false\n      });\n\n      var activeChild = null;\n      if (data.value) {\n        activeChild = _.find(children, function (child) {\n          return (child.value === node.value);\n        });\n      }\n      if (!activeChild) {\n        activeChild = _.find(children, function (child) {\n          return child.node.hasNonDefaultValue();\n        });\n      }\n      if (!activeChild) {\n        activeChild = children[0];\n      }\n      activeChild.node.active = true;\n      data.value = activeChild.value;\n\n      var elt = node.formElement;\n      var tabs = \'<select class=\"nav form-control\"\' +\n        (node.disabled ? \' disabled\' : \'\') +\n        \'>\';\n      _.each(children, function (child, idx) {\n        tabs += \'<option data-idx=\"\' + idx + \'\" value=\"\' + child.value + \'\"\' +\n          (child.node.active ? \' selected=\"selected\" class=\"active\"\' : \'\') +\n          \'>\' +\n          escapeHTML(child.title) +\n          \'</option>\';\n      });\n      tabs += \'</select>\';\n\n      data.tabs = tabs;\n      return data;\n    },\n    \'onInsert\': function (evt, node) {\n      $(node.el).find(\'select.nav\').first().on(\'change\', function (evt) {\n        var $option = $(this).find(\'option:selected\');\n        $(node.el).find(\'input[type=\"hidden\"]\').first().val($option.attr(\'value\'));\n      });\n    }\n  },\n  \'optionfieldset\': {\n    \'template\': \'<div\' +\n      \'<% if (node.id) { %> id=\"<%= node.id %>\"<% } %>\' +\n      \'>\' +\n      \'<%= children %>\' +\n      \'</div>\'\n  },\n  \'section\': {\n    \'template\': \'<div\' +\n      \'<% if (elt.htmlClass) { %> class=\"<%= elt.htmlClass %>\"<% } %>\' +\n      \'<% if (node.id) { %> id=\"<%= node.id %>\"<% } %>\' +\n      \'><%= children %></div>\'\n  },\n\n  /**\n   * A \"questions\" field renders a series of question fields and binds the\n   * result to the value of a schema key.\n   */\n  \'questions\': {\n    \'template\': \'<div>\' +\n      \'<input type=\"hidden\" id=\"<%= node.id %>\" name=\"<%= node.name %>\" value=\"<%= escape(value) %>\" />\' +\n      \'<%= children %>\' +\n      \'</div>\',\n    \'fieldtemplate\': true,\n    \'inputfield\': true,\n    \'getElement\': function (el) {\n      return $(el).parent().get(0);\n    },\n    \'onInsert\': function (evt, node) {\n      if (!node.children || (node.children.length === 0)) return;\n      _.each(node.children, function (child) {\n        $(child.el).hide();\n      });\n      $(node.children[0].el).show();\n    }\n  },\n\n  /**\n   * A \"question\" field lets user choose a response among possible choices.\n   * The field is not associated with any schema key. A question should be\n   * part of a \"questions\" field that binds a series of questions to a\n   * schema key.\n   */\n  \'question\': {\n    \'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>\',\n    \'fieldtemplate\': true,\n    \'onInsert\': function (evt, node) {\n      var activeClass = \'active\';\n      var elt = node.formElement || {};\n      if (elt.activeClass) {\n        activeClass += \' \' + elt.activeClass;\n      }\n\n      // Bind to change events on radio buttons\n      $(node.el).find(\'input[type=\"radio\"]\').on(\'change\', function (evt) {\n        var questionNode = null;\n        var option = node.options[$(this).val()];\n        if (!node.parentNode || !node.parentNode.el) return;\n\n        $(this).parent().parent().find(\'label\').removeClass(activeClass);\n        $(this).parent().addClass(activeClass);\n        $(node.el).nextAll().hide();\n        $(node.el).nextAll().find(\'input[type=\"radio\"]\').prop(\'checked\', false);\n\n        // Execute possible actions (set key value, form submission, open link,\n        // move on to next question)\n        if (option.value) {\n          // Set the key of the \'Questions\' parent\n          $(node.parentNode.el).find(\'input[type=\"hidden\"]\').val(option.value);\n        }\n        if (option.next) {\n          questionNode = _.find(node.parentNode.children, function (child) {\n            return (child.formElement && (child.formElement.qid === option.next));\n          });\n          $(questionNode.el).show();\n          $(questionNode.el).nextAll().hide();\n          $(questionNode.el).nextAll().find(\'input[type=\"radio\"]\').prop(\'checked\', false);\n        }\n        if (option.href) {\n          if (option.target) {\n            window.open(option.href, option.target);\n          }\n          else {\n            window.location = option.href;\n          }\n        }\n        if (option.submit) {\n          setTimeout(function () {\n            node.ownerTree.submit();\n          }, 0);\n        }\n      });\n    }\n  }\n};\n\n\n//Allow to access subproperties by splitting \".\"\n/**\n * Retrieves the key identified by a path selector in the structured object.\n *\n * Levels in the path are separated by a dot. Array items are marked\n * with [x]. For instance:\n *  foo.bar[3].baz\n *\n * @function\n * @param {Object} obj Structured object to parse\n * @param {String} key Path to the key to retrieve\n * @param {boolean} ignoreArrays True to use first element in an array when\n *   stucked on a property. This parameter is basically only useful when\n *   parsing a JSON schema for which the \"items\" property may either be an\n *   object or an array with one object (only one because JSON form does not\n *   support mix of items for arrays).\n * @return {Object} The key\'s value.\n */\njsonform.util.getObjKey = function (obj, key, ignoreArrays) {\n  var innerobj = obj;\n  var keyparts = key.split(\".\");\n  var subkey = null;\n  var arrayMatch = null;\n  var prop = null;\n\n  for (var i = 0; i < keyparts.length; i++) {\n    if ((innerobj === null) || (typeof innerobj !== \"object\")) return null;\n    subkey = keyparts[i];\n    prop = subkey.replace(reArray, \'\');\n    reArray.lastIndex = 0;\n    arrayMatch = reArray.exec(subkey);\n    if (arrayMatch) {\n      while (true) {\n        if (prop && !_.isArray(innerobj[prop])) return null;\n        innerobj = prop ? innerobj[prop][parseInt(arrayMatch[1])] : innerobj[parseInt(arrayMatch[1])];\n        arrayMatch = reArray.exec(subkey);\n        if (!arrayMatch) break;\n        // In the case of multidimensional arrays,\n        // we should not take innerobj[prop][0] anymore,\n        // but innerobj[0] directly\n        prop = null;\n      }\n    } else if (ignoreArrays &&\n        !innerobj[prop] &&\n        _.isArray(innerobj) &&\n        innerobj[0]) {\n      innerobj = innerobj[0][prop];\n    } else {\n      innerobj = innerobj[prop];\n    }\n  }\n\n  if (ignoreArrays && _.isArray(innerobj) && innerobj[0]) {\n    return innerobj[0];\n  } else {\n    return innerobj;\n  }\n};\n\n\n/**\n * Sets the key identified by a path selector to the given value.\n *\n * Levels in the path are separated by a dot. Array items are marked\n * with [x]. For instance:\n *  foo.bar[3].baz\n *\n * The hierarchy is automatically created if it does not exist yet.\n *\n * @function\n * @param {Object} obj The object to build\n * @param {String} key The path to the key to set where each level\n *  is separated by a dot, and array items are flagged with [x].\n * @param {Object} value The value to set, may be of any type.\n */\njsonform.util.setObjKey = function(obj,key,value) {\n  var innerobj = obj;\n  var keyparts = key.split(\".\");\n  var subkey = null;\n  var arrayMatch = null;\n  var prop = null;\n\n  for (var i = 0; i < keyparts.length-1; i++) {\n    subkey = keyparts[i];\n    prop = subkey.replace(reArray, \'\');\n    reArray.lastIndex = 0;\n    arrayMatch = reArray.exec(subkey);\n    if (arrayMatch) {\n      // Subkey is part of an array\n      while (true) {\n        if (!_.isArray(innerobj[prop])) {\n          innerobj[prop] = [];\n        }\n        innerobj = innerobj[prop];\n        prop = parseInt(arrayMatch[1], 10);\n        arrayMatch = reArray.exec(subkey);\n        if (!arrayMatch) break;\n      }\n      if ((typeof innerobj[prop] !== \'object\') ||\n        (innerobj[prop] === null)) {\n        innerobj[prop] = {};\n      }\n      innerobj = innerobj[prop];\n    }\n    else {\n      // \"Normal\" subkey\n      if ((typeof innerobj[prop] !== \'object\') ||\n        (innerobj[prop] === null)) {\n        innerobj[prop] = {};\n      }\n      innerobj = innerobj[prop];\n    }\n  }\n\n  // Set the final value\n  subkey = keyparts[keyparts.length - 1];\n  prop = subkey.replace(reArray, \'\');\n  reArray.lastIndex = 0;\n  arrayMatch = reArray.exec(subkey);\n  if (arrayMatch) {\n    while (true) {\n      if (!_.isArray(innerobj[prop])) {\n        innerobj[prop] = [];\n      }\n      innerobj = innerobj[prop];\n      prop = parseInt(arrayMatch[1], 10);\n      arrayMatch = reArray.exec(subkey);\n      if (!arrayMatch) break;\n    }\n    innerobj[prop] = value;\n  }\n  else {\n    innerobj[prop] = value;\n  }\n};\n\n\n/**\n * Retrieves the key definition from the given schema.\n *\n * The key is identified by the path that leads to the key in the\n * structured object that the schema would generate. Each level is\n * separated by a \'.\'. Array levels are marked with []. For instance:\n *  foo.bar[].baz\n * ... to retrieve the definition of the key at the following location\n * in the JSON schema (using a dotted path notation):\n *  foo.properties.bar.items.properties.baz\n *\n * @function\n * @param {Object} schema The JSON schema to retrieve the key from\n * @param {String} key The path to the key, each level being separated\n *  by a dot and array items being flagged with [].\n * @return {Object} The key definition in the schema, null if not found.\n */\nvar getSchemaKey = function(schema,key) {\n  var schemaKey = key\n    .replace(/\\./g, \'.properties.\')\n    .replace(/\\[[0-9]*\\]/g, \'.items\');\n  var schemaDef = jsonform.util.getObjKey(schema, schemaKey, true);\n  if (schemaDef && schemaDef.$ref) {\n    throw new Error(\'JSONForm does not yet support schemas that use the \' +\n      \'$ref keyword. See: https://github.com/joshfire/jsonform/issues/54\');\n  }\n  return schemaDef;\n};\n\n\n/**\n * Truncates the key path to the requested depth.\n *\n * For instance, if the key path is:\n *  foo.bar[].baz.toto[].truc[].bidule\n * and the requested depth is 1, the returned key will be:\n *  foo.bar[].baz.toto\n *\n * Note the function includes the path up to the next depth level.\n *\n * @function\n * @param {String} key The path to the key in the schema, each level being\n *  separated by a dot and array items being flagged with [].\n * @param {Number} depth The array depth\n * @return {String} The path to the key truncated to the given depth.\n */\nvar truncateToArrayDepth = function (key, arrayDepth) {\n  var depth = 0;\n  var pos = 0;\n  if (!key) return null;\n\n  if (arrayDepth > 0) {\n    while (depth < arrayDepth) {\n      pos = key.indexOf(\'[]\', pos);\n      if (pos === -1) {\n        // Key path is not \"deep\" enough, simply return the full key\n        return key;\n      }\n      pos = pos + 2;\n      depth += 1;\n    }\n  }\n\n  // Move one step further to the right without including the final []\n  pos = key.indexOf(\'[]\', pos);\n  if (pos === -1) return key;\n  else return key.substring(0, pos);\n};\n\n/**\n * Applies the array path to the key path.\n *\n * For instance, if the key path is:\n *  foo.bar[].baz.toto[].truc[].bidule\n * and the arrayPath [4, 2], the returned key will be:\n *  foo.bar[4].baz.toto[2].truc[].bidule\n *\n * @function\n * @param {String} key The path to the key in the schema, each level being\n *  separated by a dot and array items being flagged with [].\n * @param {Array(Number)} arrayPath The array path to apply, e.g. [4, 2]\n * @return {String} The path to the key that matches the array path.\n */\nvar applyArrayPath = function (key, arrayPath) {\n  var depth = 0;\n  if (!key) return null;\n  if (!arrayPath || (arrayPath.length === 0)) return key;\n  var newKey = key.replace(reArray, function (str, p1) {\n    // Note this function gets called as many times as there are [x] in the ID,\n    // from left to right in the string. The goal is to replace the [x] with\n    // the appropriate index in the new array path, if defined.\n    var newIndex = str;\n    if (isSet(arrayPath[depth])) {\n      newIndex = \'[\' + arrayPath[depth] + \']\';\n    }\n    depth += 1;\n    return newIndex;\n  });\n  return newKey;\n};\n\n\n/**\n * Returns the initial value that a field identified by its key\n * should take.\n *\n * The \"initial\" value is defined as:\n * 1. the previously submitted value if already submitted\n * 2. the default value defined in the layout of the form\n * 3. the default value defined in the schema\n *\n * The \"value\" returned is intended for rendering purpose,\n * meaning that, for fields that define a titleMap property,\n * the function returns the label, and not the intrinsic value.\n *\n * The function handles values that contains template strings,\n * e.g. {{values.foo[].bar}} or {{idx}}.\n *\n * When the form is a string, the function truncates the resulting string\n * to meet a potential \"maxLength\" constraint defined in the schema, using\n * \"...\" to mark the truncation. Note it does not validate the resulting\n * string against other constraints (e.g. minLength, pattern) as it would\n * be hard to come up with an automated course of action to \"fix\" the value.\n *\n * @function\n * @param {Object} formObject The JSON Form object\n * @param {String} key The generic key path (e.g. foo[].bar.baz[])\n * @param {Array(Number)} arrayPath The array path that identifies\n *  the unique value in the submitted form (e.g. [1, 3])\n * @param {Object} tpldata Template data object\n * @param {Boolean} usePreviousValues true to use previously submitted values\n *  if defined.\n */\nvar getInitialValue = function (formObject, key, arrayPath, tpldata, usePreviousValues) {\n  var value = null;\n\n  // Complete template data for template function\n  tpldata = tpldata || {};\n  tpldata.idx = tpldata.idx ||\n    (arrayPath ? arrayPath[arrayPath.length-1] : 1);\n  tpldata.value = isSet(tpldata.value) ? tpldata.value : \'\';\n  tpldata.getValue = tpldata.getValue || function (key) {\n    return getInitialValue(formObject, key, arrayPath, tpldata, usePreviousValues);\n  };\n\n  // Helper function that returns the form element that explicitly\n  // references the given key in the schema.\n  var getFormElement = function (elements, key) {\n    var formElement = null;\n    if (!elements || !elements.length) return null;\n    _.each(elements, function (elt) {\n      if (formElement) return;\n      if (elt === key) {\n        formElement = { key: elt };\n        return;\n      }\n      if (_.isString(elt)) return;\n      if (elt.key === key) {\n        formElement = elt;\n      }\n      else if (elt.items) {\n        formElement = getFormElement(elt.items, key);\n      }\n    });\n    return formElement;\n  };\n  var formElement = getFormElement(formObject.form || [], key);\n  var schemaElement = getSchemaKey(formObject.schema.properties, key);\n\n  if (usePreviousValues && formObject.value) {\n    // If values were previously submitted, use them directly if defined\n    value = jsonform.util.getObjKey(formObject.value, applyArrayPath(key, arrayPath));\n  }\n  if (!isSet(value)) {\n    if (formElement && (typeof formElement[\'value\'] !== \'undefined\')) {\n      // Extract the definition of the form field associated with\n      // the key as it may override the schema\'s default value\n      // (note a \"null\" value overrides a schema default value as well)\n      value = formElement[\'value\'];\n    }\n    else if (schemaElement) {\n      // Simply extract the default value from the schema\n      if (isSet(schemaElement[\'default\'])) {\n        value = schemaElement[\'default\'];\n      }\n    }\n    if (value && value.indexOf(\'{{values.\') !== -1) {\n      // This label wants to use the value of another input field.\n      // Convert that construct into {{getValue(key)}} for\n      // Underscore to call the appropriate function of formData\n      // when template gets called (note calling a function is not\n      // exactly Mustache-friendly but is supported by Underscore).\n      value = value.replace(\n        /\\{\\{values\\.([^\\}]+)\\}\\}/g,\n        \'{{getValue(\"$1\")}}\');\n    }\n    if (value) {\n      value = _.template(value, valueTemplateSettings)(tpldata);\n    }\n  }\n\n  // TODO: handle on the formElement.options, because user can setup it too.\n  // Apply titleMap if needed\n  if (isSet(value) && formElement && hasOwnProperty(formElement.titleMap, value)) {\n    value = _.template(formElement.titleMap[value], valueTemplateSettings)(tpldata);\n  }\n\n  // Check maximum length of a string\n  if (value && _.isString(value) &&\n    schemaElement && schemaElement.maxLength) {\n    if (value.length > schemaElement.maxLength) {\n      // Truncate value to maximum length, adding continuation dots\n      value = value.substr(0, schemaElement.maxLength - 1) + \'\xe2\x80\xa6\';\n    }\n  }\n\n  if (!isSet(value)) {\n    return null;\n  }\n  else {\n    return value;\n  }\n};\n\n\n/**\n * Represents a node in the form.\n *\n * Nodes that have an ID are linked to the corresponding DOM element\n * when rendered\n *\n * Note the form element and the schema elements that gave birth to the\n * node may be shared among multiple nodes (in the case of arrays).\n *\n * @class\n */\nvar formNode = function () {\n  /**\n   * The node\'s ID (may not be set)\n   */\n  this.id = null;\n\n  /**\n   * The node\'s key path (may not be set)\n   */\n  this.key = null;\n\n  /**\n   * DOM element associated witht the form element.\n   *\n   * The DOM element is set when the form element is rendered.\n   */\n  this.el = null;\n\n  /**\n   * Link to the form element that describes the node\'s layout\n   * (note the form element is shared among nodes in arrays)\n   */\n  this.formElement = null;\n\n  /**\n   * Link to the schema element that describes the node\'s value constraints\n   * (note the schema element is shared among nodes in arrays)\n   */\n  this.schemaElement = null;\n\n  /**\n   * Pointer to the \"view\" associated with the node, typically the right\n   * object in jsonform.elementTypes\n   */\n  this.view = null;\n\n  /**\n   * Node\'s subtree (if one is defined)\n   */\n  this.children = [];\n\n  /**\n   * A pointer to the form tree the node is attached to\n   */\n  this.ownerTree = null;\n\n  /**\n   * A pointer to the parent node of the node in the tree\n   */\n  this.parentNode = null;\n\n  /**\n   * Child template for array-like nodes.\n   *\n   * The child template gets cloned to create new array items.\n   */\n  this.childTemplate = null;\n\n\n  /**\n   * Direct children of array-like containers may use the value of a\n   * specific input field in their subtree as legend. The link to the\n   * legend child is kept here and initialized in computeInitialValues\n   * when a child sets \"valueInLegend\"\n   */\n  this.legendChild = null;\n\n\n  /**\n   * The path of indexes that lead to the current node when the\n   * form element is not at the root array level.\n   *\n   * Note a form element may well be nested element and still be\n   * at the root array level. That\'s typically the case for \"fieldset\"\n   * elements. An array level only gets created when a form element\n   * is of type \"array\" (or a derivated type such as \"tabarray\").\n   *\n   * The array path of a form element linked to the foo[2].bar.baz[3].toto\n   * element in the submitted values is [2, 3] for instance.\n   *\n   * The array path is typically used to compute the right ID for input\n   * fields. It is also used to update positions when an array item is\n   * created, moved around or suppressed.\n   *\n   * @type {Array(Number)}\n   */\n  this.arrayPath = [];\n\n  /**\n   * Position of the node in the list of children of its parents\n   */\n  this.childPos = 0;\n};\n\n\n/**\n * Clones a node\n *\n * @function\n * @param {formNode} New parent node to attach the node to\n * @return {formNode} Cloned node\n */\nformNode.prototype.clone = function (parentNode) {\n  var node = new formNode();\n  node.arrayPath = _.clone(this.arrayPath);\n  node.ownerTree = this.ownerTree;\n  node.parentNode = parentNode || this.parentNode;\n  node.formElement = this.formElement;\n  node.schemaElement = this.schemaElement;\n  node.view = this.view;\n  node.children = _.map(this.children, function (child) {\n    return child.clone(node);\n  });\n  if (this.childTemplate) {\n    node.childTemplate = this.childTemplate.clone(node);\n  }\n  return node;\n};\n\n\n/**\n * Returns true if the subtree that starts at the current node\n * has some non empty value attached to it\n */\nformNode.prototype.hasNonDefaultValue = function () {\n\n  // hidden elements don\'t count because they could make the wrong selectfieldset element active\n  if (this.formElement && this.formElement.type==\"hidden\") {\n    return false;\n  }\n\n  if (this.value && !this.defaultValue) {\n    return true;\n  }\n  var child = _.find(this.children, function (child) {\n    return child.hasNonDefaultValue();\n  });\n  return !!child;\n};\n\n\n/**\n * Attaches a child node to the current node.\n *\n * The child node is appended to the end of the list.\n *\n * @function\n * @param {formNode} node The child node to append\n * @return {formNode} The inserted node (same as the one given as parameter)\n */\nformNode.prototype.appendChild = function (node) {\n  node.parentNode = this;\n  node.childPos = this.children.length;\n  this.children.push(node);\n  return node;\n};\n\n\n/**\n * Removes the last child of the node.\n *\n * @function\n */\nformNode.prototype.removeChild = function () {\n  var child = this.children[this.children.length-1];\n  if (!child) return;\n\n  // Remove the child from the DOM\n  $(child.el).remove();\n\n  // Remove the child from the array\n  return this.children.pop();\n};\n\n\n/**\n * Moves the user entered values set in the current node\'s subtree to the\n * given node\'s subtree.\n *\n * The target node must follow the same structure as the current node\n * (typically, they should have been generated from the same node template)\n *\n * The current node MUST be rendered in the DOM.\n *\n * TODO: when current node is not in the DOM, extract values from formNode.value\n * properties, so that the function be available even when current node is not\n * in the DOM.\n *\n * Moving values around allows to insert/remove array items at arbitrary\n * positions.\n *\n * @function\n * @param {formNode} node Target node.\n */\nformNode.prototype.moveValuesTo = function (node) {\n  var values = this.getFormValues(node.arrayPath);\n  node.resetValues();\n  node.computeInitialValues(values, true);\n};\n\n\n/**\n * Switches nodes user entered values.\n *\n * The target node must follow the same structure as the current node\n * (typically, they should have been generated from the same node template)\n *\n * Both nodes MUST be rendered in the DOM.\n *\n * TODO: update getFormValues to work even if node is not rendered, using\n * formNode\'s \"value\" property.\n *\n * @function\n * @param {formNode} node Target node\n */\nformNode.prototype.switchValuesWith = function (node) {\n  var values = this.getFormValues(node.arrayPath);\n  var nodeValues = node.getFormValues(this.arrayPath);\n  node.resetValues();\n  node.computeInitialValues(values, true);\n  this.resetValues();\n  this.computeInitialValues(nodeValues, true);\n};\n\n\n/**\n * Resets all DOM values in the node\'s subtree.\n *\n * This operation also drops all array item nodes.\n * Note values are not reset to their default values, they are rather removed!\n *\n * @function\n */\nformNode.prototype.resetValues = function () {\n  var params = null;\n  var idx = 0;\n\n  // Reset value\n  this.value = null;\n\n  // Propagate the array path from the parent node\n  // (adding the position of the child for nodes that are direct\n  // children of array-like nodes)\n  if (this.parentNode) {\n    this.arrayPath = _.clone(this.parentNode.arrayPath);\n    if (this.parentNode.view && this.parentNode.view.array) {\n      this.arrayPath.push(this.childPos);\n    }\n  }\n  else {\n    this.arrayPath = [];\n  }\n\n  if (this.view && this.view.inputfield) {\n    // Simple input field, extract the value from the origin,\n    // set the target value and reset the origin value\n    params = $(\':input\', this.el).serializeArray();\n    _.each(params, function (param) {\n      // TODO: check this, there may exist corner cases with this approach\n      // (with multiple checkboxes for instance)\n      $(\'[name=\"\' + escapeSelector(param.name) + \'\"]\', $(this.el)).val(\'\');\n    }, this);\n  }\n  else if (this.view && this.view.array) {\n    // The current node is an array, drop all children\n    while (this.children.length > 0) {\n      this.removeChild();\n    }\n  }\n\n  // Recurse down the tree\n  _.each(this.children, function (child) {\n    child.resetValues();\n  });\n};\n\n\n/**\n * Sets the child template node for the current node.\n *\n * The child template node is used to create additional children\n * in an array-like form element. The template is never rendered.\n *\n * @function\n * @param {formNode} node The child template node to set\n */\nformNode.prototype.setChildTemplate = function (node) {\n  this.childTemplate = node;\n  node.parentNode = this;\n};\n\n\n/**\n * Recursively sets values to all nodes of the current subtree\n * based on previously submitted values, or based on default\n * values when the submitted values are not enough\n *\n * The function should be called once in the lifetime of a node\n * in the tree. It expects its parent\'s arrayPath to be up to date.\n *\n * Three cases may arise:\n * 1. if the form element is a simple input field, the value is\n * extracted from previously submitted values of from default values\n * defined in the schema.\n * 2. if the form element is an array-like node, the child template\n * is used to create as many children as possible (and at least one).\n * 3. the function simply recurses down the node\'s subtree otherwise\n * (this happens when the form element is a fieldset-like element).\n *\n * @function\n * @param {Object} values Previously submitted values for the form\n * @param {Boolean} ignoreDefaultValues Ignore default values defined in the\n *  schema when set.\n */\nformNode.prototype.computeInitialValues = function (values, ignoreDefaultValues) {\n  var self = this;\n  var node = null;\n  var nbChildren = 1;\n  var i = 0;\n  var formData = this.ownerTree.formDesc.tpldata || {};\n\n  // Propagate the array path from the parent node\n  // (adding the position of the child for nodes that are direct\n  // children of array-like nodes)\n  if (this.parentNode) {\n    this.arrayPath = _.clone(this.parentNode.arrayPath);\n    if (this.parentNode.view && this.parentNode.view.array) {\n      this.arrayPath.push(this.childPos);\n    }\n  }\n  else {\n    this.arrayPath = [];\n  }\n\n  // Prepare special data param \"idx\" for templated values\n  // (is is the index of the child in its wrapping array, starting\n  // at 1 since that\'s more human-friendly than a zero-based index)\n  formData.idx = (this.arrayPath.length > 0) ?\n    this.arrayPath[this.arrayPath.length-1] + 1 :\n    this.childPos + 1;\n\n  // Prepare special data param \"value\" for templated values\n  formData.value = \'\';\n\n  // Prepare special function to compute the value of another field\n  formData.getValue = function (key) {\n    if (!values) {\n      return \'\';\n    }\n    var returnValue = values;\n    var listKey = key.split(\'[].\');\n    var i;\n    for (i = 0; i < listKey.length - 1; i++) {\n      returnValue = returnValue[listKey[i]][self.arrayPath[i]];\n    }\n    return returnValue[listKey[i]];\n  };\n\n  if (this.formElement) {\n    // Compute the ID of the field (if needed)\n    if (this.formElement.id) {\n      this.id = applyArrayPath(this.formElement.id, this.arrayPath);\n    }\n    else if (this.view && this.view.array) {\n      this.id = escapeSelector(this.ownerTree.formDesc.prefix) +\n        \'-elt-counter-\' + _.uniqueId();\n    }\n    else if (this.parentNode && this.parentNode.view &&\n      this.parentNode.view.array) {\n      // Array items need an array to associate the right DOM element\n      // to the form node when the parent is rendered.\n      this.id = escapeSelector(this.ownerTree.formDesc.prefix) +\n        \'-elt-counter-\' + _.uniqueId();\n    }\n    else if ((this.formElement.type === \'button\') ||\n      (this.formElement.type === \'selectfieldset\') ||\n      (this.formElement.type === \'question\') ||\n      (this.formElement.type === \'buttonquestion\')) {\n      // Buttons do need an id for \"onClick\" purpose\n      this.id = escapeSelector(this.ownerTree.formDesc.prefix) +\n        \'-elt-counter-\' + _.uniqueId();\n    }\n\n    // Compute the actual key (the form element\'s key is index-free,\n    // i.e. it looks like foo[].bar.baz[].truc, so we need to apply\n    // the array path of the node to get foo[4].bar.baz[2].truc)\n    if (this.formElement.key) {\n      this.key = applyArrayPath(this.formElement.key, this.arrayPath);\n      this.keydash = slugify(this.key.replace(/\\./g, \'---\'));\n    }\n\n    // Same idea for the field\'s name\n    this.name = applyArrayPath(this.formElement.name, this.arrayPath);\n\n    // Consider that label values are template values and apply the\n    // form\'s data appropriately (note we also apply the array path\n    // although that probably doesn\'t make much sense for labels...)\n    _.each([\n      \'title\',\n      \'legend\',\n      \'description\',\n      \'append\',\n      \'prepend\',\n      \'inlinetitle\',\n      \'helpvalue\',\n      \'value\',\n      \'disabled\',\n      \'placeholder\',\n      \'readOnly\'\n    ], function (prop) {\n      if (_.isString(this.formElement[prop])) {\n        if (this.formElement[prop].indexOf(\'{{values.\') !== -1) {\n          // This label wants to use the value of another input field.\n          // Convert that construct into {{jsonform.getValue(key)}} for\n          // Underscore to call the appropriate function of formData\n          // when template gets called (note calling a function is not\n          // exactly Mustache-friendly but is supported by Underscore).\n          this[prop] = this.formElement[prop].replace(\n            /\\{\\{values\\.([^\\}]+)\\}\\}/g,\n            \'{{getValue(\"$1\")}}\');\n        }\n        else {\n          // Note applying the array path probably doesn\'t make any sense,\n          // but some geek might want to have a label \"foo[].bar[].baz\",\n          // with the [] replaced by the appropriate array path.\n          this[prop] = applyArrayPath(this.formElement[prop], this.arrayPath);\n        }\n        if (this[prop]) {\n          this[prop] = _.template(this[prop], valueTemplateSettings)(formData);\n        }\n      }\n      else {\n        this[prop] = this.formElement[prop];\n      }\n    }, this);\n\n    // Apply templating to options created with \"titleMap\" as well\n    if (this.formElement.options) {\n      this.options = _.map(this.formElement.options, function (option) {\n        var title = null;\n        if (_.isObject(option) && option.title) {\n          // See a few lines above for more details about templating\n          // preparation here.\n          if (option.title.indexOf(\'{{values.\') !== -1) {\n            title = option.title.replace(\n              /\\{\\{values\\.([^\\}]+)\\}\\}/g,\n              \'{{getValue(\"$1\")}}\');\n          }\n          else {\n            title = applyArrayPath(option.title, self.arrayPath);\n          }\n          return _.extend({}, option, {\n            value: (isSet(option.value) ? option.value : \'\'),\n            title: _.template(title, valueTemplateSettings)(formData)\n          });\n        }\n        else {\n          return option;\n        }\n      });\n    }\n  }\n\n  if (this.view && this.view.inputfield && this.schemaElement) {\n    // Case 1: simple input field\n    if (values) {\n      // Form has already been submitted, use former value if defined.\n      // Note we won\'t set the field to its default value otherwise\n      // (since the user has already rejected it)\n      if (isSet(jsonform.util.getObjKey(values, this.key))) {\n        this.value = jsonform.util.getObjKey(values, this.key);\n      } else if (isSet(this.schemaElement[\'default\'])) {\n        // the value is not provided in the values section but the\n        // default is set in the schemaElement (which we have)\n        this.value = this.schemaElement[\'default\']\n        // We only apply a template if it\'s a string\n        if (typeof this.value === \'string\') {\n          this.value = _.template(this.value, valueTemplateSettings)(formData);\n        }\n\n      }\n    }\n    else if (!ignoreDefaultValues) {\n      // No previously submitted form result, use default value\n      // defined in the schema if it\'s available and not already\n      // defined in the form element\n      if (!isSet(this.value) && isSet(this.schemaElement[\'default\'])) {\n        this.value = this.schemaElement[\'default\'];\n        if (_.isString(this.value)) {\n          if (this.value.indexOf(\'{{values.\') !== -1) {\n            // This label wants to use the value of another input field.\n            // Convert that construct into {{jsonform.getValue(key)}} for\n            // Underscore to call the appropriate function of formData\n            // when template gets called (note calling a function is not\n            // exactly Mustache-friendly but is supported by Underscore).\n            this.value = this.value.replace(\n              /\\{\\{values\\.([^\\}]+)\\}\\}/g,\n              \'{{getValue(\"$1\")}}\');\n          }\n          else {\n            // Note applying the array path probably doesn\'t make any sense,\n            // but some geek might want to have a label \"foo[].bar[].baz\",\n            // with the [] replaced by the appropriate array path.\n            this.value = applyArrayPath(this.value, this.arrayPath);\n          }\n          if (this.value) {\n            this.value = _.template(this.value, valueTemplateSettings)(formData);\n          }\n        }\n        this.defaultValue = true;\n      }\n    }\n  }\n  else if (this.view && this.view.array) {\n    // Case 2: array-like node\n    nbChildren = 0;\n    if (values) {\n      nbChildren = this.getPreviousNumberOfItems(values, this.arrayPath);\n    }\n    // TODO: use default values at the array level when form has not been\n    // submitted before. Note it\'s not that easy because each value may\n    // be a complex structure that needs to be pushed down the subtree.\n    // The easiest way is probably to generate a \"values\" object and\n    // compute initial values from that object\n    /*\n    else if (this.schemaElement[\'default\']) {\n      nbChildren = this.schemaElement[\'default\'].length;\n    }\n    */\n    else if (nbChildren === 0) {\n      // If form has already been submitted with no children, the array\n      // needs to be rendered without children. If there are no previously\n      // submitted values, the array gets rendered with one empty item as\n      // it\'s more natural from a user experience perspective. That item can\n      // be removed with a click on the \"-\" button.\n      nbChildren = 1;\n    }\n    for (i = 0; i < nbChildren; i++) {\n      this.appendChild(this.childTemplate.clone());\n    }\n  }\n\n  // Case 3 and in any case: recurse through the list of children\n  _.each(this.children, function (child) {\n    child.computeInitialValues(values, ignoreDefaultValues);\n  });\n\n  // If the node\'s value is to be used as legend for its \"container\"\n  // (typically the array the node belongs to), ensure that the container\n  // has a direct link to the node for the corresponding tab.\n  if (this.formElement && this.formElement.valueInLegend) {\n    node = this;\n    while (node) {\n      if (node.parentNode &&\n        node.parentNode.view &&\n        node.parentNode.view.array) {\n        node.legendChild = this;\n        if (node.formElement && node.formElement.legend) {\n          node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);\n          formData.idx = (node.arrayPath.length > 0) ?\n            node.arrayPath[node.arrayPath.length-1] + 1 :\n            node.childPos + 1;\n          formData.value = isSet(this.value) ? this.value : \'\';\n          node.legend = _.template(node.legend, valueTemplateSettings)(formData);\n          break;\n        }\n      }\n      node = node.parentNode;\n    }\n  }\n};\n\n\n/**\n * Returns the number of items that the array node should have based on\n * previously submitted values.\n *\n * The whole difficulty is that values may be hidden deep in the subtree\n * of the node and may actually target different arrays in the JSON schema.\n *\n * @function\n * @param {Object} values Previously submitted values\n * @param {Array(Number)} arrayPath the array path we\'re interested in\n * @return {Number} The number of items in the array\n */\nformNode.prototype.getPreviousNumberOfItems = function (values, arrayPath) {\n  var key = null;\n  var arrayValue = null;\n  var childNumbers = null;\n  var idx = 0;\n\n  if (!values) {\n    // No previously submitted values, no need to go any further\n    return 0;\n  }\n\n  if (this.view.inputfield && this.schemaElement) {\n    // Case 1: node is a simple input field that links to a key in the schema.\n    // The schema key looks typically like:\n    //  foo.bar[].baz.toto[].truc[].bidule\n    // The goal is to apply the array path and truncate the key to the last\n    // array we\'re interested in, e.g. with an arrayPath [4, 2]:\n    //  foo.bar[4].baz.toto[2]\n    key = truncateToArrayDepth(this.formElement.key, arrayPath.length);\n    key = applyArrayPath(key, arrayPath);\n    arrayValue = jsonform.util.getObjKey(values, key);\n    if (!arrayValue) {\n      // No key? That means this field had been left empty\n      // in previous submit\n      return 0;\n    }\n    childNumbers = _.map(this.children, function (child) {\n      return child.getPreviousNumberOfItems(values, arrayPath);\n    });\n    return _.max([_.max(childNumbers) || 0, arrayValue.length]);\n  }\n  else if (this.view.array) {\n    // Case 2: node is an array-like node, look for input fields\n    // in its child template\n    return this.childTemplate.getPreviousNumberOfItems(values, arrayPath);\n  }\n  else {\n    // Case 3: node is a leaf or a container,\n    // recurse through the list of children and return the maximum\n    // number of items found in each subtree\n    childNumbers = _.map(this.children, function (child) {\n      return child.getPreviousNumberOfItems(values, arrayPath);\n    });\n    return _.max(childNumbers) || 0;\n  }\n};\n\n\n/**\n * Returns the structured object that corresponds to the form values entered\n * by the user for the node\'s subtree.\n *\n * The returned object follows the structure of the JSON schema that gave\n * birth to the form.\n *\n * Obviously, the node must have been rendered before that function may\n * be called.\n *\n * @function\n * @param {Array(Number)} updateArrayPath Array path to use to pretend that\n *  the entered values were actually entered for another item in an array\n *  (this is used to move values around when an item is inserted/removed/moved\n *  in an array)\n * @return {Object} The object that follows the data schema and matches the\n *  values entered by the user.\n */\nformNode.prototype.getFormValues = function (updateArrayPath) {\n  // The values object that will be returned\n  var values = {};\n\n  if (!this.el) {\n    throw new Error(\'formNode.getFormValues can only be called on nodes that are associated with a DOM element in the tree\');\n  }\n\n  // Form fields values\n  var formArray = $(\':input\', this.el).serializeArray();\n\n  // Set values to false for unset checkboxes and radio buttons\n  // because serializeArray() ignores them\n  formArray = formArray.concat(\n    $(\':input[type=checkbox]:not(:disabled):not(:checked)\', this.el).map( function() {\n      return {\"name\": this.name, \"value\": this.checked}\n    }).get()\n  );\n\n  if (updateArrayPath) {\n    _.each(formArray, function (param) {\n      param.name = applyArrayPath(param.name, updateArrayPath);\n    });\n  }\n\n  // The underlying data schema\n  var formSchema = this.ownerTree.formDesc.schema;\n\n  for (var i = 0; i < formArray.length; i++) {\n    // Retrieve the key definition from the data schema\n    var name = formArray[i].name;\n    var eltSchema = getSchemaKey(formSchema.properties, name);\n    var arrayMatch = null;\n    var cval = null;\n\n    // Skip the input field if it\'s not part of the schema\n    if (!eltSchema) continue;\n\n    // Handle multiple checkboxes separately as the idea is to generate\n    // an array that contains the list of enumeration items that the user\n    // selected.\n    if (eltSchema._jsonform_checkboxes_as_array) {\n      arrayMatch = name.match(/\\[([0-9]*)\\]$/);\n      if (arrayMatch) {\n        name = name.replace(/\\[([0-9]*)\\]$/, \'\');\n        cval = jsonform.util.getObjKey(values, name) || [];\n        if (formArray[i].value === \'1\') {\n          // Value selected, push the corresponding enumeration item\n          // to the data result\n          cval.push(eltSchema[\'enum\'][parseInt(arrayMatch[1],10)]);\n        }\n        jsonform.util.setObjKey(values, name, cval);\n        continue;\n      }\n    }\n\n    // Type casting\n    if (eltSchema.type === \'boolean\') {\n      if (formArray[i].value === \'0\') {\n        formArray[i].value = false;\n      } else {\n        formArray[i].value = !!formArray[i].value;\n      }\n    }\n    if ((eltSchema.type === \'number\') ||\n      (eltSchema.type === \'integer\')) {\n      if (_.isString(formArray[i].value)) {\n        if (!formArray[i].value.length) {\n          formArray[i].value = null;\n        } else if (!isNaN(Number(formArray[i].value))) {\n          formArray[i].value = Number(formArray[i].value);\n        }\n      }\n    }\n    if ((eltSchema.type === \'string\') &&\n      (formArray[i].value === \'\') &&\n      !eltSchema._jsonform_allowEmpty) {\n      formArray[i].value=null;\n    }\n    if ((eltSchema.type === \'object\') &&\n      _.isString(formArray[i].value) &&\n      (formArray[i].value.substring(0,1) === \'{\')) {\n      try {\n        formArray[i].value = JSON.parse(formArray[i].value);\n      } catch (e) {\n        formArray[i].value = {};\n      }\n    }\n    //TODO: is this due to a serialization bug?\n    if ((eltSchema.type === \'object\') &&\n      (formArray[i].value === \'null\' || formArray[i].value === \'\')) {\n      formArray[i].value = null;\n    }\n\n    if (formArray[i].name && (formArray[i].value !== null)) {\n      jsonform.util.setObjKey(values, formArray[i].name, formArray[i].value);\n    }\n  }\n  return values;\n};\n\n\n\n/**\n * Renders the node.\n *\n * Rendering is done in three steps: HTML generation, DOM element creation\n * and insertion, and an enhance step to bind event handlers.\n *\n * @function\n * @param {Node} el The DOM element where the node is to be rendered. The\n *  node is inserted at the right position based on its \"childPos\" property.\n */\nformNode.prototype.render = function (el) {\n  var html = this.generate();\n  this.setContent(html, el);\n  this.enhance();\n};\n\n\n/**\n * Inserts/Updates the HTML content of the node in the DOM.\n *\n * If the HTML is an update, the new HTML content replaces the old one.\n * The new HTML content is not moved around in the DOM in particular.\n *\n * The HTML is inserted at the right position in its parent\'s DOM subtree\n * otherwise (well, provided there are enough children, but that should always\n * be the case).\n *\n * @function\n * @param {string} html The HTML content to render\n * @param {Node} parentEl The DOM element that is to contain the DOM node.\n *  This parameter is optional (the node\'s parent is used otherwise) and\n *  is ignored if the node to render is already in the DOM tree.\n */\nformNode.prototype.setContent = function (html, parentEl) {\n  var node = $(html);\n  var parentNode = parentEl ||\n    (this.parentNode ? this.parentNode.el : this.ownerTree.domRoot);\n  var nextSibling = null;\n\n  if (this.el) {\n    // Replace the contents of the DOM element if the node is already in the tree\n    $(this.el).replaceWith(node);\n  }\n  else {\n    // Insert the node in the DOM if it\'s not already there\n    nextSibling = $(parentNode).children().get(this.childPos);\n    if (nextSibling) {\n      $(nextSibling).before(node);\n    }\n    else {\n      $(parentNode).append(node);\n    }\n  }\n\n  // Save the link between the form node and the generated HTML\n  this.el = node;\n\n  // Update the node\'s subtree, extracting DOM elements that match the nodes\n  // from the generated HTML\n  this.updateElement(this.el);\n};\n\n\n/**\n * Updates the DOM element associated with the node.\n *\n * Only nodes that have ID are directly associated with a DOM element.\n *\n * @function\n */\nformNode.prototype.updateElement = function (domNode) {\n  if (this.id) {\n    this.el = $(\'#\' + escapeSelector(this.id), domNode).get(0);\n    if (this.view && this.view.getElement) {\n      this.el = this.view.getElement(this.el);\n    }\n    if ((this.fieldtemplate !== false) &&\n      this.view && this.view.fieldtemplate) {\n      // The field template wraps the element two or three level deep\n      // in the DOM tree, depending on whether there is anything prepended\n      // or appended to the input field\n      this.el = $(this.el).parent().parent();\n      if (this.prepend || this.prepend) {\n        this.el = this.el.parent();\n      }\n      this.el = this.el.get(0);\n    }\n    if (this.parentNode && this.parentNode.view &&\n      this.parentNode.view.childTemplate) {\n      // TODO: the child template may introduce more than one level,\n      // so the number of levels introduced should rather be exposed\n      // somehow in jsonform.fieldtemplate.\n      this.el = $(this.el).parent().get(0);\n    }\n  }\n\n  for (const k in  this.children) {\n     if (this.children.hasOwnProperty(k) == false) {\n         continue;\n     }\n     this.children[k].updateElement(this.el || domNode);\n }\n};\n\n\n/**\n * Generates the view\'s HTML content for the underlying model.\n *\n * @function\n */\nformNode.prototype.generate = function () {\n  var data = {\n    id: this.id,\n    keydash: this.keydash,\n    elt: this.formElement,\n    schema: this.schemaElement,\n    node: this,\n    value: isSet(this.value) ? this.value : \'\',\n    escape: escapeHTML\n  };\n  var template = null;\n  var html = \'\';\n\n  // Complete the data context if needed\n  if (this.ownerTree.formDesc.onBeforeRender) {\n    this.ownerTree.formDesc.onBeforeRender(data, this);\n  }\n  if (this.view.onBeforeRender) {\n    this.view.onBeforeRender(data, this);\n  }\n\n  // Use the template that \'onBeforeRender\' may have set,\n  // falling back to that of the form element otherwise\n  if (this.template) {\n    template = this.template;\n  }\n  else if (this.formElement && this.formElement.template) {\n    template = this.formElement.template;\n  }\n  else {\n    template = this.view.template;\n  }\n\n  // Wrap the view template in the generic field template\n  // (note the strict equality to \'false\', needed as we fallback\n  // to the view\'s setting otherwise)\n  if ((this.fieldtemplate !== false) &&\n    (this.fieldtemplate || this.view.fieldtemplate)) {\n    template = jsonform.fieldTemplate(template);\n  }\n\n  // Wrap the content in the child template of its parent if necessary.\n  if (this.parentNode && this.parentNode.view &&\n    this.parentNode.view.childTemplate) {\n    // only allow drag of children if default or enabled\n    template = this.parentNode.view.childTemplate(template, (!isSet(this.parentNode.formElement.draggable) ? true : this.parentNode.formElement.draggable));\n  }\n\n  // Prepare the HTML of the children\n  var childrenhtml = \'\';\n  _.each(this.children, function (child) {\n    childrenhtml += child.generate();\n  });\n  data.children = childrenhtml;\n\n  data.fieldHtmlClass = \'\';\n  if (this.ownerTree &&\n      this.ownerTree.formDesc &&\n      this.ownerTree.formDesc.params &&\n      this.ownerTree.formDesc.params.fieldHtmlClass) {\n    data.fieldHtmlClass = this.ownerTree.formDesc.params.fieldHtmlClass;\n  }\n  if (this.formElement &&\n      (typeof this.formElement.fieldHtmlClass !== \'undefined\')) {\n    data.fieldHtmlClass = this.formElement.fieldHtmlClass;\n  }\n\n  // Apply the HTML template\n  html = _.template(template, fieldTemplateSettings)(data);\n  return html;\n};\n\n\n/**\n * Enhances the view with additional logic, binding event handlers\n * in particular.\n *\n * The function also runs the \"insert\" event handler of the view and\n * form element if they exist (starting with that of the view)\n *\n * @function\n */\nformNode.prototype.enhance = function () {\n  var node = this;\n  var handlers = null;\n  var handler = null;\n  var formData = _.clone(this.ownerTree.formDesc.tpldata) || {};\n\n  if (this.formElement) {\n    // Check the view associated with the node as it may define an \"onInsert\"\n    // event handler to be run right away\n    if (this.view.onInsert) {\n      this.view.onInsert({ target: $(this.el) }, this);\n    }\n\n    handlers = this.handlers || this.formElement.handlers;\n\n    // Trigger the \"insert\" event handler\n    handler = this.onInsert || this.formElement.onInsert;\n    if (handler) {\n      handler({ target: $(this.el) }, this);\n    }\n    if (handlers) {\n      _.each(handlers, function (handler, onevent) {\n        if (onevent === \'insert\') {\n          handler({ target: $(this.el) }, this);\n        }\n      }, this);\n    }\n\n    // No way to register event handlers if the DOM element is unknown\n    // TODO: find some way to register event handlers even when this.el is not set.\n    if (this.el) {\n\n      // Register specific event handlers\n      // TODO: Add support for other event handlers\n      if (this.onChange)\n        $(this.el).bind(\'change\', function(evt) { node.onChange(evt, node); });\n      if (this.view.onChange)\n        $(this.el).bind(\'change\', function(evt) { node.view.onChange(evt, node); });\n      if (this.formElement.onChange)\n        $(this.el).bind(\'change\', function(evt) { node.formElement.onChange(evt, node); });\n\n      if (this.onInput)\n        $(this.el).bind(\'input\', function(evt) { node.onInput(evt, node); });\n      if (this.view.onInput)\n        $(this.el).bind(\'input\', function(evt) { node.view.onInput(evt, node); });\n      if (this.formElement.onInput)\n        $(this.el).bind(\'input\', function(evt) { node.formElement.onInput(evt, node); });\n\n      if (this.onClick)\n        $(this.el).bind(\'click\', function(evt) { node.onClick(evt, node); });\n      if (this.view.onClick)\n        $(this.el).bind(\'click\', function(evt) { node.view.onClick(evt, node); });\n      if (this.formElement.onClick)\n        $(this.el).bind(\'click\', function(evt) { node.formElement.onClick(evt, node); });\n\n      if (this.onKeyUp)\n        $(this.el).bind(\'keyup\', function(evt) { node.onKeyUp(evt, node); });\n      if (this.view.onKeyUp)\n        $(this.el).bind(\'keyup\', function(evt) { node.view.onKeyUp(evt, node); });\n      if (this.formElement.onKeyUp)\n        $(this.el).bind(\'keyup\', function(evt) { node.formElement.onKeyUp(evt, node); });\n\n      if (handlers) {\n        _.each(handlers, function (handler, onevent) {\n          if (onevent !== \'insert\') {\n            $(this.el).bind(onevent, function(evt) { handler(evt, node); });\n          }\n        }, this);\n      }\n    }\n\n    // Auto-update legend based on the input field that\'s associated with it\n    if (this.legendChild && this.legendChild.formElement) {\n      var onChangeHandler = function (evt) {\n        if (node.formElement && node.formElement.legend && node.parentNode) {\n          node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);\n          formData.idx = (node.arrayPath.length > 0) ?\n              node.arrayPath[node.arrayPath.length - 1] + 1 :\n              node.childPos + 1;\n          formData.value = $(evt.target).val();\n          node.legend = _.template(node.legend, valueTemplateSettings)(formData);\n          $(node.parentNode.el).trigger(\'legendUpdated\');\n        }\n      };\n      $(this.legendChild.el).bind(\'change\', onChangeHandler);\n      $(this.legendChild.el).bind(\'keyup\', onChangeHandler);\n    }\n  }\n\n  // Recurse down the tree to enhance children\n  _.each(this.children, function (child) {\n    child.enhance();\n  });\n};\n\n\n\n/**\n * Inserts an item in the array at the requested position and renders the item.\n *\n * @function\n * @param {Number} idx Insertion index\n */\nformNode.prototype.insertArrayItem = function (idx, domElement) {\n  var i = 0;\n\n  // Insert element at the end of the array if index is not given\n  if (idx === undefined) {\n    idx = this.children.length;\n  }\n\n  // Create the additional array item at the end of the list,\n  // using the item template created when tree was initialized\n  // (the call to resetValues ensures that \'arrayPath\' is correctly set)\n  var child = this.childTemplate.clone();\n  this.appendChild(child);\n  child.resetValues();\n\n  // To create a blank array item at the requested position,\n  // shift values down starting at the requested position\n  // one to insert (note we start with the end of the array on purpose)\n  for (i = this.children.length-2; i >= idx; i--) {\n    this.children[i].moveValuesTo(this.children[i+1]);\n  }\n\n  // Initialize the blank node we\'ve created with default values\n  this.children[idx].resetValues();\n  this.children[idx].computeInitialValues();\n\n  // Re-render all children that have changed\n  for (i = idx; i < this.children.length; i++) {\n    this.children[i].render(domElement);\n  }\n};\n\n\n/**\n * Remove an item from an array\n *\n * @function\n * @param {Number} idx The index number of the item to remove\n */\nformNode.prototype.deleteArrayItem = function (idx) {\n  var i = 0;\n  var child = null;\n\n  // Delete last item if no index is given\n  if (idx === undefined) {\n    idx = this.children.length - 1;\n  }\n\n  // Move values up in the array\n  for (i = idx; i < this.children.length-1; i++) {\n    this.children[i+1].moveValuesTo(this.children[i]);\n    this.children[i].render();\n  }\n\n  // Remove the last array item from the DOM tree and from the form tree\n  this.removeChild();\n};\n\n/**\n * Returns the minimum/maximum number of items that an array field\n * is allowed to have according to the schema definition of the fields\n * it contains.\n *\n * The function parses the schema definitions of the array items that\n * compose the current \"array\" node and returns the minimum value of\n * \"maxItems\" it encounters as the maximum number of items, and the\n * maximum value of \"minItems\" as the minimum number of items.\n *\n * The function reports a -1 for either of the boundaries if the schema\n * does not put any constraint on the number of elements the current\n * array may have of if the current node is not an array.\n *\n * Note that array boundaries should be defined in the JSON Schema using\n * \"minItems\" and \"maxItems\". The code also supports \"minLength\" and\n * \"maxLength\" as a fallback, mostly because it used to by mistake (see #22)\n * and because other people could make the same mistake.\n *\n * @function\n * @return {Object} An object with properties \"minItems\" and \"maxItems\"\n *  that reports the corresponding number of items that the array may\n *  have (value is -1 when there is no constraint for that boundary)\n */\nformNode.prototype.getArrayBoundaries = function () {\n  var boundaries = {\n    minItems: -1,\n    maxItems: -1\n  };\n  if (!this.view || !this.view.array) return boundaries;\n\n  var getNodeBoundaries = function (node, initialNode) {\n    var schemaKey = null;\n    var arrayKey = null;\n    var boundaries = {\n      minItems: -1,\n      maxItems: -1\n    };\n    initialNode = initialNode || node;\n\n    if (node.view && node.view.array && (node !== initialNode)) {\n      // New array level not linked to an array in the schema,\n      // so no size constraints\n      return boundaries;\n    }\n\n    if (node.key) {\n      // Note the conversion to target the actual array definition in the\n      // schema where minItems/maxItems may be defined. If we\'re still looking\n      // at the initial node, the goal is to convert from:\n      //  foo[0].bar[3].baz to foo[].bar[].baz\n      // If we\'re not looking at the initial node, the goal is to look at the\n      // closest array parent:\n      //  foo[0].bar[3].baz to foo[].bar\n      arrayKey = node.key.replace(/\\[[0-9]+\\]/g, \'[]\');\n      if (node !== initialNode) {\n        arrayKey = arrayKey.replace(/\\[\\][^\\[\\]]*$/, \'\');\n      }\n      schemaKey = getSchemaKey(\n        node.ownerTree.formDesc.schema.properties,\n        arrayKey\n      );\n      if (!schemaKey) return boundaries;\n      return {\n        minItems: schemaKey.minItems || schemaKey.minLength || -1,\n        maxItems: schemaKey.maxItems || schemaKey.maxLength || -1\n      };\n    }\n    else {\n      _.each(node.children, function (child) {\n        var subBoundaries = getNodeBoundaries(child, initialNode);\n        if (subBoundaries.minItems !== -1) {\n          if (boundaries.minItems !== -1) {\n            boundaries.minItems = Math.max(\n              boundaries.minItems,\n              subBoundaries.minItems\n            );\n          }\n          else {\n            boundaries.minItems = subBoundaries.minItems;\n          }\n        }\n        if (subBoundaries.maxItems !== -1) {\n          if (boundaries.maxItems !== -1) {\n            boundaries.maxItems = Math.min(\n              boundaries.maxItems,\n              subBoundaries.maxItems\n            );\n          }\n          else {\n            boundaries.maxItems = subBoundaries.maxItems;\n          }\n        }\n      });\n    }\n    return boundaries;\n  };\n  return getNodeBoundaries(this);\n};\n\n\n/**\n * Form tree class.\n *\n * Holds the internal representation of the form.\n * The tree is always in sync with the rendered form, this allows to parse\n * it easily.\n *\n * @class\n */\nvar formTree = function () {\n  this.eventhandlers = [];\n  this.root = null;\n  this.formDesc = null;\n};\n\n/**\n * Initializes the form tree structure from the JSONForm object\n *\n * This function is the main entry point of the JSONForm library.\n *\n * Initialization steps:\n * 1. the internal tree structure that matches the JSONForm object\n *  gets created (call to buildTree)\n * 2. initial values are computed from previously submitted values\n *  or from the default values defined in the JSON schema.\n *\n * When the function returns, the tree is ready to be rendered through\n * a call to \"render\".\n *\n * @function\n */\nformTree.prototype.initialize = function (formDesc) {\n  formDesc = formDesc || {};\n\n  // Keep a pointer to the initial JSONForm\n  // (note clone returns a shallow copy, only first-level is cloned)\n  this.formDesc = _.clone(formDesc);\n\n  // Compute form prefix if no prefix is given.\n  this.formDesc.prefix = this.formDesc.prefix ||\n    \'jsonform-\' + _.uniqueId();\n\n  // JSON schema shorthand\n  if (this.formDesc.schema && !this.formDesc.schema.properties) {\n    this.formDesc.schema = {\n      properties: this.formDesc.schema\n    };\n  }\n\n  // Ensure layout is set\n  this.formDesc.form = this.formDesc.form || [\n    \'*\',\n    {\n      type: \'actions\',\n      items: [\n        {\n          type: \'submit\',\n          value: \'Submit\'\n        }\n      ]\n    }\n  ];\n  this.formDesc.form = (_.isArray(this.formDesc.form) ?\n    this.formDesc.form :\n    [this.formDesc.form]);\n\n  this.formDesc.params = this.formDesc.params || {};\n\n  // Create the root of the tree\n  this.root = new formNode();\n  this.root.ownerTree = this;\n  this.root.view = jsonform.elementTypes[\'root\'];\n\n  // Generate the tree from the form description\n  this.buildTree();\n\n  // Compute the values associated with each node\n  // (for arrays, the computation actually creates the form nodes)\n  this.computeInitialValues();\n};\n\n\n/**\n * Constructs the tree from the form description.\n *\n * The function must be called once when the tree is first created.\n *\n * @function\n */\nformTree.prototype.buildTree = function () {\n  // Parse and generate the form structure based on the elements encountered:\n  // - \'*\' means \"generate all possible fields using default layout\"\n  // - a key reference to target a specific data element\n  // - a more complex object to generate specific form sections\n  _.each(this.formDesc.form, function (formElement) {\n    if (formElement === \'*\') {\n      _.each(this.formDesc.schema.properties, function (element, key) {\n        this.root.appendChild(this.buildFromLayout({\n          key: key\n        }));\n      }, this);\n    }\n    else {\n      if (_.isString(formElement)) {\n        formElement = {\n          key: formElement\n        };\n      }\n      this.root.appendChild(this.buildFromLayout(formElement));\n    }\n  }, this);\n};\n\n\n/**\n * Builds the internal form tree representation from the requested layout.\n *\n * The function is recursive, generating the node children as necessary.\n * The function extracts the values from the previously submitted values\n * (this.formDesc.value) or from default values defined in the schema.\n *\n * @function\n * @param {Object} formElement JSONForm element to render\n * @param {Object} context The parsing context (the array depth in particular)\n * @return {Object} The node that matches the element.\n */\nformTree.prototype.buildFromLayout = function (formElement, context) {\n  var schemaElement = null;\n  var node = new formNode();\n  var view = null;\n  var key = null;\n\n  // The form element parameter directly comes from the initial\n  // JSONForm object. We\'ll make a shallow copy of it and of its children\n  // not to pollute the original object.\n  // (note JSON.parse(JSON.stringify()) cannot be used since there may be\n  // event handlers in there!)\n  formElement = _.clone(formElement);\n  if (formElement.items) {\n    if (_.isArray(formElement.items)) {\n      formElement.items = _.map(formElement.items, _.clone);\n    }\n    else {\n      formElement.items = [ _.clone(formElement.items) ];\n    }\n  }\n\n  if (formElement.key) {\n    // The form element is directly linked to an element in the JSON\n    // schema. The properties of the form element override those of the\n    // element in the JSON schema. Properties from the JSON schema complete\n    // those of the form element otherwise.\n\n    // Retrieve the element from the JSON schema\n    schemaElement = getSchemaKey(\n      this.formDesc.schema.properties,\n      formElement.key);\n    if (!schemaElement) {\n      // The JSON Form is invalid!\n      throw new Error(\'The JSONForm object references the schema key \"\' +\n        formElement.key + \'\" but that key does not exist in the JSON schema\');\n    }\n\n    // Schema element has just been found, let\'s trigger the\n    // \"onElementSchema\" event\n    // (tidoust: not sure what the use case for this is, keeping the\n    // code for backward compatibility)\n    if (this.formDesc.onElementSchema) {\n      this.formDesc.onElementSchema(formElement, schemaElement);\n    }\n\n    formElement.name =\n      formElement.name ||\n      formElement.key;\n    formElement.title =\n      formElement.title ||\n      schemaElement.title;\n    formElement.description =\n      formElement.description ||\n      schemaElement.description;\n    formElement.readOnly =\n      formElement.readOnly ||\n      schemaElement.readOnly ||\n      formElement.readonly ||\n      schemaElement.readonly;\n\n    // Compute the ID of the input field\n    if (!formElement.id) {\n      formElement.id = escapeSelector(this.formDesc.prefix) +\n        \'-elt-\' + slugify(formElement.key);\n    }\n\n    // Should empty strings be included in the final value?\n    // TODO: it\'s rather unclean to pass it through the schema.\n    if (formElement.allowEmpty) {\n      schemaElement._jsonform_allowEmpty = true;\n    }\n\n    // If the form element does not define its type, use the type of\n    // the schema element.\n    if (!formElement.type) {\n      // If schema type is an array containing only a type and \"null\",\n      // remove null and make the element non-required\n      if (_.isArray(schemaElement.type)) {\n        if (_.contains(schemaElement.type, \"null\")) {\n          schemaElement.type = _.without(schemaElement.type, \"null\");\n          schemaElement.required = false;\n        }\n        if (schemaElement.type.length > 1) {\n          throw new Error(\"Cannot process schema element with multiple types.\");\n        }\n        schemaElement.type = _.first(schemaElement.type);\n      }\n\n      if ((schemaElement.type === \'string\') &&\n        (schemaElement.format === \'color\')) {\n        formElement.type = \'color\';\n      } else if ((schemaElement.type === \'number\' ||\n        schemaElement.type === \'integer\') &&\n        !schemaElement[\'enum\']) {\n       formElement.type = \'number\';\n       if (schemaElement.type === \'number\') schemaElement.step = \'any\';\n      } else if ((schemaElement.type === \'string\' ||\n        schemaElement.type === \'any\') &&\n        !schemaElement[\'enum\']) {\n        formElement.type = \'text\';\n      } else if (schemaElement.type === \'boolean\') {\n        formElement.type = \'checkbox\';\n      } else if (schemaElement.type === \'object\') {\n        if (schemaElement.properties) {\n          formElement.type = \'fieldset\';\n        } else {\n          formElement.type = \'textarea\';\n        }\n      } else if (!_.isUndefined(schemaElement[\'enum\'])) {\n        formElement.type = \'select\';\n      } else {\n        formElement.type = schemaElement.type;\n      }\n    }\n\n    // Unless overridden in the definition of the form element (or unless\n    // there\'s a titleMap defined), use the enumeration list defined in\n    // the schema\n    if (!formElement.options && schemaElement[\'enum\']) {\n      if (formElement.titleMap) {\n        formElement.options = _.map(schemaElement[\'enum\'], function (value) {\n          return {\n            value: value,\n            title: hasOwnProperty(formElement.titleMap, value) ? formElement.titleMap[value] : value\n          };\n        });\n      }\n      else {\n        formElement.options = schemaElement[\'enum\'];\n      }\n    }\n\n    // Flag a list of checkboxes with multiple choices\n    if ((formElement.type === \'checkboxes\') && schemaElement.items) {\n      var itemsEnum = schemaElement.items[\'enum\'];\n      if (itemsEnum) {\n        schemaElement.items._jsonform_checkboxes_as_array = true;\n      }\n      if (!itemsEnum && schemaElement.items[0]) {\n        itemsEnum = schemaElement.items[0][\'enum\'];\n        if (itemsEnum) {\n          schemaElement.items[0]._jsonform_checkboxes_as_array = true;\n        }\n      }\n    }\n\n    // If the form element targets an \"object\" in the JSON schema,\n    // we need to recurse through the list of children to create an\n    // input field per child property of the object in the JSON schema\n    if (schemaElement.type === \'object\') {\n      _.each(schemaElement.properties, function (prop, propName) {\n        node.appendChild(this.buildFromLayout({\n          key: formElement.key + \'.\' + propName\n        }));\n      }, this);\n    }\n  }\n\n  if (!formElement.type) {\n    formElement.type = \'none\';\n  }\n  view = jsonform.elementTypes[formElement.type];\n  if (!view) {\n    throw new Error(\'The JSONForm contains an element whose type is unknown: \"\' +\n      formElement.type + \'\"\');\n  }\n\n\n  if (schemaElement) {\n    // The form element is linked to an element in the schema.\n    // Let\'s make sure the types are compatible.\n    // In particular, the element must not be a \"container\"\n    // (or must be an \"object\" or \"array\" container)\n    if (!view.inputfield && !view.array &&\n      (formElement.type !== \'selectfieldset\') &&\n      (schemaElement.type !== \'object\')) {\n      throw new Error(\'The JSONForm contains an element that links to an \' +\n        \'element in the JSON schema (key: \"\' + formElement.key + \'\") \' +\n        \'and that should not based on its type (\"\' + formElement.type + \'\")\');\n    }\n  }\n  else {\n    // The form element is not linked to an element in the schema.\n    // This means the form element must be a \"container\" element,\n    // and must not define an input field.\n    if (view.inputfield && (formElement.type !== \'selectfieldset\')) {\n      throw new Error(\'The JSONForm defines an element of type \' +\n        \'\"\' + formElement.type + \'\" \' +\n        \'but no \"key\" property to link the input field to the JSON schema\');\n    }\n  }\n\n  // A few characters need to be escaped to use the ID as jQuery selector\n  formElement.iddot = escapeSelector(formElement.id || \'\');\n\n  // Initialize the form node from the form element and schema element\n  node.formElement = formElement;\n  node.schemaElement = schemaElement;\n  node.view = view;\n  node.ownerTree = this;\n\n  // Set event handlers\n  if (!formElement.handlers) {\n    formElement.handlers = {};\n  }\n\n  // Parse children recursively\n  if (node.view.array) {\n    // The form element is an array. The number of items in an array\n    // is by definition dynamic, up to the form user (through \"Add more\",\n    // \"Delete\" commands). The positions of the items in the array may\n    // also change over time (through \"Move up\", \"Move down\" commands).\n    //\n    // The form node stores a \"template\" node that serves as basis for\n    // the creation of an item in the array.\n    //\n    // Array items may be complex forms themselves, allowing for nesting.\n    //\n    // The initial values set the initial number of items in the array.\n    // Note a form element contains at least one item when it is rendered.\n    if (formElement.items) {\n      key = formElement.items[0] || formElement.items;\n    }\n    else {\n      key = formElement.key + \'[]\';\n    }\n    if (_.isString(key)) {\n      key = { key: key };\n    }\n    node.setChildTemplate(this.buildFromLayout(key));\n  }\n  else if (formElement.items) {\n    // The form element defines children elements\n    _.each(formElement.items, function (item) {\n      if (_.isString(item)) {\n        item = { key: item };\n      }\n      node.appendChild(this.buildFromLayout(item));\n    }, this);\n  }\n\n  return node;\n};\n\n\n/**\n * Computes the values associated with each input field in the tree based\n * on previously submitted values or default values in the JSON schema.\n *\n * For arrays, the function actually creates and inserts additional\n * nodes in the tree based on previously submitted values (also ensuring\n * that the array has at least one item).\n *\n * The function sets the array path on all nodes.\n * It should be called once in the lifetime of a form tree right after\n * the tree structure has been created.\n *\n * @function\n */\nformTree.prototype.computeInitialValues = function () {\n  this.root.computeInitialValues(this.formDesc.value);\n};\n\n\n/**\n * Renders the form tree\n *\n * @function\n * @param {Node} domRoot The \"form\" element in the DOM tree that serves as\n *  root for the form\n */\nformTree.prototype.render = function (domRoot) {\n  if (!domRoot) return;\n  this.domRoot = domRoot;\n  this.root.render();\n\n  // If the schema defines required fields, flag the form with the\n  // \"jsonform-hasrequired\" class for styling purpose\n  // (typically so that users may display a legend)\n  if (this.hasRequiredField()) {\n    $(domRoot).addClass(\'jsonform-hasrequired\');\n  }\n};\n\n/**\n * Walks down the element tree with a callback\n *\n * @function\n * @param {Function} callback The callback to call on each element\n */\nformTree.prototype.forEachElement = function (callback) {\n\n  var f = function(root) {\n    for (var i=0;i<root.children.length;i++) {\n      callback(root.children[i]);\n      f(root.children[i]);\n    }\n  };\n  f(this.root);\n\n};\n\nformTree.prototype.validate = function(noErrorDisplay) {\n\n  var values = jsonform.getFormValue(this.domRoot);\n  var errors = false;\n\n  var options = this.formDesc;\n\n  if (options.validate!==false) {\n    var validator = false;\n    if (typeof options.validate!=\"object\") {\n      if (global.JSONFormValidator) {\n        validator = global.JSONFormValidator.createEnvironment(\"json-schema-draft-03\");\n      }\n    } else {\n      validator = options.validate;\n    }\n    if (validator) {\n      var v = validator.validate(values, this.formDesc.schema);\n      $(this.domRoot).jsonFormErrors(false,options);\n      if (v.errors.length) {\n        if (!errors) errors = [];\n        errors = errors.concat(v.errors);\n      }\n    }\n  }\n\n  if (errors && !noErrorDisplay) {\n    if (options.displayErrors) {\n      options.displayErrors(errors,this.domRoot);\n    } else {\n      $(this.domRoot).jsonFormErrors(errors,options);\n    }\n  }\n\n  return {\"errors\":errors}\n\n}\n\nformTree.prototype.submit = function(evt) {\n\n  var stopEvent = function() {\n    if (evt) {\n      evt.preventDefault();\n      evt.stopPropagation();\n    }\n    return false;\n  };\n  var values = jsonform.getFormValue(this.domRoot);\n  var options = this.formDesc;\n\n  var brk=false;\n  this.forEachElement(function(elt) {\n    if (brk) return;\n    if (elt.view.onSubmit) {\n      brk = !elt.view.onSubmit(evt, elt); //may be called multiple times!!\n    }\n  });\n\n  if (brk) return stopEvent();\n\n  var validated = this.validate();\n\n  if (options.onSubmit && !options.onSubmit(validated.errors,values)) {\n    return stopEvent();\n  }\n\n  if (validated.errors) return stopEvent();\n\n  if (options.onSubmitValid && !options.onSubmitValid(values)) {\n    return stopEvent();\n  }\n\n  return false;\n\n};\n\n\n/**\n * Returns true if the form displays a \"required\" field.\n *\n * To keep things simple, the function parses the form\'s schema and returns\n * true as soon as it finds a \"required\" flag even though, in theory, that\n * schema key may not appear in the final form.\n *\n * Note that a \"required\" constraint on a boolean type is always enforced,\n * the code skips such definitions.\n *\n * @function\n * @return {boolean} True when the form has some required field,\n *  false otherwise.\n */\nformTree.prototype.hasRequiredField = function () {\n  var parseElement = function (element) {\n    if (!element) return null;\n    if (element.required && (element.type !== \'boolean\')) {\n      return element;\n    }\n\n    var prop = _.find(element.properties, function (property) {\n      return parseElement(property);\n    });\n    if (prop) {\n      return prop;\n    }\n\n    if (element.items) {\n      if (_.isArray(element.items)) {\n        prop = _.find(element.items, function (item) {\n          return parseElement(item);\n        });\n      }\n      else {\n        prop = parseElement(element.items);\n      }\n      if (prop) {\n        return prop;\n      }\n    }\n  };\n\n  return parseElement(this.formDesc.schema);\n};\n\n\n/**\n * Returns the structured object that corresponds to the form values entered\n * by the use for the given form.\n *\n * The form must have been previously rendered through a call to jsonform.\n *\n * @function\n * @param {Node} The <form> tag in the DOM\n * @return {Object} The object that follows the data schema and matches the\n *  values entered by the user.\n */\njsonform.getFormValue = function (formelt) {\n  var form = $(formelt).data(\'jsonform-tree\');\n  if (!form) return null;\n  return form.root.getFormValues();\n};\n\n\n/**\n * Highlights errors reported by the JSON schema validator in the document.\n *\n * @function\n * @param {Object} errors List of errors reported by the JSON schema validator\n * @param {Object} options The JSON Form object that describes the form\n *  (unused for the time being, could be useful to store example values or\n *   specific error messages)\n */\n$.fn.jsonFormErrors = function(errors, options) {\n  $(\".error\", this).removeClass(\"error\");\n  $(\".warning\", this).removeClass(\"warning\");\n\n  $(\".jsonform-errortext\", this).hide();\n  if (!errors) return;\n\n  var errorSelectors = [];\n  for (var i = 0; i < errors.length; i++) {\n    // Compute the address of the input field in the form from the URI\n    // returned by the JSON schema validator.\n    // These URIs typically look like:\n    //  urn:uuid:cccc265e-ffdd-4e40-8c97-977f7a512853#/pictures/1/thumbnail\n    // What we need from that is the path in the value object:\n    //  pictures[1].thumbnail\n    // ... and the jQuery-friendly class selector of the input field:\n    //  .jsonform-error-pictures\\[1\\]---thumbnail\n    var key = errors[i].uri\n      .replace(/.*#\\//, \'\')\n      .replace(/\\//g, \'.\')\n      .replace(/\\.([0-9]+)(?=\\.|$)/g, \'[$1]\');\n    var errormarkerclass = \".jsonform-error-\" +\n      escapeSelector(key.replace(/\\./g,\"---\"));\n    errorSelectors.push(errormarkerclass);\n\n    var errorType = errors[i].type || \"error\";\n    $(errormarkerclass, this).addClass(errorType);\n    $(errormarkerclass + \" .jsonform-errortext\", this).html(errors[i].message).show();\n  }\n\n  // Look for the first error in the DOM and ensure the element\n  // is visible so that the user understands that something went wrong\n  errorSelectors = errorSelectors.join(\',\');\n  var firstError = $(errorSelectors).get(0);\n  if (firstError && firstError.scrollIntoView) {\n    firstError.scrollIntoView(true, {\n      behavior: \'smooth\'\n    });\n  }\n};\n\n\n/**\n * Generates the HTML form from the given JSON Form object and renders the form.\n *\n * Main entry point of the library. Defined as a jQuery function that typically\n * needs to be applied to a <form> element in the document.\n *\n * The function handles the following properties for the JSON Form object it\n * receives as parameter:\n * - schema (required): The JSON Schema that describes the form to render\n * - form: The options form layout description, overrides default layout\n * - prefix: String to use to prefix computed IDs. Default is an empty string.\n *  Use this option if JSON Form is used multiple times in an application with\n *  schemas that have overlapping parameter names to avoid running into multiple\n *  IDs issues. Default value is \"jsonform-[counter]\".\n * - transloadit: Transloadit parameters when transloadit is used\n * - validate: Validates form against schema upon submission. Uses the value\n * of the \"validate\" property as validator if it is an object.\n * - displayErrors: Function to call with errors upon form submission.\n *  Default is to render the errors next to the input fields.\n * - submitEvent: Name of the form submission event to bind to.\n *  Default is \"submit\". Set this option to false to avoid event binding.\n * - onSubmit: Callback function to call when form is submitted\n * - onSubmitValid: Callback function to call when form is submitted without\n *  errors.\n *\n * @function\n * @param {Object} options The JSON Form object to use as basis for the form\n */\n$.fn.jsonForm = function(options) {\n  var formElt = this;\n\n  options = _.defaults({}, options, {submitEvent: \'submit\'});\n\n  var form = new formTree();\n  form.initialize(options);\n  form.render(formElt.get(0));\n\n  // TODO: move that to formTree.render\n  if (options.transloadit) {\n    formElt.append(\'<input type=\"hidden\" name=\"params\" value=\\\'\' +\n      escapeHTML(JSON.stringify(options.transloadit.params)) +\n      \'\\\'>\');\n  }\n\n  // Keep a direct pointer to the JSON schema for form submission purpose\n  formElt.data(\"jsonform-tree\", form);\n\n  if (options.submitEvent) {\n    formElt.unbind((options.submitEvent)+\'.jsonform\');\n    formElt.bind((options.submitEvent)+\'.jsonform\', function(evt) {\n      form.submit(evt);\n    });\n  }\n\n  // Initialize tabs sections, if any\n  initializeTabs(formElt);\n\n  // Initialize expandable sections, if any\n  $(\'.expandable > div, .expandable > fieldset\', formElt).hide();\n  formElt.on(\'click\', \'.expandable > legend\', function () {\n    var parent = $(this).parent();\n    parent.toggleClass(\'expanded\');\n    parent.find(\'legend\').attr(\"aria-expanded\", parent.hasClass(\"expanded\"))\n    $(\'> div\', parent).slideToggle(100);\n  });\n\n  return form;\n};\n\n\n/**\n * Retrieves the structured values object generated from the values\n * entered by the user and the data schema that gave birth to the form.\n *\n * Defined as a jQuery function that typically needs to be applied to\n * a <form> element whose content has previously been generated by a\n * call to \"jsonForm\".\n *\n * Unless explicitly disabled, the values are automatically validated\n * against the constraints expressed in the schema.\n *\n * @function\n * @return {Object} Structured values object that matches the user inputs\n *  and the data schema.\n */\n$.fn.jsonFormValue = function() {\n  return jsonform.getFormValue(this);\n};\n\n// Expose the getFormValue method to the global object\n// (other methods exposed as jQuery functions)\nglobal.JSONForm = global.JSONForm || {util:{}};\nglobal.JSONForm.getFormValue = jsonform.getFormValue;\nglobal.JSONForm.fieldTemplate = jsonform.fieldTemplate;\nglobal.JSONForm.fieldTypes = jsonform.elementTypes;\nglobal.JSONForm.getInitialValue = getInitialValue;\nglobal.JSONForm.util.getObjKey = jsonform.util.getObjKey;\nglobal.JSONForm.util.setObjKey = jsonform.util.setObjKey;\n\n})((typeof exports !== \'undefined\'),\n  ((typeof exports !== \'undefined\') ? exports : window),\n  ((typeof jQuery !== \'undefined\') ? jQuery : { fn: {} }),\n  ((typeof _ !== \'undefined\') ? _ : null),\n  JSON);\n";