(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i/g, interpolate : /<%=([\s\S]+?)%>/g }; /** * Template settings for value replacement */ var valueTemplateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; /** * Returns true if given value is neither "undefined" nor null */ var isSet = function (value) { return !(_.isUndefined(value) || _.isNull(value)); }; /** * Returns true if given property is directly property of an object */ var hasOwnProperty = function (obj, prop) { return typeof obj === 'object' && obj.hasOwnProperty(prop); } /** * The jsonform object whose methods will be exposed to the window object */ var jsonform = {util:{}}; // From backbonejs var escapeHTML = function (string) { if (!isSet(string)) { return ''; } string = '' + string; if (!string) { return ''; } return string .replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); }; /** * Escapes selector name for use with jQuery * * All meta-characters listed in jQuery doc are escaped: * http://api.jquery.com/category/selectors/ * * @function * @param {String} selector The jQuery selector to escape * @return {String} The escaped selector. */ var escapeSelector = function (selector) { return selector.replace(/([ \!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '\\$1'); }; /** * * Slugifies a string by replacing spaces with _. Used to create * valid classnames and ids for the form. * * @function * @param {String} str The string to slugify * @return {String} The slugified string. */ var slugify = function(str) { return str.replace(/\ /g, '_'); } /** * Initializes tabular sections in forms. Such sections are generated by the * 'selectfieldset' type of elements in JSON Form. * * Input fields that are not visible are automatically disabled * not to appear in the submitted form. That's on purpose, as tabs * are meant to convey an alternative (and not a sequence of steps). * * The tabs menu is not rendered as tabs but rather as a select field because * it's easier to grasp that it's an alternative. * * Code based on bootstrap-tabs.js, updated to: * - react to option selection instead of tab click * - disable input fields in non visible tabs * - disable the possibility to have dropdown menus (no meaning here) * - act as a regular function instead of as a jQuery plug-in. * * @function * @param {Object} tabs jQuery object that contains the tabular sections * to initialize. The object may reference more than one element. */ var initializeTabs = function (tabs) { var activate = function (element, container) { container .find('> .active') .removeClass('active'); element.addClass('active'); }; var enableFields = function ($target, targetIndex) { // Enable all fields in the targeted tab $target.find('input, textarea, select').removeAttr('disabled'); // Disable all fields in other tabs $target.parent() .children(':not([data-idx=' + targetIndex + '])') .find('input, textarea, select') .attr('disabled', 'disabled'); }; var optionSelected = function (e) { var $option = $("option:selected", $(this)), $select = $(this), // do not use .attr() as it sometimes unexplicably fails targetIdx = $option.get(0).getAttribute('data-idx') || $option.attr('value'), $target; e.preventDefault(); if ($option.hasClass('active')) { return; } $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']'); activate($option, $select); activate($target, $target.parent()); enableFields($target, targetIdx); }; var tabClicked = function (e) { var $a = $('a', $(this)); var $content = $(this).parents('.tabbable').first() .find('.tab-content').first(); var targetIdx = $(this).index(); // The `>` here is to prevent activating selectfieldsets inside a tabarray var $target = $content.find('> [data-idx=' + targetIdx + ']'); e.preventDefault(); activate($(this), $(this).parent()); activate($target, $target.parent()); if ($(this).parent().hasClass('jsonform-alternative')) { enableFields($target, targetIdx); } }; tabs.each(function () { $(this).delegate('select.nav', 'change', optionSelected); $(this).find('select.nav').each(function () { $(this).val($(this).find('.active').attr('value')); // do not use .attr() as it sometimes unexplicably fails var targetIdx = $(this).find('option:selected').get(0).getAttribute('data-idx') || $(this).find('option:selected').attr('value'); var $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']'); enableFields($target, targetIdx); }); $(this).delegate('ul.nav li', 'click', tabClicked); $(this).find('ul.nav li.active').click(); }); }; // Twitter bootstrap-friendly HTML boilerplate for standard inputs jsonform.fieldTemplate = function(inner) { return '
' + '<%= key %>="<%= elt.htmlMetaData[key] %>" ' + '<% }%>' + 'class="form-group jsonform-error-<%= keydash %>' + '<%= elt.htmlClass ? " " + elt.htmlClass : "" %>' + '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " jsonform-required" : "") %>' + '<%= (node.readOnly ? " jsonform-readonly" : "") %>' + '<%= (node.disabled ? " jsonform-disabled" : "") %>' + '">' + '<% if (!elt.notitle) { %>' + '' + '<% } %>' + '
' + '<% if (node.prepend || node.append) { %>' + '
' + '<% if (node.prepend) { %>' + '<%= node.prepend %>' + '<% } %>' + '<% } %>' + inner + '<% if (node.append) { %>' + '<%= node.append %>' + '<% } %>' + '<% if (node.prepend || node.append) { %>' + '
' + '<% } %>' + '<% if (node.description) { %>' + '<%= node.description %>' + '<% } %>' + '' + '
'; }; var fileDisplayTemplate = '
' + '<% if (value.type=="image") { %>' + '' + '<% } else { %>' + '<%= value.name %> (<%= Math.ceil(value.size/1024) %>kB)' + '<% } %>' + '
' + ' '; var inputFieldTemplate = function (type) { return { 'template': '\'' + 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' + '<%= (node.schemaElement && (node.schemaElement.step > 0 || node.schemaElement.step == "any") ? " step=\'" + node.schemaElement.step + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' + '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' + ' />', 'fieldtemplate': true, 'inputfield': true } }; jsonform.elementTypes = { 'none': { 'template': '' }, 'root': { 'template': '
<%= children %>
' }, 'text': inputFieldTemplate('text'), 'password': inputFieldTemplate('password'), 'date': inputFieldTemplate('date'), 'datetime': inputFieldTemplate('datetime'), 'datetime-local': inputFieldTemplate('datetime-local'), 'email': inputFieldTemplate('email'), 'month': inputFieldTemplate('month'), 'number': inputFieldTemplate('number'), 'search': inputFieldTemplate('search'), 'tel': inputFieldTemplate('tel'), 'time': inputFieldTemplate('time'), 'url': inputFieldTemplate('url'), 'week': inputFieldTemplate('week'), 'range': { 'template': '' + 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' + '<%= (node.disabled? " disabled" : "")%>' + ' min=<%= range.min %>' + ' max=<%= range.max %>' + ' step=<%= range.step %>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + ' />', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { data.range = { min: 1, max: 100, step: 1 }; if (!node || !node.schemaElement) return; if (node.formElement && node.formElement.step) { data.range.step = node.formElement.step; } if (typeof node.schemaElement.minimum !== 'undefined') { if (node.schemaElement.exclusiveMinimum) { data.range.min = node.schemaElement.minimum + data.range.step; } else { data.range.min = node.schemaElement.minimum; } } if (typeof node.schemaElement.maximum !== 'undefined') { if (node.schemaElement.exclusiveMaximum) { data.range.max = node.schemaElement.maximum - data.range.step; } else { data.range.max = node.schemaElement.maximum; } } } }, 'color':{ 'template':'' + 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + ' />', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function(evt, node) { $(node.el).find('#' + escapeSelector(node.id)).spectrum({ preferredFormat: "hex", showInput: true }); } }, 'textarea':{ 'template':'', 'fieldtemplate': true, 'inputfield': true }, 'wysihtml5':{ 'template':'', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function (evt, node) { var setup = function () { //protect from double init if ($(node.el).data("wysihtml5")) return; $(node.el).data("wysihtml5_loaded",true); $(node.el).find('#' + escapeSelector(node.id)).wysihtml5({ "html": true, "link": true, "font-styles":true, "image": false, "events": { "load": function () { // In chrome, if an element is required and hidden, it leads to // the error 'An invalid form control with name='' is not focusable' // See http://stackoverflow.com/questions/7168645/invalid-form-control-only-in-google-chrome $(this.textareaElement).removeAttr('required'); } } }); }; // Is there a setup hook? if (window.jsonform_wysihtml5_setup) { window.jsonform_wysihtml5_setup(setup); return; } // Wait until wysihtml5 is loaded var itv = window.setInterval(function() { if (window.wysihtml5) { window.clearInterval(itv); setup(); } },1000); } }, 'ace':{ 'template':'
;">
;height:<%= elt.height || "300px" %>;">
', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function (evt, node) { var setup = function () { var formElement = node.formElement || {}; var ace = window.ace; var editor = ace.edit($(node.el).find('#' + escapeSelector(node.id) + '__ace').get(0)); var idSelector = '#' + escapeSelector(node.id) + '__hidden'; // Force editor to use "\n" for new lines, not to bump into ACE "\r" conversion issue // (ACE is ok with "\r" on pasting but fails to return "\r" when value is extracted) editor.getSession().setNewLineMode('unix'); editor.renderer.setShowPrintMargin(false); editor.setTheme("ace/theme/"+(formElement.aceTheme||"twilight")); if (formElement.aceMode) { editor.getSession().setMode("ace/mode/"+formElement.aceMode); } editor.getSession().setTabSize(2); // Set the contents of the initial manifest file editor.getSession().setValue(node.value||""); //TODO this is clearly sub-optimal // 'Lazily' bind to the onchange 'ace' event to give // priority to user edits var lazyChanged = _.debounce(function () { $(node.el).find(idSelector).val(editor.getSession().getValue()); $(node.el).find(idSelector).change(); }, 600); editor.getSession().on('change', lazyChanged); editor.on('blur', function() { $(node.el).find(idSelector).change(); $(node.el).find(idSelector).trigger("blur"); }); editor.on('focus', function() { $(node.el).find(idSelector).trigger("focus"); }); }; // Is there a setup hook? if (window.jsonform_ace_setup) { window.jsonform_ace_setup(setup); return; } // Wait until ACE is loaded var itv = window.setInterval(function() { if (window.ace) { window.clearInterval(itv); setup(); } },1000); } }, 'checkbox':{ 'template': '
', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); } }, 'file':{ 'template':'' + '/>', 'fieldtemplate': true, 'inputfield': true }, 'file-hosted-public':{ 'template':'<% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %>\' />', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); }, 'onBeforeRender': function (data, node) { if (!node.ownerTree._transloadit_generic_public_index) { node.ownerTree._transloadit_generic_public_index=1; } else { node.ownerTree._transloadit_generic_public_index++; } data.transloaditname = "_transloadit_jsonform_genericupload_public_"+node.ownerTree._transloadit_generic_public_index; if (!node.ownerTree._transloadit_generic_elts) node.ownerTree._transloadit_generic_elts = {}; node.ownerTree._transloadit_generic_elts[data.transloaditname] = node; }, 'onChange': function(evt,elt) { // The "transloadit" function should be called only once to enable // the service when the form is submitted. Has it already been done? if (elt.ownerTree._transloadit_bound) { return false; } elt.ownerTree._transloadit_bound = true; // Call the "transloadit" function on the form element var formElt = $(elt.ownerTree.domRoot); formElt.transloadit({ autoSubmit: false, wait: true, onSuccess: function (assembly) { // Image has been uploaded. Check the "results" property that // contains the list of files that Transloadit produced. There // should be one image per file input in the form at most. // console.log(assembly.results); var results = _.values(assembly.results); results = _.flatten(results); _.each(results, function (result) { // Save the assembly result in the right hidden input field var id = elt.ownerTree._transloadit_generic_elts[result.field].id; var input = formElt.find('#' + escapeSelector(id)); var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) { return !!isSet(result.meta[key]); }); result.meta = _.pick(result.meta, nonEmptyKeys); input.val(JSON.stringify(result)); }); // Unbind transloadit from the form elt.ownerTree._transloadit_bound = false; formElt.unbind('submit.transloadit'); // Submit the form on next tick _.delay(function () { console.log('submit form'); elt.ownerTree.submit(); }, 10); }, onError: function (assembly) { // TODO: report the error to the user console.log('assembly error', assembly); } }); }, 'onInsert': function (evt, node) { $(node.el).find('a._jsonform-delete').on('click', function (evt) { $(node.el).find('._jsonform-preview').remove(); $(node.el).find('a._jsonform-delete').remove(); $(node.el).find('#' + escapeSelector(node.id)).val(''); evt.preventDefault(); return false; }); }, 'onSubmit':function(evt, elt) { if (elt.ownerTree._transloadit_bound) { return false; } return true; } }, 'file-transloadit': { 'template': '<% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %>\' />', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); }, 'onChange': function (evt, elt) { // The "transloadit" function should be called only once to enable // the service when the form is submitted. Has it already been done? if (elt.ownerTree._transloadit_bound) { return false; } elt.ownerTree._transloadit_bound = true; // Call the "transloadit" function on the form element var formElt = $(elt.ownerTree.domRoot); formElt.transloadit({ autoSubmit: false, wait: true, onSuccess: function (assembly) { // Image has been uploaded. Check the "results" property that // contains the list of files that Transloadit produced. Note // JSONForm only supports 1-to-1 associations, meaning it // expects the "results" property to contain only one image // per file input in the form. // console.log(assembly.results); var results = _.values(assembly.results); results = _.flatten(results); _.each(results, function (result) { // Save the assembly result in the right hidden input field var input = formElt.find('input[name="' + result.field.replace(/^_transloadit_/, '') + '"]'); var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) { return !!isSet(result.meta[key]); }); result.meta = _.pick(result.meta, nonEmptyKeys); input.val(JSON.stringify(result)); }); // Unbind transloadit from the form elt.ownerTree._transloadit_bound = false; formElt.unbind('submit.transloadit'); // Submit the form on next tick _.delay(function () { console.log('submit form'); elt.ownerTree.submit(); }, 10); }, onError: function (assembly) { // TODO: report the error to the user console.log('assembly error', assembly); } }); }, 'onInsert': function (evt, node) { $(node.el).find('a._jsonform-delete').on('click', function (evt) { $(node.el).find('._jsonform-preview').remove(); $(node.el).find('a._jsonform-delete').remove(); $(node.el).find('#' + escapeSelector(node.id)).val(''); evt.preventDefault(); return false; }); }, 'onSubmit': function (evt, elt) { if (elt.ownerTree._transloadit_bound) { return false; } return true; } }, 'select':{ 'template':'', 'fieldtemplate': true, 'inputfield': true }, 'imageselect': { 'template': '
' + '' + '' + '
', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { var elt = node.formElement || {}; var nbRows = null; var maxColumns = elt.imageSelectorColumns || 5; data.buttonTitle = elt.imageSelectorTitle || 'Select...'; data.prefix = elt.imagePrefix || ''; data.suffix = elt.imageSuffix || ''; data.width = elt.imageWidth || 32; data.height = elt.imageHeight || 32; data.buttonClass = elt.imageButtonClass || false; if (node.options.length > maxColumns) { nbRows = Math.ceil(node.options.length / maxColumns); data.columns = Math.ceil(node.options.length / nbRows); } else { data.columns = maxColumns; } }, 'getElement': function (el) { return $(el).parent().get(0); }, 'onInsert': function (evt, node) { $(node.el).on('click', '.dropdown-menu a', function (evt) { evt.preventDefault(); evt.stopPropagation(); var img = (evt.target.nodeName.toLowerCase() === 'img') ? $(evt.target) : $(evt.target).find('img'); var value = img.attr('src'); var elt = node.formElement || {}; var prefix = elt.imagePrefix || ''; var suffix = elt.imageSuffix || ''; var width = elt.imageWidth || 32; var height = elt.imageHeight || 32; if (value) { if (value.indexOf(prefix) === 0) { value = value.substring(prefix.length); } value = value.substring(0, value.length - suffix.length); $(node.el).find('input').attr('value', value); $(node.el).find('a[data-toggle="dropdown"]') .addClass(elt.imageButtonClass) .attr('style', 'max-width:' + width + 'px;max-height:' + height + 'px') .html(''); } else { $(node.el).find('input').attr('value', ''); $(node.el).find('a[data-toggle="dropdown"]') .removeClass(elt.imageButtonClass) .removeAttr('style') .html(elt.imageSelectorTitle || 'Select...'); } }); } }, 'iconselect': { 'template': '
' + '' + '' + '
', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { var elt = node.formElement || {}; var nbRows = null; var maxColumns = elt.imageSelectorColumns || 5; data.buttonTitle = elt.imageSelectorTitle || 'Select...'; data.buttonClass = elt.imageButtonClass || false; if (node.options.length > maxColumns) { nbRows = Math.ceil(node.options.length / maxColumns); data.columns = Math.ceil(node.options.length / nbRows); } else { data.columns = maxColumns; } }, 'getElement': function (el) { return $(el).parent().get(0); }, 'onInsert': function (evt, node) { $(node.el).on('click', '.dropdown-menu a', function (evt) { evt.preventDefault(); evt.stopPropagation(); var i = (evt.target.nodeName.toLowerCase() === 'i') ? $(evt.target) : $(evt.target).find('i'); var value = i.attr('class'); var elt = node.formElement || {}; if (value) { value = value; $(node.el).find('input').attr('value', value); $(node.el).find('a[data-toggle="dropdown"]') .addClass(elt.imageButtonClass) .html(''); } else { $(node.el).find('input').attr('value', ''); $(node.el).find('a[data-toggle="dropdown"]') .removeClass(elt.imageButtonClass) .html(elt.imageSelectorTitle || 'Select...'); } }); } }, 'radios':{ 'template': '
<% _.each(node.options, function(key, val) { %>
<% }); %>
', 'fieldtemplate': true, 'inputfield': true }, 'radiobuttons': { 'template': '
' + '<% _.each(node.options, function(key, val) { %>' + ' ' + '<% }); %>' + '
', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function (evt, node) { var activeClass = 'active'; var elt = node.formElement || {}; if (elt.activeClass) { activeClass += ' ' + elt.activeClass; } $(node.el).find('label').on('click', function () { $(this).parent().find('label').removeClass(activeClass); $(this).addClass(activeClass); }); } }, 'checkboxes':{ 'template': '
<%= choiceshtml %>
', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { // Build up choices from the enumeration list var choices = null; var choiceshtml = null; var template = '
'; if (!node || !node.schemaElement) return; if (node.schemaElement.items) { choices = node.schemaElement.items["enum"] || node.schemaElement.items[0]["enum"]; } else { choices = node.schemaElement["enum"]; } if (!choices) return; choiceshtml = ''; _.each(choices, function (choice, idx) { choiceshtml += _.template(template, fieldTemplateSettings)({ name: node.key + '[' + idx + ']', value: _.include(node.value, choice), title: hasOwnProperty(node.formElement.titleMap, choice) ? node.formElement.titleMap[choice] : choice, node: node }); }); data.choiceshtml = choiceshtml; } }, 'array': { 'template': '
    <%= children %>
' + '' + ' ' + '' + '' + '
', 'fieldtemplate': true, 'array': true, 'childTemplate': function (inner) { if ($('').sortable) { // Insert a "draggable" icon // floating to the left of the main element return '
  • ' + '' + inner + '
  • '; } else { return '
  • ' + inner + '
  • '; } }, 'onInsert': function (evt, node) { var $nodeid = $(node.el).find('#' + escapeSelector(node.id)); var boundaries = node.getArrayBoundaries(); // Switch two nodes in an array var moveNodeTo = function (fromIdx, toIdx) { // Note "switchValuesWith" extracts values from the DOM since field // values are not synchronized with the tree data structure, so calls // to render are needed at each step to force values down to the DOM // before next move. // TODO: synchronize field values and data structure completely and // call render only once to improve efficiency. if (fromIdx === toIdx) return; var incr = (fromIdx < toIdx) ? 1: -1; var i = 0; var parentEl = $('> ul', $nodeid); for (i = fromIdx; i !== toIdx; i += incr) { node.children[i].switchValuesWith(node.children[i + incr]); node.children[i].render(parentEl.get(0)); node.children[i + incr].render(parentEl.get(0)); } // No simple way to prevent DOM reordering with jQuery UI Sortable, // so we're going to need to move sorted DOM elements back to their // origin position in the DOM ourselves (we switched values but not // DOM elements) var fromEl = $(node.children[fromIdx].el); var toEl = $(node.children[toIdx].el); fromEl.detach(); toEl.detach(); if (fromIdx < toIdx) { if (fromIdx === 0) parentEl.prepend(fromEl); else $(node.children[fromIdx-1].el).after(fromEl); $(node.children[toIdx-1].el).after(toEl); } else { if (toIdx === 0) parentEl.prepend(toEl); else $(node.children[toIdx-1].el).after(toEl); $(node.children[fromIdx-1].el).after(fromEl); } }; $('> span > a._jsonform-array-addmore', $nodeid).click(function (evt) { evt.preventDefault(); evt.stopPropagation(); var idx = node.children.length; if (boundaries.maxItems >= 0) { if (node.children.length > boundaries.maxItems - 2) { $nodeid.find('> span > a._jsonform-array-addmore') .addClass('disabled'); } if (node.children.length > boundaries.maxItems - 1) { return false; } } node.insertArrayItem(idx, $('> ul', $nodeid).get(0)); if ((boundaries.minItems <= 0) || ((boundaries.minItems > 0) && (node.children.length > boundaries.minItems - 1))) { $nodeid.find('> span > a._jsonform-array-deletelast') .removeClass('disabled'); } }); //Simulate Users click to setup the form with its minItems var curItems = $('> ul > li', $nodeid).length; if ((boundaries.minItems > 0) && (curItems < boundaries.minItems)) { for (var i = 0; i < (boundaries.minItems - 1) && ($nodeid.find('> ul > li').length < boundaries.minItems); i++) { //console.log('Calling click: ',$nodeid); //$('> span > a._jsonform-array-addmore', $nodeid).click(); node.insertArrayItem(curItems, $nodeid.find('> ul').get(0)); } } if ((boundaries.minItems > 0) && (node.children.length <= boundaries.minItems)) { $nodeid.find('> span > a._jsonform-array-deletelast') .addClass('disabled'); } $('> span > a._jsonform-array-deletelast', $nodeid).click(function (evt) { var idx = node.children.length - 1; evt.preventDefault(); evt.stopPropagation(); if (boundaries.minItems > 0) { if (node.children.length < boundaries.minItems + 2) { $nodeid.find('> span > a._jsonform-array-deletelast') .addClass('disabled'); } if (node.children.length <= boundaries.minItems) { return false; } } else if (node.children.length === 1) { $nodeid.find('> span > a._jsonform-array-deletelast') .addClass('disabled'); } node.deleteArrayItem(idx); if ((boundaries.maxItems >= 0) && (idx <= boundaries.maxItems - 1)) { $nodeid.find('> span > a._jsonform-array-addmore') .removeClass('disabled'); } }); if ($(node.el).sortable) { $('> ul', $nodeid).sortable(); $('> ul', $nodeid).bind('sortstop', function (event, ui) { var idx = $(ui.item).data('idx'); var newIdx = $(ui.item).index(); moveNodeTo(idx, newIdx); }); } } }, 'tabarray': { 'template': '
    ' + '' + '
    ' + '<%= children %>' + '
    ' + '
    ' + ' ' + '
    ', 'fieldtemplate': true, 'array': true, 'childTemplate': function (inner) { return '
    ' + inner + '
    '; }, 'onBeforeRender': function (data, node) { // Generate the initial 'tabs' from the children var tabs = ''; _.each(node.children, function (child, idx) { var title = child.legend || child.title || ('Item ' + (idx+1)); tabs += '
  • ' + escapeHTML(title) + '
  • '; }); data.tabs = tabs; }, 'onInsert': function (evt, node) { var $nodeid = $(node.el).find('#' + escapeSelector(node.id)); var boundaries = node.getArrayBoundaries(); var moveNodeTo = function (fromIdx, toIdx) { // Note "switchValuesWith" extracts values from the DOM since field // values are not synchronized with the tree data structure, so calls // to render are needed at each step to force values down to the DOM // before next move. // TODO: synchronize field values and data structure completely and // call render only once to improve efficiency. if (fromIdx === toIdx) return; var incr = (fromIdx < toIdx) ? 1: -1; var i = 0; var tabEl = $('> .tabbable > .tab-content', $nodeid).get(0); for (i = fromIdx; i !== toIdx; i += incr) { node.children[i].switchValuesWith(node.children[i + incr]); node.children[i].render(tabEl); node.children[i + incr].render(tabEl); } }; // Refreshes the list of tabs var updateTabs = function (selIdx) { var tabs = ''; var activateFirstTab = false; if (selIdx === undefined) { selIdx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx'); if (selIdx) { selIdx = parseInt(selIdx, 10); } else { activateFirstTab = true; selIdx = 0; } } if (selIdx >= node.children.length) { selIdx = node.children.length - 1; } _.each(node.children, function (child, idx) { $('> .tabbable > .tab-content > [data-idx="' + idx + '"] > fieldset > legend', $nodeid).html(child.legend); var title = child.legend || child.title || ('Item ' + (idx+1)); tabs += '
  • ' + '' + escapeHTML(title) + '
  • '; }); $('> .tabbable > .nav-tabs', $nodeid).html(tabs); if (activateFirstTab) { $('> .tabbable > .nav-tabs [data-idx="0"]', $nodeid).addClass('active'); } $('> .tabbable > .nav-tabs [data-toggle="tab"]', $nodeid).eq(selIdx).click(); }; $('> a._jsonform-array-deleteitem', $nodeid).click(function (evt) { var idx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx'); evt.preventDefault(); evt.stopPropagation(); if (boundaries.minItems > 0) { if (node.children.length < boundaries.minItems + 1) { $nodeid.find('> a._jsonform-array-deleteitem') .addClass('disabled'); } if (node.children.length <= boundaries.minItems) return false; } node.deleteArrayItem(idx); updateTabs(); if ((node.children.length < boundaries.minItems + 1) || (node.children.length === 0)) { $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled'); } if ((boundaries.maxItems >= 0) && (node.children.length <= boundaries.maxItems)) { $nodeid.find('> a._jsonform-array-addmore').removeClass('disabled'); } }); $('> a._jsonform-array-addmore', $nodeid).click(function (evt) { var idx = node.children.length; if (boundaries.maxItems>=0) { if (node.children.length>boundaries.maxItems-2) { $('> a._jsonform-array-addmore', $nodeid).addClass("disabled"); } if (node.children.length > boundaries.maxItems - 1) { return false; } } evt.preventDefault(); evt.stopPropagation(); node.insertArrayItem(idx, $nodeid.find('> .tabbable > .tab-content').get(0)); updateTabs(idx); if ((boundaries.minItems <= 0) || ((boundaries.minItems > 0) && (idx > boundaries.minItems - 1))) { $nodeid.find('> a._jsonform-array-deleteitem').removeClass('disabled'); } }); $(node.el).on('legendUpdated', function (evt) { updateTabs(); evt.preventDefault(); evt.stopPropagation(); }); if ($(node.el).sortable) { $('> .tabbable > .nav-tabs', $nodeid).sortable({ containment: node.el, tolerance: 'pointer' }); $('> .tabbable > .nav-tabs', $nodeid).bind('sortstop', function (event, ui) { var idx = $(ui.item).data('idx'); var newIdx = $(ui.item).index(); moveNodeTo(idx, newIdx); updateTabs(newIdx); }); } // Simulate User's click to setup the form with its minItems if ((boundaries.minItems >= 0) && (node.children.length <= boundaries.minItems)) { for (var i = 0; i < (boundaries.minItems - 1); i++) { $nodeid.find('> a._jsonform-array-addmore').click(); } $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled'); updateTabs(); } if ((boundaries.maxItems >= 0) && (node.children.length >= boundaries.maxItems)) { $nodeid.find('> a._jsonform-array-addmore').addClass('disabled'); } if ((boundaries.minItems >= 0) && (node.children.length <= boundaries.minItems)) { $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled'); } } }, 'help': { 'template':'<%= elt.helpvalue %>', 'fieldtemplate': true }, 'msg': { 'template': '<%= elt.msg %>' }, 'fieldset': { 'template': '
    " ' + '<% if (id) { %> id="<%= id %>"<% } %>' + '>' + '<% if (node.title || node.legend) { %><%= node.title || node.legend %><% } %>' + '<% if (elt.expandable) { %>
    <% } %>' + '<%= children %>' + '<% if (elt.expandable) { %>
    <% } %>' + '
    ', onInsert: function (evt, node) { $('.expandable > div, .expandable > fieldset', node.el).hide(); // See #233 $(".expandable", node.el).removeClass("expanded"); } }, 'advancedfieldset': { 'template': ' id="<%= id %>"<% } %>' + ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' + '<%= (node.title || node.legend) ? (node.title || node.legend) : "Advanced options" %>' + '
    ' + '<%= children %>' + '
    ' + '', onInsert: function (evt, node) { $('.expandable > div, .expandable > fieldset', node.el).hide(); // See #233 $(".expandable", node.el).removeClass("expanded"); } }, 'authfieldset': { 'template': ' id="<%= id %>"<% } %>' + ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' + '<%= (node.title || node.legend) ? (node.title || node.legend) : "Authentication settings" %>' + '
    ' + '<%= children %>' + '
    ' + '', onInsert: function (evt, node) { $('.expandable > div, .expandable > fieldset', node.el).hide(); // See #233 $(".expandable", node.el).removeClass("expanded"); } }, 'submit':{ 'template':' id="<%= id %>" <% } %> class="btn btn-primary <%= elt.htmlClass?elt.htmlClass:"" %>" value="<%= value || node.title %>"<%= (node.disabled? " disabled" : "")%>/>' }, 'button':{ 'template':' ' }, 'actions':{ 'template':'
    "><%= children %>
    ' }, 'hidden':{ 'template':'', 'inputfield': true }, 'selectfieldset': { 'template': '
    ">' + '<% if (node.legend) { %><%= node.legend %><% } %>' + '<% if (node.formElement.key) { %><% } else { %>' + '<% } %>' + '
    ' + '
    ">' + '<% if (!elt.notitle) { %><% } %>' + '
    <%= tabs %>
    ' + '
    ' + '
    ' + '<%= children %>' + '
    ' + '
    ' + '
    ', 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); }, 'childTemplate': function (inner) { return '
    ' + inner + '
    '; }, 'onBeforeRender': function (data, node) { // Before rendering, this function ensures that: // 1. direct children have IDs (used to show/hide the tabs contents) // 2. the tab to active is flagged accordingly. The active tab is // the first one, except if form values are available, in which case // it's the first tab for which there is some value available (or back // to the first one if there are none) // 3. the HTML of the select field used to select tabs is exposed in the // HTML template data as "tabs" var children = null; var choices = []; if (node.schemaElement) { choices = node.schemaElement['enum'] || []; } if (node.options) { children = _.map(node.options, function (option, idx) { var child = node.children[idx]; child.childPos = idx; // When nested the childPos is always 0. if (option instanceof Object) { option = _.extend({ node: child }, option); option.title = option.title || child.legend || child.title || ('Option ' + (child.childPos+1)); option.value = isSet(option.value) ? option.value : isSet(choices[idx]) ? choices[idx] : idx; return option; } else { return { title: option, value: isSet(choices[child.childPos]) ? choices[child.childPos] : child.childPos, node: child }; } }); } else { children = _.map(node.children, function (child, idx) { child.childPos = idx; // When nested the childPos is always 0. return { title: child.legend || child.title || ('Option ' + (child.childPos+1)), value: choices[child.childPos] || child.childPos, node: child }; }); } // Reset each children to inactive so that they are not shown on insert // The active one will then be shown later one. This is useful when sorting // arrays with selectfieldset, otherwise both fields could be active at the // same time. _.each(children, function (child, idx) { child.node.active = false }); var activeChild = null; if (data.value) { activeChild = _.find(children, function (child) { return (child.value === node.value); }); } if (!activeChild) { activeChild = _.find(children, function (child) { return child.node.hasNonDefaultValue(); }); } if (!activeChild) { activeChild = children[0]; } activeChild.node.active = true; data.value = activeChild.value; var elt = node.formElement; var tabs = ''; data.tabs = tabs; return data; }, 'onInsert': function (evt, node) { $(node.el).find('select.nav').first().on('change', function (evt) { var $option = $(this).find('option:selected'); $(node.el).find('input[type="hidden"]').first().val($option.attr('value')); }); } }, 'optionfieldset': { 'template': ' id="<%= node.id %>"<% } %>' + '>' + '<%= children %>' + '' }, 'section': { 'template': ' class="<%= elt.htmlClass %>"<% } %>' + '<% if (node.id) { %> id="<%= node.id %>"<% } %>' + '><%= children %>' }, /** * A "questions" field renders a series of question fields and binds the * result to the value of a schema key. */ 'questions': { 'template': '
    ' + '' + '<%= children %>' + '
    ', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); }, 'onInsert': function (evt, node) { if (!node.children || (node.children.length === 0)) return; _.each(node.children, function (child) { $(child.el).hide(); }); $(node.children[0].el).show(); } }, /** * A "question" field lets user choose a response among possible choices. * The field is not associated with any schema key. A question should be * part of a "questions" field that binds a series of questions to a * schema key. */ 'question': { 'template': '
    <% _.each(node.options, function(key, val) { %> <% }); %>
    ', 'fieldtemplate': true, 'onInsert': function (evt, node) { var activeClass = 'active'; var elt = node.formElement || {}; if (elt.activeClass) { activeClass += ' ' + elt.activeClass; } // Bind to change events on radio buttons $(node.el).find('input[type="radio"]').on('change', function (evt) { var questionNode = null; var option = node.options[$(this).val()]; if (!node.parentNode || !node.parentNode.el) return; $(this).parent().parent().find('label').removeClass(activeClass); $(this).parent().addClass(activeClass); $(node.el).nextAll().hide(); $(node.el).nextAll().find('input[type="radio"]').prop('checked', false); // Execute possible actions (set key value, form submission, open link, // move on to next question) if (option.value) { // Set the key of the 'Questions' parent $(node.parentNode.el).find('input[type="hidden"]').val(option.value); } if (option.next) { questionNode = _.find(node.parentNode.children, function (child) { return (child.formElement && (child.formElement.qid === option.next)); }); $(questionNode.el).show(); $(questionNode.el).nextAll().hide(); $(questionNode.el).nextAll().find('input[type="radio"]').prop('checked', false); } if (option.href) { if (option.target) { window.open(option.href, option.target); } else { window.location = option.href; } } if (option.submit) { setTimeout(function () { node.ownerTree.submit(); }, 0); } }); } } }; //Allow to access subproperties by splitting "." /** * Retrieves the key identified by a path selector in the structured object. * * Levels in the path are separated by a dot. Array items are marked * with [x]. For instance: * foo.bar[3].baz * * @function * @param {Object} obj Structured object to parse * @param {String} key Path to the key to retrieve * @param {boolean} ignoreArrays True to use first element in an array when * stucked on a property. This parameter is basically only useful when * parsing a JSON schema for which the "items" property may either be an * object or an array with one object (only one because JSON form does not * support mix of items for arrays). * @return {Object} The key's value. */ jsonform.util.getObjKey = function (obj, key, ignoreArrays) { var innerobj = obj; var keyparts = key.split("."); var subkey = null; var arrayMatch = null; var prop = null; for (var i = 0; i < keyparts.length; i++) { if ((innerobj === null) || (typeof innerobj !== "object")) return null; subkey = keyparts[i]; prop = subkey.replace(reArray, ''); reArray.lastIndex = 0; arrayMatch = reArray.exec(subkey); if (arrayMatch) { while (true) { if (prop && !_.isArray(innerobj[prop])) return null; innerobj = prop ? innerobj[prop][parseInt(arrayMatch[1])] : innerobj[parseInt(arrayMatch[1])]; arrayMatch = reArray.exec(subkey); if (!arrayMatch) break; // In the case of multidimensional arrays, // we should not take innerobj[prop][0] anymore, // but innerobj[0] directly prop = null; } } else if (ignoreArrays && !innerobj[prop] && _.isArray(innerobj) && innerobj[0]) { innerobj = innerobj[0][prop]; } else { innerobj = innerobj[prop]; } } if (ignoreArrays && _.isArray(innerobj) && innerobj[0]) { return innerobj[0]; } else { return innerobj; } }; /** * Sets the key identified by a path selector to the given value. * * Levels in the path are separated by a dot. Array items are marked * with [x]. For instance: * foo.bar[3].baz * * The hierarchy is automatically created if it does not exist yet. * * @function * @param {Object} obj The object to build * @param {String} key The path to the key to set where each level * is separated by a dot, and array items are flagged with [x]. * @param {Object} value The value to set, may be of any type. */ jsonform.util.setObjKey = function(obj,key,value) { var innerobj = obj; var keyparts = key.split("."); var subkey = null; var arrayMatch = null; var prop = null; for (var i = 0; i < keyparts.length-1; i++) { subkey = keyparts[i]; prop = subkey.replace(reArray, ''); reArray.lastIndex = 0; arrayMatch = reArray.exec(subkey); if (arrayMatch) { // Subkey is part of an array while (true) { if (!_.isArray(innerobj[prop])) { innerobj[prop] = []; } innerobj = innerobj[prop]; prop = parseInt(arrayMatch[1], 10); arrayMatch = reArray.exec(subkey); if (!arrayMatch) break; } if ((typeof innerobj[prop] !== 'object') || (innerobj[prop] === null)) { innerobj[prop] = {}; } innerobj = innerobj[prop]; } else { // "Normal" subkey if ((typeof innerobj[prop] !== 'object') || (innerobj[prop] === null)) { innerobj[prop] = {}; } innerobj = innerobj[prop]; } } // Set the final value subkey = keyparts[keyparts.length - 1]; prop = subkey.replace(reArray, ''); reArray.lastIndex = 0; arrayMatch = reArray.exec(subkey); if (arrayMatch) { while (true) { if (!_.isArray(innerobj[prop])) { innerobj[prop] = []; } innerobj = innerobj[prop]; prop = parseInt(arrayMatch[1], 10); arrayMatch = reArray.exec(subkey); if (!arrayMatch) break; } innerobj[prop] = value; } else { innerobj[prop] = value; } }; /** * Retrieves the key definition from the given schema. * * The key is identified by the path that leads to the key in the * structured object that the schema would generate. Each level is * separated by a '.'. Array levels are marked with []. For instance: * foo.bar[].baz * ... to retrieve the definition of the key at the following location * in the JSON schema (using a dotted path notation): * foo.properties.bar.items.properties.baz * * @function * @param {Object} schema The JSON schema to retrieve the key from * @param {String} key The path to the key, each level being separated * by a dot and array items being flagged with []. * @return {Object} The key definition in the schema, null if not found. */ var getSchemaKey = function(schema,key) { var schemaKey = key .replace(/\./g, '.properties.') .replace(/\[[0-9]*\]/g, '.items'); var schemaDef = jsonform.util.getObjKey(schema, schemaKey, true); if (schemaDef && schemaDef.$ref) { throw new Error('JSONForm does not yet support schemas that use the ' + '$ref keyword. See: https://github.com/joshfire/jsonform/issues/54'); } return schemaDef; }; /** * Truncates the key path to the requested depth. * * For instance, if the key path is: * foo.bar[].baz.toto[].truc[].bidule * and the requested depth is 1, the returned key will be: * foo.bar[].baz.toto * * Note the function includes the path up to the next depth level. * * @function * @param {String} key The path to the key in the schema, each level being * separated by a dot and array items being flagged with []. * @param {Number} depth The array depth * @return {String} The path to the key truncated to the given depth. */ var truncateToArrayDepth = function (key, arrayDepth) { var depth = 0; var pos = 0; if (!key) return null; if (arrayDepth > 0) { while (depth < arrayDepth) { pos = key.indexOf('[]', pos); if (pos === -1) { // Key path is not "deep" enough, simply return the full key return key; } pos = pos + 2; depth += 1; } } // Move one step further to the right without including the final [] pos = key.indexOf('[]', pos); if (pos === -1) return key; else return key.substring(0, pos); }; /** * Applies the array path to the key path. * * For instance, if the key path is: * foo.bar[].baz.toto[].truc[].bidule * and the arrayPath [4, 2], the returned key will be: * foo.bar[4].baz.toto[2].truc[].bidule * * @function * @param {String} key The path to the key in the schema, each level being * separated by a dot and array items being flagged with []. * @param {Array(Number)} arrayPath The array path to apply, e.g. [4, 2] * @return {String} The path to the key that matches the array path. */ var applyArrayPath = function (key, arrayPath) { var depth = 0; if (!key) return null; if (!arrayPath || (arrayPath.length === 0)) return key; var newKey = key.replace(reArray, function (str, p1) { // Note this function gets called as many times as there are [x] in the ID, // from left to right in the string. The goal is to replace the [x] with // the appropriate index in the new array path, if defined. var newIndex = str; if (isSet(arrayPath[depth])) { newIndex = '[' + arrayPath[depth] + ']'; } depth += 1; return newIndex; }); return newKey; }; /** * Returns the initial value that a field identified by its key * should take. * * The "initial" value is defined as: * 1. the previously submitted value if already submitted * 2. the default value defined in the layout of the form * 3. the default value defined in the schema * * The "value" returned is intended for rendering purpose, * meaning that, for fields that define a titleMap property, * the function returns the label, and not the intrinsic value. * * The function handles values that contains template strings, * e.g. {{values.foo[].bar}} or {{idx}}. * * When the form is a string, the function truncates the resulting string * to meet a potential "maxLength" constraint defined in the schema, using * "..." to mark the truncation. Note it does not validate the resulting * string against other constraints (e.g. minLength, pattern) as it would * be hard to come up with an automated course of action to "fix" the value. * * @function * @param {Object} formObject The JSON Form object * @param {String} key The generic key path (e.g. foo[].bar.baz[]) * @param {Array(Number)} arrayPath The array path that identifies * the unique value in the submitted form (e.g. [1, 3]) * @param {Object} tpldata Template data object * @param {Boolean} usePreviousValues true to use previously submitted values * if defined. */ var getInitialValue = function (formObject, key, arrayPath, tpldata, usePreviousValues) { var value = null; // Complete template data for template function tpldata = tpldata || {}; tpldata.idx = tpldata.idx || (arrayPath ? arrayPath[arrayPath.length-1] : 1); tpldata.value = isSet(tpldata.value) ? tpldata.value : ''; tpldata.getValue = tpldata.getValue || function (key) { return getInitialValue(formObject, key, arrayPath, tpldata, usePreviousValues); }; // Helper function that returns the form element that explicitly // references the given key in the schema. var getFormElement = function (elements, key) { var formElement = null; if (!elements || !elements.length) return null; _.each(elements, function (elt) { if (formElement) return; if (elt === key) { formElement = { key: elt }; return; } if (_.isString(elt)) return; if (elt.key === key) { formElement = elt; } else if (elt.items) { formElement = getFormElement(elt.items, key); } }); return formElement; }; var formElement = getFormElement(formObject.form || [], key); var schemaElement = getSchemaKey(formObject.schema.properties, key); if (usePreviousValues && formObject.value) { // If values were previously submitted, use them directly if defined value = jsonform.util.getObjKey(formObject.value, applyArrayPath(key, arrayPath)); } if (!isSet(value)) { if (formElement && (typeof formElement['value'] !== 'undefined')) { // Extract the definition of the form field associated with // the key as it may override the schema's default value // (note a "null" value overrides a schema default value as well) value = formElement['value']; } else if (schemaElement) { // Simply extract the default value from the schema if (isSet(schemaElement['default'])) { value = schemaElement['default']; } } if (value && value.indexOf('{{values.') !== -1) { // This label wants to use the value of another input field. // Convert that construct into {{getValue(key)}} for // Underscore to call the appropriate function of formData // when template gets called (note calling a function is not // exactly Mustache-friendly but is supported by Underscore). value = value.replace( /\{\{values\.([^\}]+)\}\}/g, '{{getValue("$1")}}'); } if (value) { value = _.template(value, valueTemplateSettings)(tpldata); } } // TODO: handle on the formElement.options, because user can setup it too. // Apply titleMap if needed if (isSet(value) && formElement && hasOwnProperty(formElement.titleMap, value)) { value = _.template(formElement.titleMap[value], valueTemplateSettings)(tpldata); } // Check maximum length of a string if (value && _.isString(value) && schemaElement && schemaElement.maxLength) { if (value.length > schemaElement.maxLength) { // Truncate value to maximum length, adding continuation dots value = value.substr(0, schemaElement.maxLength - 1) + '…'; } } if (!isSet(value)) { return null; } else { return value; } }; /** * Represents a node in the form. * * Nodes that have an ID are linked to the corresponding DOM element * when rendered * * Note the form element and the schema elements that gave birth to the * node may be shared among multiple nodes (in the case of arrays). * * @class */ var formNode = function () { /** * The node's ID (may not be set) */ this.id = null; /** * The node's key path (may not be set) */ this.key = null; /** * DOM element associated witht the form element. * * The DOM element is set when the form element is rendered. */ this.el = null; /** * Link to the form element that describes the node's layout * (note the form element is shared among nodes in arrays) */ this.formElement = null; /** * Link to the schema element that describes the node's value constraints * (note the schema element is shared among nodes in arrays) */ this.schemaElement = null; /** * Pointer to the "view" associated with the node, typically the right * object in jsonform.elementTypes */ this.view = null; /** * Node's subtree (if one is defined) */ this.children = []; /** * A pointer to the form tree the node is attached to */ this.ownerTree = null; /** * A pointer to the parent node of the node in the tree */ this.parentNode = null; /** * Child template for array-like nodes. * * The child template gets cloned to create new array items. */ this.childTemplate = null; /** * Direct children of array-like containers may use the value of a * specific input field in their subtree as legend. The link to the * legend child is kept here and initialized in computeInitialValues * when a child sets "valueInLegend" */ this.legendChild = null; /** * The path of indexes that lead to the current node when the * form element is not at the root array level. * * Note a form element may well be nested element and still be * at the root array level. That's typically the case for "fieldset" * elements. An array level only gets created when a form element * is of type "array" (or a derivated type such as "tabarray"). * * The array path of a form element linked to the foo[2].bar.baz[3].toto * element in the submitted values is [2, 3] for instance. * * The array path is typically used to compute the right ID for input * fields. It is also used to update positions when an array item is * created, moved around or suppressed. * * @type {Array(Number)} */ this.arrayPath = []; /** * Position of the node in the list of children of its parents */ this.childPos = 0; }; /** * Clones a node * * @function * @param {formNode} New parent node to attach the node to * @return {formNode} Cloned node */ formNode.prototype.clone = function (parentNode) { var node = new formNode(); node.arrayPath = _.clone(this.arrayPath); node.ownerTree = this.ownerTree; node.parentNode = parentNode || this.parentNode; node.formElement = this.formElement; node.schemaElement = this.schemaElement; node.view = this.view; node.children = _.map(this.children, function (child) { return child.clone(node); }); if (this.childTemplate) { node.childTemplate = this.childTemplate.clone(node); } return node; }; /** * Returns true if the subtree that starts at the current node * has some non empty value attached to it */ formNode.prototype.hasNonDefaultValue = function () { // hidden elements don't count because they could make the wrong selectfieldset element active if (this.formElement && this.formElement.type=="hidden") { return false; } if (this.value && !this.defaultValue) { return true; } var child = _.find(this.children, function (child) { return child.hasNonDefaultValue(); }); return !!child; }; /** * Attaches a child node to the current node. * * The child node is appended to the end of the list. * * @function * @param {formNode} node The child node to append * @return {formNode} The inserted node (same as the one given as parameter) */ formNode.prototype.appendChild = function (node) { node.parentNode = this; node.childPos = this.children.length; this.children.push(node); return node; }; /** * Removes the last child of the node. * * @function */ formNode.prototype.removeChild = function () { var child = this.children[this.children.length-1]; if (!child) return; // Remove the child from the DOM $(child.el).remove(); // Remove the child from the array return this.children.pop(); }; /** * Moves the user entered values set in the current node's subtree to the * given node's subtree. * * The target node must follow the same structure as the current node * (typically, they should have been generated from the same node template) * * The current node MUST be rendered in the DOM. * * TODO: when current node is not in the DOM, extract values from formNode.value * properties, so that the function be available even when current node is not * in the DOM. * * Moving values around allows to insert/remove array items at arbitrary * positions. * * @function * @param {formNode} node Target node. */ formNode.prototype.moveValuesTo = function (node) { var values = this.getFormValues(node.arrayPath); node.resetValues(); node.computeInitialValues(values, true); }; /** * Switches nodes user entered values. * * The target node must follow the same structure as the current node * (typically, they should have been generated from the same node template) * * Both nodes MUST be rendered in the DOM. * * TODO: update getFormValues to work even if node is not rendered, using * formNode's "value" property. * * @function * @param {formNode} node Target node */ formNode.prototype.switchValuesWith = function (node) { var values = this.getFormValues(node.arrayPath); var nodeValues = node.getFormValues(this.arrayPath); node.resetValues(); node.computeInitialValues(values, true); this.resetValues(); this.computeInitialValues(nodeValues, true); }; /** * Resets all DOM values in the node's subtree. * * This operation also drops all array item nodes. * Note values are not reset to their default values, they are rather removed! * * @function */ formNode.prototype.resetValues = function () { var params = null; var idx = 0; // Reset value this.value = null; // Propagate the array path from the parent node // (adding the position of the child for nodes that are direct // children of array-like nodes) if (this.parentNode) { this.arrayPath = _.clone(this.parentNode.arrayPath); if (this.parentNode.view && this.parentNode.view.array) { this.arrayPath.push(this.childPos); } } else { this.arrayPath = []; } if (this.view && this.view.inputfield) { // Simple input field, extract the value from the origin, // set the target value and reset the origin value params = $(':input', this.el).serializeArray(); _.each(params, function (param) { // TODO: check this, there may exist corner cases with this approach // (with multiple checkboxes for instance) $('[name="' + escapeSelector(param.name) + '"]', $(this.el)).val(''); }, this); } else if (this.view && this.view.array) { // The current node is an array, drop all children while (this.children.length > 0) { this.removeChild(); } } // Recurse down the tree _.each(this.children, function (child) { child.resetValues(); }); }; /** * Sets the child template node for the current node. * * The child template node is used to create additional children * in an array-like form element. The template is never rendered. * * @function * @param {formNode} node The child template node to set */ formNode.prototype.setChildTemplate = function (node) { this.childTemplate = node; node.parentNode = this; }; /** * Recursively sets values to all nodes of the current subtree * based on previously submitted values, or based on default * values when the submitted values are not enough * * The function should be called once in the lifetime of a node * in the tree. It expects its parent's arrayPath to be up to date. * * Three cases may arise: * 1. if the form element is a simple input field, the value is * extracted from previously submitted values of from default values * defined in the schema. * 2. if the form element is an array-like node, the child template * is used to create as many children as possible (and at least one). * 3. the function simply recurses down the node's subtree otherwise * (this happens when the form element is a fieldset-like element). * * @function * @param {Object} values Previously submitted values for the form * @param {Boolean} ignoreDefaultValues Ignore default values defined in the * schema when set. */ formNode.prototype.computeInitialValues = function (values, ignoreDefaultValues) { var self = this; var node = null; var nbChildren = 1; var i = 0; var formData = this.ownerTree.formDesc.tpldata || {}; // Propagate the array path from the parent node // (adding the position of the child for nodes that are direct // children of array-like nodes) if (this.parentNode) { this.arrayPath = _.clone(this.parentNode.arrayPath); if (this.parentNode.view && this.parentNode.view.array) { this.arrayPath.push(this.childPos); } } else { this.arrayPath = []; } // Prepare special data param "idx" for templated values // (is is the index of the child in its wrapping array, starting // at 1 since that's more human-friendly than a zero-based index) formData.idx = (this.arrayPath.length > 0) ? this.arrayPath[this.arrayPath.length-1] + 1 : this.childPos + 1; // Prepare special data param "value" for templated values formData.value = ''; // Prepare special function to compute the value of another field formData.getValue = function (key) { if (!values) { return ''; } var returnValue = values; var listKey = key.split('[].'); var i; for (i = 0; i < listKey.length - 1; i++) { returnValue = returnValue[listKey[i]][self.arrayPath[i]]; } return returnValue[listKey[i]]; }; if (this.formElement) { // Compute the ID of the field (if needed) if (this.formElement.id) { this.id = applyArrayPath(this.formElement.id, this.arrayPath); } else if (this.view && this.view.array) { this.id = escapeSelector(this.ownerTree.formDesc.prefix) + '-elt-counter-' + _.uniqueId(); } else if (this.parentNode && this.parentNode.view && this.parentNode.view.array) { // Array items need an array to associate the right DOM element // to the form node when the parent is rendered. this.id = escapeSelector(this.ownerTree.formDesc.prefix) + '-elt-counter-' + _.uniqueId(); } else if ((this.formElement.type === 'button') || (this.formElement.type === 'selectfieldset') || (this.formElement.type === 'question') || (this.formElement.type === 'buttonquestion')) { // Buttons do need an id for "onClick" purpose this.id = escapeSelector(this.ownerTree.formDesc.prefix) + '-elt-counter-' + _.uniqueId(); } // Compute the actual key (the form element's key is index-free, // i.e. it looks like foo[].bar.baz[].truc, so we need to apply // the array path of the node to get foo[4].bar.baz[2].truc) if (this.formElement.key) { this.key = applyArrayPath(this.formElement.key, this.arrayPath); this.keydash = slugify(this.key.replace(/\./g, '---')); } // Same idea for the field's name this.name = applyArrayPath(this.formElement.name, this.arrayPath); // Consider that label values are template values and apply the // form's data appropriately (note we also apply the array path // although that probably doesn't make much sense for labels...) _.each([ 'title', 'legend', 'description', 'append', 'prepend', 'inlinetitle', 'helpvalue', 'value', 'disabled', 'placeholder', 'readOnly' ], function (prop) { if (_.isString(this.formElement[prop])) { if (this.formElement[prop].indexOf('{{values.') !== -1) { // This label wants to use the value of another input field. // Convert that construct into {{jsonform.getValue(key)}} for // Underscore to call the appropriate function of formData // when template gets called (note calling a function is not // exactly Mustache-friendly but is supported by Underscore). this[prop] = this.formElement[prop].replace( /\{\{values\.([^\}]+)\}\}/g, '{{getValue("$1")}}'); } else { // Note applying the array path probably doesn't make any sense, // but some geek might want to have a label "foo[].bar[].baz", // with the [] replaced by the appropriate array path. this[prop] = applyArrayPath(this.formElement[prop], this.arrayPath); } if (this[prop]) { this[prop] = _.template(this[prop], valueTemplateSettings)(formData); } } else { this[prop] = this.formElement[prop]; } }, this); // Apply templating to options created with "titleMap" as well if (this.formElement.options) { this.options = _.map(this.formElement.options, function (option) { var title = null; if (_.isObject(option) && option.title) { // See a few lines above for more details about templating // preparation here. if (option.title.indexOf('{{values.') !== -1) { title = option.title.replace( /\{\{values\.([^\}]+)\}\}/g, '{{getValue("$1")}}'); } else { title = applyArrayPath(option.title, self.arrayPath); } return _.extend({}, option, { value: (isSet(option.value) ? option.value : ''), title: _.template(title, valueTemplateSettings)(formData) }); } else { return option; } }); } } if (this.view && this.view.inputfield && this.schemaElement) { // Case 1: simple input field if (values) { // Form has already been submitted, use former value if defined. // Note we won't set the field to its default value otherwise // (since the user has already rejected it) if (isSet(jsonform.util.getObjKey(values, this.key))) { this.value = jsonform.util.getObjKey(values, this.key); } } else if (!ignoreDefaultValues) { // No previously submitted form result, use default value // defined in the schema if it's available and not already // defined in the form element if (!isSet(this.value) && isSet(this.schemaElement['default'])) { this.value = this.schemaElement['default']; if (_.isString(this.value)) { if (this.value.indexOf('{{values.') !== -1) { // This label wants to use the value of another input field. // Convert that construct into {{jsonform.getValue(key)}} for // Underscore to call the appropriate function of formData // when template gets called (note calling a function is not // exactly Mustache-friendly but is supported by Underscore). this.value = this.value.replace( /\{\{values\.([^\}]+)\}\}/g, '{{getValue("$1")}}'); } else { // Note applying the array path probably doesn't make any sense, // but some geek might want to have a label "foo[].bar[].baz", // with the [] replaced by the appropriate array path. this.value = applyArrayPath(this.value, this.arrayPath); } if (this.value) { this.value = _.template(this.value, valueTemplateSettings)(formData); } } this.defaultValue = true; } } } else if (this.view && this.view.array) { // Case 2: array-like node nbChildren = 0; if (values) { nbChildren = this.getPreviousNumberOfItems(values, this.arrayPath); } // TODO: use default values at the array level when form has not been // submitted before. Note it's not that easy because each value may // be a complex structure that needs to be pushed down the subtree. // The easiest way is probably to generate a "values" object and // compute initial values from that object /* else if (this.schemaElement['default']) { nbChildren = this.schemaElement['default'].length; } */ else if (nbChildren === 0) { // If form has already been submitted with no children, the array // needs to be rendered without children. If there are no previously // submitted values, the array gets rendered with one empty item as // it's more natural from a user experience perspective. That item can // be removed with a click on the "-" button. nbChildren = 1; } for (i = 0; i < nbChildren; i++) { this.appendChild(this.childTemplate.clone()); } } // Case 3 and in any case: recurse through the list of children _.each(this.children, function (child) { child.computeInitialValues(values, ignoreDefaultValues); }); // If the node's value is to be used as legend for its "container" // (typically the array the node belongs to), ensure that the container // has a direct link to the node for the corresponding tab. if (this.formElement && this.formElement.valueInLegend) { node = this; while (node) { if (node.parentNode && node.parentNode.view && node.parentNode.view.array) { node.legendChild = this; if (node.formElement && node.formElement.legend) { node.legend = applyArrayPath(node.formElement.legend, node.arrayPath); formData.idx = (node.arrayPath.length > 0) ? node.arrayPath[node.arrayPath.length-1] + 1 : node.childPos + 1; formData.value = isSet(this.value) ? this.value : ''; node.legend = _.template(node.legend, valueTemplateSettings)(formData); break; } } node = node.parentNode; } } }; /** * Returns the number of items that the array node should have based on * previously submitted values. * * The whole difficulty is that values may be hidden deep in the subtree * of the node and may actually target different arrays in the JSON schema. * * @function * @param {Object} values Previously submitted values * @param {Array(Number)} arrayPath the array path we're interested in * @return {Number} The number of items in the array */ formNode.prototype.getPreviousNumberOfItems = function (values, arrayPath) { var key = null; var arrayValue = null; var childNumbers = null; var idx = 0; if (!values) { // No previously submitted values, no need to go any further return 0; } if (this.view.inputfield && this.schemaElement) { // Case 1: node is a simple input field that links to a key in the schema. // The schema key looks typically like: // foo.bar[].baz.toto[].truc[].bidule // The goal is to apply the array path and truncate the key to the last // array we're interested in, e.g. with an arrayPath [4, 2]: // foo.bar[4].baz.toto[2] key = truncateToArrayDepth(this.formElement.key, arrayPath.length); key = applyArrayPath(key, arrayPath); arrayValue = jsonform.util.getObjKey(values, key); if (!arrayValue) { // No key? That means this field had been left empty // in previous submit return 0; } childNumbers = _.map(this.children, function (child) { return child.getPreviousNumberOfItems(values, arrayPath); }); return _.max([_.max(childNumbers) || 0, arrayValue.length]); } else if (this.view.array) { // Case 2: node is an array-like node, look for input fields // in its child template return this.childTemplate.getPreviousNumberOfItems(values, arrayPath); } else { // Case 3: node is a leaf or a container, // recurse through the list of children and return the maximum // number of items found in each subtree childNumbers = _.map(this.children, function (child) { return child.getPreviousNumberOfItems(values, arrayPath); }); return _.max(childNumbers) || 0; } }; /** * Returns the structured object that corresponds to the form values entered * by the user for the node's subtree. * * The returned object follows the structure of the JSON schema that gave * birth to the form. * * Obviously, the node must have been rendered before that function may * be called. * * @function * @param {Array(Number)} updateArrayPath Array path to use to pretend that * the entered values were actually entered for another item in an array * (this is used to move values around when an item is inserted/removed/moved * in an array) * @return {Object} The object that follows the data schema and matches the * values entered by the user. */ formNode.prototype.getFormValues = function (updateArrayPath) { // The values object that will be returned var values = {}; if (!this.el) { throw new Error('formNode.getFormValues can only be called on nodes that are associated with a DOM element in the tree'); } // Form fields values var formArray = $(':input', this.el).serializeArray(); // Set values to false for unset checkboxes and radio buttons // because serializeArray() ignores them formArray = formArray.concat( $(':input[type=checkbox]:not(:disabled):not(:checked)', this.el).map( function() { return {"name": this.name, "value": this.checked} }).get() ); if (updateArrayPath) { _.each(formArray, function (param) { param.name = applyArrayPath(param.name, updateArrayPath); }); } // The underlying data schema var formSchema = this.ownerTree.formDesc.schema; for (var i = 0; i < formArray.length; i++) { // Retrieve the key definition from the data schema var name = formArray[i].name; var eltSchema = getSchemaKey(formSchema.properties, name); var arrayMatch = null; var cval = null; // Skip the input field if it's not part of the schema if (!eltSchema) continue; // Handle multiple checkboxes separately as the idea is to generate // an array that contains the list of enumeration items that the user // selected. if (eltSchema._jsonform_checkboxes_as_array) { arrayMatch = name.match(/\[([0-9]*)\]$/); if (arrayMatch) { name = name.replace(/\[([0-9]*)\]$/, ''); cval = jsonform.util.getObjKey(values, name) || []; if (formArray[i].value === '1') { // Value selected, push the corresponding enumeration item // to the data result cval.push(eltSchema['enum'][parseInt(arrayMatch[1],10)]); } jsonform.util.setObjKey(values, name, cval); continue; } } // Type casting if (eltSchema.type === 'boolean') { if (formArray[i].value === '0') { formArray[i].value = false; } else { formArray[i].value = !!formArray[i].value; } } if ((eltSchema.type === 'number') || (eltSchema.type === 'integer')) { if (_.isString(formArray[i].value)) { if (!formArray[i].value.length) { formArray[i].value = null; } else if (!isNaN(Number(formArray[i].value))) { formArray[i].value = Number(formArray[i].value); } } } if ((eltSchema.type === 'string') && (formArray[i].value === '') && !eltSchema._jsonform_allowEmpty) { formArray[i].value=null; } if ((eltSchema.type === 'object') && _.isString(formArray[i].value) && (formArray[i].value.substring(0,1) === '{')) { try { formArray[i].value = JSON.parse(formArray[i].value); } catch (e) { formArray[i].value = {}; } } //TODO is this due to a serialization bug? if ((eltSchema.type === 'object') && (formArray[i].value === 'null' || formArray[i].value === '')) { formArray[i].value = null; } if (formArray[i].name && (formArray[i].value !== null)) { jsonform.util.setObjKey(values, formArray[i].name, formArray[i].value); } } // console.log("Form value",values); return values; }; /** * Renders the node. * * Rendering is done in three steps: HTML generation, DOM element creation * and insertion, and an enhance step to bind event handlers. * * @function * @param {Node} el The DOM element where the node is to be rendered. The * node is inserted at the right position based on its "childPos" property. */ formNode.prototype.render = function (el) { var html = this.generate(); this.setContent(html, el); this.enhance(); }; /** * Inserts/Updates the HTML content of the node in the DOM. * * If the HTML is an update, the new HTML content replaces the old one. * The new HTML content is not moved around in the DOM in particular. * * The HTML is inserted at the right position in its parent's DOM subtree * otherwise (well, provided there are enough children, but that should always * be the case). * * @function * @param {string} html The HTML content to render * @param {Node} parentEl The DOM element that is to contain the DOM node. * This parameter is optional (the node's parent is used otherwise) and * is ignored if the node to render is already in the DOM tree. */ formNode.prototype.setContent = function (html, parentEl) { var node = $(html); var parentNode = parentEl || (this.parentNode ? this.parentNode.el : this.ownerTree.domRoot); var nextSibling = null; if (this.el) { // Replace the contents of the DOM element if the node is already in the tree $(this.el).replaceWith(node); } else { // Insert the node in the DOM if it's not already there nextSibling = $(parentNode).children().get(this.childPos); if (nextSibling) { $(nextSibling).before(node); } else { $(parentNode).append(node); } } // Save the link between the form node and the generated HTML this.el = node; // Update the node's subtree, extracting DOM elements that match the nodes // from the generated HTML this.updateElement(this.el); }; /** * Updates the DOM element associated with the node. * * Only nodes that have ID are directly associated with a DOM element. * * @function */ formNode.prototype.updateElement = function (domNode) { if (this.id) { this.el = $('#' + escapeSelector(this.id), domNode).get(0); if (this.view && this.view.getElement) { this.el = this.view.getElement(this.el); } if ((this.fieldtemplate !== false) && this.view && this.view.fieldtemplate) { // The field template wraps the element two or three level deep // in the DOM tree, depending on whether there is anything prepended // or appended to the input field this.el = $(this.el).parent().parent(); if (this.prepend || this.prepend) { this.el = this.el.parent(); } this.el = this.el.get(0); } if (this.parentNode && this.parentNode.view && this.parentNode.view.childTemplate) { // TODO: the child template may introduce more than one level, // so the number of levels introduced should rather be exposed // somehow in jsonform.fieldtemplate. this.el = $(this.el).parent().get(0); } } _.each(this.children, function (child) { child.updateElement(this.el || domNode); }); }; /** * Generates the view's HTML content for the underlying model. * * @function */ formNode.prototype.generate = function () { var data = { id: this.id, keydash: this.keydash, elt: this.formElement, schema: this.schemaElement, node: this, value: isSet(this.value) ? this.value : '', escape: escapeHTML }; var template = null; var html = ''; // Complete the data context if needed if (this.ownerTree.formDesc.onBeforeRender) { this.ownerTree.formDesc.onBeforeRender(data, this); } if (this.view.onBeforeRender) { this.view.onBeforeRender(data, this); } // Use the template that 'onBeforeRender' may have set, // falling back to that of the form element otherwise if (this.template) { template = this.template; } else if (this.formElement && this.formElement.template) { template = this.formElement.template; } else { template = this.view.template; } // Wrap the view template in the generic field template // (note the strict equality to 'false', needed as we fallback // to the view's setting otherwise) if ((this.fieldtemplate !== false) && (this.fieldtemplate || this.view.fieldtemplate)) { template = jsonform.fieldTemplate(template); } // Wrap the content in the child template of its parent if necessary. if (this.parentNode && this.parentNode.view && this.parentNode.view.childTemplate) { template = this.parentNode.view.childTemplate(template); } // Prepare the HTML of the children var childrenhtml = ''; _.each(this.children, function (child) { childrenhtml += child.generate(); }); data.children = childrenhtml; data.fieldHtmlClass = ''; if (this.ownerTree && this.ownerTree.formDesc && this.ownerTree.formDesc.params && this.ownerTree.formDesc.params.fieldHtmlClass) { data.fieldHtmlClass = this.ownerTree.formDesc.params.fieldHtmlClass; } if (this.formElement && (typeof this.formElement.fieldHtmlClass !== 'undefined')) { data.fieldHtmlClass = this.formElement.fieldHtmlClass; } // Apply the HTML template html = _.template(template, fieldTemplateSettings)(data); return html; }; /** * Enhances the view with additional logic, binding event handlers * in particular. * * The function also runs the "insert" event handler of the view and * form element if they exist (starting with that of the view) * * @function */ formNode.prototype.enhance = function () { var node = this; var handlers = null; var handler = null; var formData = _.clone(this.ownerTree.formDesc.tpldata) || {}; if (this.formElement) { // Check the view associated with the node as it may define an "onInsert" // event handler to be run right away if (this.view.onInsert) { this.view.onInsert({ target: $(this.el) }, this); } handlers = this.handlers || this.formElement.handlers; // Trigger the "insert" event handler handler = this.onInsert || this.formElement.onInsert; if (handler) { handler({ target: $(this.el) }, this); } if (handlers) { _.each(handlers, function (handler, onevent) { if (onevent === 'insert') { handler({ target: $(this.el) }, this); } }, this); } // No way to register event handlers if the DOM element is unknown // TODO: find some way to register event handlers even when this.el is not set. if (this.el) { // Register specific event handlers // TODO: Add support for other event handlers if (this.onChange) $(this.el).bind('change', function(evt) { node.onChange(evt, node); }); if (this.view.onChange) $(this.el).bind('change', function(evt) { node.view.onChange(evt, node); }); if (this.formElement.onChange) $(this.el).bind('change', function(evt) { node.formElement.onChange(evt, node); }); if (this.onClick) $(this.el).bind('click', function(evt) { node.onClick(evt, node); }); if (this.view.onClick) $(this.el).bind('click', function(evt) { node.view.onClick(evt, node); }); if (this.formElement.onClick) $(this.el).bind('click', function(evt) { node.formElement.onClick(evt, node); }); if (this.onKeyUp) $(this.el).bind('keyup', function(evt) { node.onKeyUp(evt, node); }); if (this.view.onKeyUp) $(this.el).bind('keyup', function(evt) { node.view.onKeyUp(evt, node); }); if (this.formElement.onKeyUp) $(this.el).bind('keyup', function(evt) { node.formElement.onKeyUp(evt, node); }); if (handlers) { _.each(handlers, function (handler, onevent) { if (onevent !== 'insert') { $(this.el).bind(onevent, function(evt) { handler(evt, node); }); } }, this); } } // Auto-update legend based on the input field that's associated with it if (this.legendChild && this.legendChild.formElement) { var onChangeHandler = function (evt) { if (node.formElement && node.formElement.legend && node.parentNode) { node.legend = applyArrayPath(node.formElement.legend, node.arrayPath); formData.idx = (node.arrayPath.length > 0) ? node.arrayPath[node.arrayPath.length - 1] + 1 : node.childPos + 1; formData.value = $(evt.target).val(); node.legend = _.template(node.legend, valueTemplateSettings)(formData); $(node.parentNode.el).trigger('legendUpdated'); } }; $(this.legendChild.el).bind('change', onChangeHandler); $(this.legendChild.el).bind('keyup', onChangeHandler); } } // Recurse down the tree to enhance children _.each(this.children, function (child) { child.enhance(); }); }; /** * Inserts an item in the array at the requested position and renders the item. * * @function * @param {Number} idx Insertion index */ formNode.prototype.insertArrayItem = function (idx, domElement) { var i = 0; // Insert element at the end of the array if index is not given if (idx === undefined) { idx = this.children.length; } // Create the additional array item at the end of the list, // using the item template created when tree was initialized // (the call to resetValues ensures that 'arrayPath' is correctly set) var child = this.childTemplate.clone(); this.appendChild(child); child.resetValues(); // To create a blank array item at the requested position, // shift values down starting at the requested position // one to insert (note we start with the end of the array on purpose) for (i = this.children.length-2; i >= idx; i--) { this.children[i].moveValuesTo(this.children[i+1]); } // Initialize the blank node we've created with default values this.children[idx].resetValues(); this.children[idx].computeInitialValues(); // Re-render all children that have changed for (i = idx; i < this.children.length; i++) { this.children[i].render(domElement); } }; /** * Remove an item from an array * * @function * @param {Number} idx The index number of the item to remove */ formNode.prototype.deleteArrayItem = function (idx) { var i = 0; var child = null; // Delete last item if no index is given if (idx === undefined) { idx = this.children.length - 1; } // Move values up in the array for (i = idx; i < this.children.length-1; i++) { this.children[i+1].moveValuesTo(this.children[i]); this.children[i].render(); } // Remove the last array item from the DOM tree and from the form tree this.removeChild(); }; /** * Returns the minimum/maximum number of items that an array field * is allowed to have according to the schema definition of the fields * it contains. * * The function parses the schema definitions of the array items that * compose the current "array" node and returns the minimum value of * "maxItems" it encounters as the maximum number of items, and the * maximum value of "minItems" as the minimum number of items. * * The function reports a -1 for either of the boundaries if the schema * does not put any constraint on the number of elements the current * array may have of if the current node is not an array. * * Note that array boundaries should be defined in the JSON Schema using * "minItems" and "maxItems". The code also supports "minLength" and * "maxLength" as a fallback, mostly because it used to by mistake (see #22) * and because other people could make the same mistake. * * @function * @return {Object} An object with properties "minItems" and "maxItems" * that reports the corresponding number of items that the array may * have (value is -1 when there is no constraint for that boundary) */ formNode.prototype.getArrayBoundaries = function () { var boundaries = { minItems: -1, maxItems: -1 }; if (!this.view || !this.view.array) return boundaries; var getNodeBoundaries = function (node, initialNode) { var schemaKey = null; var arrayKey = null; var boundaries = { minItems: -1, maxItems: -1 }; initialNode = initialNode || node; if (node.view && node.view.array && (node !== initialNode)) { // New array level not linked to an array in the schema, // so no size constraints return boundaries; } if (node.key) { // Note the conversion to target the actual array definition in the // schema where minItems/maxItems may be defined. If we're still looking // at the initial node, the goal is to convert from: // foo[0].bar[3].baz to foo[].bar[].baz // If we're not looking at the initial node, the goal is to look at the // closest array parent: // foo[0].bar[3].baz to foo[].bar arrayKey = node.key.replace(/\[[0-9]+\]/g, '[]'); if (node !== initialNode) { arrayKey = arrayKey.replace(/\[\][^\[\]]*$/, ''); } schemaKey = getSchemaKey( node.ownerTree.formDesc.schema.properties, arrayKey ); if (!schemaKey) return boundaries; return { minItems: schemaKey.minItems || schemaKey.minLength || -1, maxItems: schemaKey.maxItems || schemaKey.maxLength || -1 }; } else { _.each(node.children, function (child) { var subBoundaries = getNodeBoundaries(child, initialNode); if (subBoundaries.minItems !== -1) { if (boundaries.minItems !== -1) { boundaries.minItems = Math.max( boundaries.minItems, subBoundaries.minItems ); } else { boundaries.minItems = subBoundaries.minItems; } } if (subBoundaries.maxItems !== -1) { if (boundaries.maxItems !== -1) { boundaries.maxItems = Math.min( boundaries.maxItems, subBoundaries.maxItems ); } else { boundaries.maxItems = subBoundaries.maxItems; } } }); } return boundaries; }; return getNodeBoundaries(this); }; /** * Form tree class. * * Holds the internal representation of the form. * The tree is always in sync with the rendered form, this allows to parse * it easily. * * @class */ var formTree = function () { this.eventhandlers = []; this.root = null; this.formDesc = null; }; /** * Initializes the form tree structure from the JSONForm object * * This function is the main entry point of the JSONForm library. * * Initialization steps: * 1. the internal tree structure that matches the JSONForm object * gets created (call to buildTree) * 2. initial values are computed from previously submitted values * or from the default values defined in the JSON schema. * * When the function returns, the tree is ready to be rendered through * a call to "render". * * @function */ formTree.prototype.initialize = function (formDesc) { formDesc = formDesc || {}; // Keep a pointer to the initial JSONForm // (note clone returns a shallow copy, only first-level is cloned) this.formDesc = _.clone(formDesc); // Compute form prefix if no prefix is given. this.formDesc.prefix = this.formDesc.prefix || 'jsonform-' + _.uniqueId(); // JSON schema shorthand if (this.formDesc.schema && !this.formDesc.schema.properties) { this.formDesc.schema = { properties: this.formDesc.schema }; } // Ensure layout is set this.formDesc.form = this.formDesc.form || [ '*', { type: 'actions', items: [ { type: 'submit', value: 'Submit' } ] } ]; this.formDesc.form = (_.isArray(this.formDesc.form) ? this.formDesc.form : [this.formDesc.form]); this.formDesc.params = this.formDesc.params || {}; // Create the root of the tree this.root = new formNode(); this.root.ownerTree = this; this.root.view = jsonform.elementTypes['root']; // Generate the tree from the form description this.buildTree(); // Compute the values associated with each node // (for arrays, the computation actually creates the form nodes) this.computeInitialValues(); }; /** * Constructs the tree from the form description. * * The function must be called once when the tree is first created. * * @function */ formTree.prototype.buildTree = function () { // Parse and generate the form structure based on the elements encountered: // - '*' means "generate all possible fields using default layout" // - a key reference to target a specific data element // - a more complex object to generate specific form sections _.each(this.formDesc.form, function (formElement) { if (formElement === '*') { _.each(this.formDesc.schema.properties, function (element, key) { this.root.appendChild(this.buildFromLayout({ key: key })); }, this); } else { if (_.isString(formElement)) { formElement = { key: formElement }; } this.root.appendChild(this.buildFromLayout(formElement)); } }, this); }; /** * Builds the internal form tree representation from the requested layout. * * The function is recursive, generating the node children as necessary. * The function extracts the values from the previously submitted values * (this.formDesc.value) or from default values defined in the schema. * * @function * @param {Object} formElement JSONForm element to render * @param {Object} context The parsing context (the array depth in particular) * @return {Object} The node that matches the element. */ formTree.prototype.buildFromLayout = function (formElement, context) { var schemaElement = null; var node = new formNode(); var view = null; var key = null; // The form element parameter directly comes from the initial // JSONForm object. We'll make a shallow copy of it and of its children // not to pollute the original object. // (note JSON.parse(JSON.stringify()) cannot be used since there may be // event handlers in there!) formElement = _.clone(formElement); if (formElement.items) { if (_.isArray(formElement.items)) { formElement.items = _.map(formElement.items, _.clone); } else { formElement.items = [ _.clone(formElement.items) ]; } } if (formElement.key) { // The form element is directly linked to an element in the JSON // schema. The properties of the form element override those of the // element in the JSON schema. Properties from the JSON schema complete // those of the form element otherwise. // Retrieve the element from the JSON schema schemaElement = getSchemaKey( this.formDesc.schema.properties, formElement.key); if (!schemaElement) { // The JSON Form is invalid! throw new Error('The JSONForm object references the schema key "' + formElement.key + '" but that key does not exist in the JSON schema'); } // Schema element has just been found, let's trigger the // "onElementSchema" event // (tidoust: not sure what the use case for this is, keeping the // code for backward compatibility) if (this.formDesc.onElementSchema) { this.formDesc.onElementSchema(formElement, schemaElement); } formElement.name = formElement.name || formElement.key; formElement.title = formElement.title || schemaElement.title; formElement.description = formElement.description || schemaElement.description; formElement.readOnly = formElement.readOnly || schemaElement.readOnly || formElement.readonly || schemaElement.readonly; // Compute the ID of the input field if (!formElement.id) { formElement.id = escapeSelector(this.formDesc.prefix) + '-elt-' + slugify(formElement.key); } // Should empty strings be included in the final value? // TODO: it's rather unclean to pass it through the schema. if (formElement.allowEmpty) { schemaElement._jsonform_allowEmpty = true; } // If the form element does not define its type, use the type of // the schema element. if (!formElement.type) { // If schema type is an array containing only a type and "null", // remove null and make the element non-required if (_.isArray(schemaElement.type)) { if (_.contains(schemaElement.type, "null")) { schemaElement.type = _.without(schemaElement.type, "null"); schemaElement.required = false; } if (schemaElement.type.length > 1) { throw new Error("Cannot process schema element with multiple types."); } schemaElement.type = _.first(schemaElement.type); } if ((schemaElement.type === 'string') && (schemaElement.format === 'color')) { formElement.type = 'color'; } else if ((schemaElement.type === 'number' || schemaElement.type === 'integer') && !schemaElement['enum']) { formElement.type = 'number'; if (schemaElement.type === 'number') schemaElement.step = 'any'; } else if ((schemaElement.type === 'string' || schemaElement.type === 'any') && !schemaElement['enum']) { formElement.type = 'text'; } else if (schemaElement.type === 'boolean') { formElement.type = 'checkbox'; } else if (schemaElement.type === 'object') { if (schemaElement.properties) { formElement.type = 'fieldset'; } else { formElement.type = 'textarea'; } } else if (!_.isUndefined(schemaElement['enum'])) { formElement.type = 'select'; } else { formElement.type = schemaElement.type; } } // Unless overridden in the definition of the form element (or unless // there's a titleMap defined), use the enumeration list defined in // the schema if (!formElement.options && schemaElement['enum']) { if (formElement.titleMap) { formElement.options = _.map(schemaElement['enum'], function (value) { return { value: value, title: hasOwnProperty(formElement.titleMap, value) ? formElement.titleMap[value] : value }; }); } else { formElement.options = schemaElement['enum']; } } // Flag a list of checkboxes with multiple choices if ((formElement.type === 'checkboxes') && schemaElement.items) { var itemsEnum = schemaElement.items['enum']; if (itemsEnum) { schemaElement.items._jsonform_checkboxes_as_array = true; } if (!itemsEnum && schemaElement.items[0]) { itemsEnum = schemaElement.items[0]['enum']; if (itemsEnum) { schemaElement.items[0]._jsonform_checkboxes_as_array = true; } } } // If the form element targets an "object" in the JSON schema, // we need to recurse through the list of children to create an // input field per child property of the object in the JSON schema if (schemaElement.type === 'object') { _.each(schemaElement.properties, function (prop, propName) { node.appendChild(this.buildFromLayout({ key: formElement.key + '.' + propName })); }, this); } } if (!formElement.type) { formElement.type = 'none'; } view = jsonform.elementTypes[formElement.type]; if (!view) { throw new Error('The JSONForm contains an element whose type is unknown: "' + formElement.type + '"'); } if (schemaElement) { // The form element is linked to an element in the schema. // Let's make sure the types are compatible. // In particular, the element must not be a "container" // (or must be an "object" or "array" container) if (!view.inputfield && !view.array && (formElement.type !== 'selectfieldset') && (schemaElement.type !== 'object')) { throw new Error('The JSONForm contains an element that links to an ' + 'element in the JSON schema (key: "' + formElement.key + '") ' + 'and that should not based on its type ("' + formElement.type + '")'); } } else { // The form element is not linked to an element in the schema. // This means the form element must be a "container" element, // and must not define an input field. if (view.inputfield && (formElement.type !== 'selectfieldset')) { throw new Error('The JSONForm defines an element of type ' + '"' + formElement.type + '" ' + 'but no "key" property to link the input field to the JSON schema'); } } // A few characters need to be escaped to use the ID as jQuery selector formElement.iddot = escapeSelector(formElement.id || ''); // Initialize the form node from the form element and schema element node.formElement = formElement; node.schemaElement = schemaElement; node.view = view; node.ownerTree = this; // Set event handlers if (!formElement.handlers) { formElement.handlers = {}; } // Parse children recursively if (node.view.array) { // The form element is an array. The number of items in an array // is by definition dynamic, up to the form user (through "Add more", // "Delete" commands). The positions of the items in the array may // also change over time (through "Move up", "Move down" commands). // // The form node stores a "template" node that serves as basis for // the creation of an item in the array. // // Array items may be complex forms themselves, allowing for nesting. // // The initial values set the initial number of items in the array. // Note a form element contains at least one item when it is rendered. if (formElement.items) { key = formElement.items[0] || formElement.items; } else { key = formElement.key + '[]'; } if (_.isString(key)) { key = { key: key }; } node.setChildTemplate(this.buildFromLayout(key)); } else if (formElement.items) { // The form element defines children elements _.each(formElement.items, function (item) { if (_.isString(item)) { item = { key: item }; } node.appendChild(this.buildFromLayout(item)); }, this); } return node; }; /** * Computes the values associated with each input field in the tree based * on previously submitted values or default values in the JSON schema. * * For arrays, the function actually creates and inserts additional * nodes in the tree based on previously submitted values (also ensuring * that the array has at least one item). * * The function sets the array path on all nodes. * It should be called once in the lifetime of a form tree right after * the tree structure has been created. * * @function */ formTree.prototype.computeInitialValues = function () { this.root.computeInitialValues(this.formDesc.value); }; /** * Renders the form tree * * @function * @param {Node} domRoot The "form" element in the DOM tree that serves as * root for the form */ formTree.prototype.render = function (domRoot) { if (!domRoot) return; this.domRoot = domRoot; this.root.render(); // If the schema defines required fields, flag the form with the // "jsonform-hasrequired" class for styling purpose // (typically so that users may display a legend) if (this.hasRequiredField()) { $(domRoot).addClass('jsonform-hasrequired'); } }; /** * Walks down the element tree with a callback * * @function * @param {Function} callback The callback to call on each element */ formTree.prototype.forEachElement = function (callback) { var f = function(root) { for (var i=0;i tag in the DOM * @return {Object} The object that follows the data schema and matches the * values entered by the user. */ jsonform.getFormValue = function (formelt) { var form = $(formelt).data('jsonform-tree'); if (!form) return null; return form.root.getFormValues(); }; /** * Highlights errors reported by the JSON schema validator in the document. * * @function * @param {Object} errors List of errors reported by the JSON schema validator * @param {Object} options The JSON Form object that describes the form * (unused for the time being, could be useful to store example values or * specific error messages) */ $.fn.jsonFormErrors = function(errors, options) { $(".error", this).removeClass("error"); $(".warning", this).removeClass("warning"); $(".jsonform-errortext", this).hide(); if (!errors) return; var errorSelectors = []; for (var i = 0; i < errors.length; i++) { // Compute the address of the input field in the form from the URI // returned by the JSON schema validator. // These URIs typically look like: // urn:uuid:cccc265e-ffdd-4e40-8c97-977f7a512853#/pictures/1/thumbnail // What we need from that is the path in the value object: // pictures[1].thumbnail // ... and the jQuery-friendly class selector of the input field: // .jsonform-error-pictures\[1\]---thumbnail var key = errors[i].uri .replace(/.*#\//, '') .replace(/\//g, '.') .replace(/\.([0-9]+)(?=\.|$)/g, '[$1]'); var errormarkerclass = ".jsonform-error-" + escapeSelector(key.replace(/\./g,"---")); errorSelectors.push(errormarkerclass); var errorType = errors[i].type || "error"; $(errormarkerclass, this).addClass(errorType); $(errormarkerclass + " .jsonform-errortext", this).html(errors[i].message).show(); } // Look for the first error in the DOM and ensure the element // is visible so that the user understands that something went wrong errorSelectors = errorSelectors.join(','); var firstError = $(errorSelectors).get(0); if (firstError && firstError.scrollIntoView) { firstError.scrollIntoView(true, { behavior: 'smooth' }); } }; /** * Generates the HTML form from the given JSON Form object and renders the form. * * Main entry point of the library. Defined as a jQuery function that typically * needs to be applied to a
    element in the document. * * The function handles the following properties for the JSON Form object it * receives as parameter: * - schema (required): The JSON Schema that describes the form to render * - form: The options form layout description, overrides default layout * - prefix: String to use to prefix computed IDs. Default is an empty string. * Use this option if JSON Form is used multiple times in an application with * schemas that have overlapping parameter names to avoid running into multiple * IDs issues. Default value is "jsonform-[counter]". * - transloadit: Transloadit parameters when transloadit is used * - validate: Validates form against schema upon submission. Uses the value * of the "validate" property as validator if it is an object. * - displayErrors: Function to call with errors upon form submission. * Default is to render the errors next to the input fields. * - submitEvent: Name of the form submission event to bind to. * Default is "submit". Set this option to false to avoid event binding. * - onSubmit: Callback function to call when form is submitted * - onSubmitValid: Callback function to call when form is submitted without * errors. * * @function * @param {Object} options The JSON Form object to use as basis for the form */ $.fn.jsonForm = function(options) { var formElt = this; options = _.defaults({}, options, {submitEvent: 'submit'}); var form = new formTree(); form.initialize(options); form.render(formElt.get(0)); // TODO: move that to formTree.render if (options.transloadit) { formElt.append(''); } // Keep a direct pointer to the JSON schema for form submission purpose formElt.data("jsonform-tree", form); if (options.submitEvent) { formElt.unbind((options.submitEvent)+'.jsonform'); formElt.bind((options.submitEvent)+'.jsonform', function(evt) { form.submit(evt); }); } // Initialize tabs sections, if any initializeTabs(formElt); // Initialize expandable sections, if any $('.expandable > div, .expandable > fieldset', formElt).hide(); formElt.on('click', '.expandable > legend', function () { var parent = $(this).parent(); parent.toggleClass('expanded'); $('> div', parent).slideToggle(100); }); return form; }; /** * Retrieves the structured values object generated from the values * entered by the user and the data schema that gave birth to the form. * * Defined as a jQuery function that typically needs to be applied to * a element whose content has previously been generated by a * call to "jsonForm". * * Unless explicitly disabled, the values are automatically validated * against the constraints expressed in the schema. * * @function * @return {Object} Structured values object that matches the user inputs * and the data schema. */ $.fn.jsonFormValue = function() { return jsonform.getFormValue(this); }; // Expose the getFormValue method to the global object // (other methods exposed as jQuery functions) global.JSONForm = global.JSONForm || {util:{}}; global.JSONForm.getFormValue = jsonform.getFormValue; global.JSONForm.fieldTemplate = jsonform.fieldTemplate; global.JSONForm.fieldTypes = jsonform.elementTypes; global.JSONForm.getInitialValue = getInitialValue; global.JSONForm.util.getObjKey = jsonform.util.getObjKey; global.JSONForm.util.setObjKey = jsonform.util.setObjKey; })((typeof exports !== 'undefined'), ((typeof exports !== 'undefined') ? exports : window), ((typeof jQuery !== 'undefined') ? jQuery : { fn: {} }), ((typeof _ !== 'undefined') ? _ : null), JSON); },{"underscore":2}],2:[function(require,module,exports){ (function (global){(function (){ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define('underscore', factory) : (global = global || self, (function () { var current = global._; var exports = global._ = factory(); exports.noConflict = function () { global._ = current; return exports; }; }())); }(this, (function () { // Underscore.js 1.12.0 // https://underscorejs.org // (c) 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. // Current version. var VERSION = '1.12.0'; // Establish the root object, `window` (`self`) in the browser, `global` // on the server, or `this` in some virtual machines. We use `self` // instead of `window` for `WebWorker` support. var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global || Function('return this')() || {}; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype; var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; // Create quick reference variables for speed access to core prototypes. var push = ArrayProto.push, slice = ArrayProto.slice, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // Modern feature detection. var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined', supportsDataView = typeof DataView !== 'undefined'; // All **ECMAScript 5+** native function implementations that we hope to use // are declared here. var nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeCreate = Object.create, nativeIsView = supportsArrayBuffer && ArrayBuffer.isView; // Create references to these builtin functions because we override them. var _isNaN = isNaN, _isFinite = isFinite; // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; // The largest integer that can be represented exactly. var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; // Some functions take a variable number of arguments, or a few expected // arguments at the beginning and then a variable number of values to operate // on. This helper accumulates all remaining arguments past the function’s // argument length (or an explicit `startIndex`), into an array that becomes // the last argument. Similar to ES6’s "rest parameter". function restArguments(func, startIndex) { startIndex = startIndex == null ? func.length - 1 : +startIndex; return function() { var length = Math.max(arguments.length - startIndex, 0), rest = Array(length), index = 0; for (; index < length; index++) { rest[index] = arguments[index + startIndex]; } switch (startIndex) { case 0: return func.call(this, rest); case 1: return func.call(this, arguments[0], rest); case 2: return func.call(this, arguments[0], arguments[1], rest); } var args = Array(startIndex + 1); for (index = 0; index < startIndex; index++) { args[index] = arguments[index]; } args[startIndex] = rest; return func.apply(this, args); }; } // Is a given variable an object? function isObject(obj) { var type = typeof obj; return type === 'function' || type === 'object' && !!obj; } // Is a given value equal to null? function isNull(obj) { return obj === null; } // Is a given variable undefined? function isUndefined(obj) { return obj === void 0; } // Is a given value a boolean? function isBoolean(obj) { return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; } // Is a given value a DOM element? function isElement(obj) { return !!(obj && obj.nodeType === 1); } // Internal function for creating a `toString`-based type tester. function tagTester(name) { var tag = '[object ' + name + ']'; return function(obj) { return toString.call(obj) === tag; }; } var isString = tagTester('String'); var isNumber = tagTester('Number'); var isDate = tagTester('Date'); var isRegExp = tagTester('RegExp'); var isError = tagTester('Error'); var isSymbol = tagTester('Symbol'); var isArrayBuffer = tagTester('ArrayBuffer'); var isFunction = tagTester('Function'); // Optimize `isFunction` if appropriate. Work around some `typeof` bugs in old // v8, IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). var nodelist = root.document && root.document.childNodes; if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { isFunction = function(obj) { return typeof obj == 'function' || false; }; } var isFunction$1 = isFunction; var hasObjectTag = tagTester('Object'); // In IE 10 - Edge 13, `DataView` has string tag `'[object Object]'`. // In IE 11, the most common among them, this problem also applies to // `Map`, `WeakMap` and `Set`. var hasStringTagBug = ( supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))) ), isIE11 = (typeof Map !== 'undefined' && hasObjectTag(new Map)); var isDataView = tagTester('DataView'); // In IE 10 - Edge 13, we need a different heuristic // to determine whether an object is a `DataView`. function ie10IsDataView(obj) { return obj != null && isFunction$1(obj.getInt8) && isArrayBuffer(obj.buffer); } var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView); // Is a given value an array? // Delegates to ECMA5's native `Array.isArray`. var isArray = nativeIsArray || tagTester('Array'); // Internal function to check whether `key` is an own property name of `obj`. function has(obj, key) { return obj != null && hasOwnProperty.call(obj, key); } var isArguments = tagTester('Arguments'); // Define a fallback version of the method in browsers (ahem, IE < 9), where // there isn't any inspectable "Arguments" type. (function() { if (!isArguments(arguments)) { isArguments = function(obj) { return has(obj, 'callee'); }; } }()); var isArguments$1 = isArguments; // Is a given object a finite number? function isFinite$1(obj) { return !isSymbol(obj) && _isFinite(obj) && !isNaN(parseFloat(obj)); } // Is the given value `NaN`? function isNaN$1(obj) { return isNumber(obj) && _isNaN(obj); } // Predicate-generating function. Often useful outside of Underscore. function constant(value) { return function() { return value; }; } // Common internal logic for `isArrayLike` and `isBufferLike`. function createSizePropertyCheck(getSizeProperty) { return function(collection) { var sizeProperty = getSizeProperty(collection); return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX; } } // Internal helper to generate a function to obtain property `key` from `obj`. function shallowProperty(key) { return function(obj) { return obj == null ? void 0 : obj[key]; }; } // Internal helper to obtain the `byteLength` property of an object. var getByteLength = shallowProperty('byteLength'); // Internal helper to determine whether we should spend extensive checks against // `ArrayBuffer` et al. var isBufferLike = createSizePropertyCheck(getByteLength); // Is a given value a typed array? var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/; function isTypedArray(obj) { // `ArrayBuffer.isView` is the most future-proof, so use it when available. // Otherwise, fall back on the above regular expression. return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) : isBufferLike(obj) && typedArrayPattern.test(toString.call(obj)); } var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false); // Internal helper to obtain the `length` property of an object. var getLength = shallowProperty('length'); // Internal helper to create a simple lookup structure. // `collectNonEnumProps` used to depend on `_.contains`, but this led to // circular imports. `emulatedSet` is a one-off solution that only works for // arrays of strings. function emulatedSet(keys) { var hash = {}; for (var l = keys.length, i = 0; i < l; ++i) hash[keys[i]] = true; return { contains: function(key) { return hash[key]; }, push: function(key) { hash[key] = true; return keys.push(key); } }; } // Internal helper. Checks `keys` for the presence of keys in IE < 9 that won't // be iterated by `for key in ...` and thus missed. Extends `keys` in place if // needed. function collectNonEnumProps(obj, keys) { keys = emulatedSet(keys); var nonEnumIdx = nonEnumerableProps.length; var constructor = obj.constructor; var proto = isFunction$1(constructor) && constructor.prototype || ObjProto; // Constructor is a special case. var prop = 'constructor'; if (has(obj, prop) && !keys.contains(prop)) keys.push(prop); while (nonEnumIdx--) { prop = nonEnumerableProps[nonEnumIdx]; if (prop in obj && obj[prop] !== proto[prop] && !keys.contains(prop)) { keys.push(prop); } } } // Retrieve the names of an object's own properties. // Delegates to **ECMAScript 5**'s native `Object.keys`. function keys(obj) { if (!isObject(obj)) return []; if (nativeKeys) return nativeKeys(obj); var keys = []; for (var key in obj) if (has(obj, key)) keys.push(key); // Ahem, IE < 9. if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; } // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. function isEmpty(obj) { if (obj == null) return true; // Skip the more expensive `toString`-based type checks if `obj` has no // `.length`. var length = getLength(obj); if (typeof length == 'number' && ( isArray(obj) || isString(obj) || isArguments$1(obj) )) return length === 0; return getLength(keys(obj)) === 0; } // Returns whether an object has a given set of `key:value` pairs. function isMatch(object, attrs) { var _keys = keys(attrs), length = _keys.length; if (object == null) return !length; var obj = Object(object); for (var i = 0; i < length; i++) { var key = _keys[i]; if (attrs[key] !== obj[key] || !(key in obj)) return false; } return true; } // If Underscore is called as a function, it returns a wrapped object that can // be used OO-style. This wrapper holds altered versions of all functions added // through `_.mixin`. Wrapped objects may be chained. function _(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; } _.VERSION = VERSION; // Extracts the result from a wrapped and chained object. _.prototype.value = function() { return this._wrapped; }; // Provide unwrapping proxies for some methods used in engine operations // such as arithmetic and JSON stringification. _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; _.prototype.toString = function() { return String(this._wrapped); }; // Internal function to wrap or shallow-copy an ArrayBuffer, // typed array or DataView to a new view, reusing the buffer. function toBufferView(bufferSource) { return new Uint8Array( bufferSource.buffer || bufferSource, bufferSource.byteOffset || 0, getByteLength(bufferSource) ); } // We use this string twice, so give it a name for minification. var tagDataView = '[object DataView]'; // Internal recursive comparison function for `_.isEqual`. function eq(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a === 1 / b; // `null` or `undefined` only equal to itself (strict comparison). if (a == null || b == null) return false; // `NaN`s are equivalent, but non-reflexive. if (a !== a) return b !== b; // Exhaust primitive checks var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; return deepEq(a, b, aStack, bStack); } // Internal recursive comparison function for `_.isEqual`. function deepEq(a, b, aStack, bStack) { // Unwrap any wrapped objects. if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. var className = toString.call(a); if (className !== toString.call(b)) return false; // Work around a bug in IE 10 - Edge 13. if (hasStringTagBug && className == '[object Object]' && isDataView$1(a)) { if (!isDataView$1(b)) return false; className = tagDataView; } switch (className) { // These types are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return '' + a === '' + b; case '[object Number]': // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN. if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a === +b; case '[object Symbol]': return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); case '[object ArrayBuffer]': case tagDataView: // Coerce to typed array so we can fall through. return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); } var areArrays = className === '[object Array]'; if (!areArrays && isTypedArray$1(a)) { var byteLength = getByteLength(a); if (byteLength !== getByteLength(b)) return false; if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; areArrays = true; } if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; // Objects with different constructors are not equivalent, but `Object`s or `Array`s // from different frames are. var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && isFunction$1(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // Initializing stack of traversed objects. // It's done here since we only need them for objects and arrays comparison. aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; } // Add the first object to the stack of traversed objects. aStack.push(a); bStack.push(b); // Recursively compare objects and arrays. if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. length = a.length; if (length !== b.length) return false; // Deep compare the contents, ignoring non-numeric properties. while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // Deep compare objects. var _keys = keys(a), key; length = _keys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. if (keys(b).length !== length) return false; while (length--) { // Deep compare each member key = _keys[length]; if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } } // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); return true; } // Perform a deep comparison to check if two objects are equal. function isEqual(a, b) { return eq(a, b); } // Retrieve all the enumerable property names of an object. function allKeys(obj) { if (!isObject(obj)) return []; var keys = []; for (var key in obj) keys.push(key); // Ahem, IE < 9. if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; } // Since the regular `Object.prototype.toString` type tests don't work for // some types in IE 11, we use a fingerprinting heuristic instead, based // on the methods. It's not great, but it's the best we got. // The fingerprint method lists are defined below. function ie11fingerprint(methods) { var length = getLength(methods); return function(obj) { if (obj == null) return false; // `Map`, `WeakMap` and `Set` have no enumerable keys. var keys = allKeys(obj); if (getLength(keys)) return false; for (var i = 0; i < length; i++) { if (!isFunction$1(obj[methods[i]])) return false; } // If we are testing against `WeakMap`, we need to ensure that // `obj` doesn't have a `forEach` method in order to distinguish // it from a regular `Map`. return methods !== weakMapMethods || !isFunction$1(obj[forEachName]); }; } // In the interest of compact minification, we write // each string in the fingerprints only once. var forEachName = 'forEach', hasName = 'has', commonInit = ['clear', 'delete'], mapTail = ['get', hasName, 'set']; // `Map`, `WeakMap` and `Set` each have slightly different // combinations of the above sublists. var mapMethods = commonInit.concat(forEachName, mapTail), weakMapMethods = commonInit.concat(mapTail), setMethods = ['add'].concat(commonInit, forEachName, hasName); var isMap = isIE11 ? ie11fingerprint(mapMethods) : tagTester('Map'); var isWeakMap = isIE11 ? ie11fingerprint(weakMapMethods) : tagTester('WeakMap'); var isSet = isIE11 ? ie11fingerprint(setMethods) : tagTester('Set'); var isWeakSet = tagTester('WeakSet'); // Retrieve the values of an object's properties. function values(obj) { var _keys = keys(obj); var length = _keys.length; var values = Array(length); for (var i = 0; i < length; i++) { values[i] = obj[_keys[i]]; } return values; } // Convert an object into a list of `[key, value]` pairs. // The opposite of `_.object` with one argument. function pairs(obj) { var _keys = keys(obj); var length = _keys.length; var pairs = Array(length); for (var i = 0; i < length; i++) { pairs[i] = [_keys[i], obj[_keys[i]]]; } return pairs; } // Invert the keys and values of an object. The values must be serializable. function invert(obj) { var result = {}; var _keys = keys(obj); for (var i = 0, length = _keys.length; i < length; i++) { result[obj[_keys[i]]] = _keys[i]; } return result; } // Return a sorted list of the function names available on the object. function functions(obj) { var names = []; for (var key in obj) { if (isFunction$1(obj[key])) names.push(key); } return names.sort(); } // An internal function for creating assigner functions. function createAssigner(keysFunc, defaults) { return function(obj) { var length = arguments.length; if (defaults) obj = Object(obj); if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!defaults || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; } // Extend a given object with all the properties in passed-in object(s). var extend = createAssigner(allKeys); // Assigns a given object with all the own properties in the passed-in // object(s). // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) var extendOwn = createAssigner(keys); // Fill in a given object with default properties. var defaults = createAssigner(allKeys, true); // Create a naked function reference for surrogate-prototype-swapping. function ctor() { return function(){}; } // An internal function for creating a new object that inherits from another. function baseCreate(prototype) { if (!isObject(prototype)) return {}; if (nativeCreate) return nativeCreate(prototype); var Ctor = ctor(); Ctor.prototype = prototype; var result = new Ctor; Ctor.prototype = null; return result; } // Creates an object that inherits from the given prototype object. // If additional properties are provided then they will be added to the // created object. function create(prototype, props) { var result = baseCreate(prototype); if (props) extendOwn(result, props); return result; } // Create a (shallow-cloned) duplicate of an object. function clone(obj) { if (!isObject(obj)) return obj; return isArray(obj) ? obj.slice() : extend({}, obj); } // Invokes `interceptor` with the `obj` and then returns `obj`. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. function tap(obj, interceptor) { interceptor(obj); return obj; } // Normalize a (deep) property `path` to array. // Like `_.iteratee`, this function can be customized. function toPath(path) { return isArray(path) ? path : [path]; } _.toPath = toPath; // Internal wrapper for `_.toPath` to enable minification. // Similar to `cb` for `_.iteratee`. function toPath$1(path) { return _.toPath(path); } // Internal function to obtain a nested property in `obj` along `path`. function deepGet(obj, path) { var length = path.length; for (var i = 0; i < length; i++) { if (obj == null) return void 0; obj = obj[path[i]]; } return length ? obj : void 0; } // Get the value of the (deep) property on `path` from `object`. // If any property in `path` does not exist or if the value is // `undefined`, return `defaultValue` instead. // The `path` is normalized through `_.toPath`. function get(object, path, defaultValue) { var value = deepGet(object, toPath$1(path)); return isUndefined(value) ? defaultValue : value; } // Shortcut function for checking if an object has a given property directly on // itself (in other words, not on a prototype). Unlike the internal `has` // function, this public version can also traverse nested properties. function has$1(obj, path) { path = toPath$1(path); var length = path.length; for (var i = 0; i < length; i++) { var key = path[i]; if (!has(obj, key)) return false; obj = obj[key]; } return !!length; } // Keep the identity function around for default iteratees. function identity(value) { return value; } // Returns a predicate for checking whether an object has a given set of // `key:value` pairs. function matcher(attrs) { attrs = extendOwn({}, attrs); return function(obj) { return isMatch(obj, attrs); }; } // Creates a function that, when passed an object, will traverse that object’s // properties down the given `path`, specified as an array of keys or indices. function property(path) { path = toPath$1(path); return function(obj) { return deepGet(obj, path); }; } // Internal function that returns an efficient (for current engines) version // of the passed-in callback, to be repeatedly applied in other Underscore // functions. function optimizeCb(func, context, argCount) { if (context === void 0) return func; switch (argCount == null ? 3 : argCount) { case 1: return function(value) { return func.call(context, value); }; // The 2-argument case is omitted because we’re not using it. case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } return function() { return func.apply(context, arguments); }; } // An internal function to generate callbacks that can be applied to each // element in a collection, returning the desired result — either `_.identity`, // an arbitrary callback, a property matcher, or a property accessor. function baseIteratee(value, context, argCount) { if (value == null) return identity; if (isFunction$1(value)) return optimizeCb(value, context, argCount); if (isObject(value) && !isArray(value)) return matcher(value); return property(value); } // External wrapper for our callback generator. Users may customize // `_.iteratee` if they want additional predicate/iteratee shorthand styles. // This abstraction hides the internal-only `argCount` argument. function iteratee(value, context) { return baseIteratee(value, context, Infinity); } _.iteratee = iteratee; // The function we call internally to generate a callback. It invokes // `_.iteratee` if overridden, otherwise `baseIteratee`. function cb(value, context, argCount) { if (_.iteratee !== iteratee) return _.iteratee(value, context); return baseIteratee(value, context, argCount); } // Returns the results of applying the `iteratee` to each element of `obj`. // In contrast to `_.map` it returns an object. function mapObject(obj, iteratee, context) { iteratee = cb(iteratee, context); var _keys = keys(obj), length = _keys.length, results = {}; for (var index = 0; index < length; index++) { var currentKey = _keys[index]; results[currentKey] = iteratee(obj[currentKey], currentKey, obj); } return results; } // Predicate-generating function. Often useful outside of Underscore. function noop(){} // Generates a function for a given object that returns a given property. function propertyOf(obj) { if (obj == null) return noop; return function(path) { return get(obj, path); }; } // Run a function **n** times. function times(n, iteratee, context) { var accum = Array(Math.max(0, n)); iteratee = optimizeCb(iteratee, context, 1); for (var i = 0; i < n; i++) accum[i] = iteratee(i); return accum; } // Return a random integer between `min` and `max` (inclusive). function random(min, max) { if (max == null) { max = min; min = 0; } return min + Math.floor(Math.random() * (max - min + 1)); } // A (possibly faster) way to get the current timestamp as an integer. var now = Date.now || function() { return new Date().getTime(); }; // Internal helper to generate functions for escaping and unescaping strings // to/from HTML interpolation. function createEscaper(map) { var escaper = function(match) { return map[match]; }; // Regexes for identifying a key that needs to be escaped. var source = '(?:' + keys(map).join('|') + ')'; var testRegexp = RegExp(source); var replaceRegexp = RegExp(source, 'g'); return function(string) { string = string == null ? '' : '' + string; return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; }; } // Internal list of HTML entities for escaping. var escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' }; // Function for escaping strings to HTML interpolation. var _escape = createEscaper(escapeMap); // Internal list of HTML entities for unescaping. var unescapeMap = invert(escapeMap); // Function for unescaping strings from HTML interpolation. var _unescape = createEscaper(unescapeMap); // By default, Underscore uses ERB-style template delimiters. Change the // following template settings to use alternative delimiters. var templateSettings = _.templateSettings = { evaluate: /<%([\s\S]+?)%>/g, interpolate: /<%=([\s\S]+?)%>/g, escape: /<%-([\s\S]+?)%>/g }; // When customizing `_.templateSettings`, if you don't want to define an // interpolation, evaluation or escaping regex, we need one that is // guaranteed not to match. var noMatch = /(.)^/; // Certain characters need to be escaped so that they can be put into a // string literal. var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; function escapeChar(match) { return '\\' + escapes[match]; } // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. // NB: `oldSettings` only exists for backwards compatibility. function template(text, settings, oldSettings) { if (!settings && oldSettings) settings = oldSettings; settings = defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. var matcher = RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); // Compile the template source, escaping string literals appropriately. var index = 0; var source = "__p+='"; text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset).replace(escapeRegExp, escapeChar); index = offset + match.length; if (escape) { source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } else if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } else if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } // Adobe VMs need the match returned to produce the correct offset. return match; }); source += "';\n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + 'return __p;\n'; var render; try { render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } var template = function(data) { return render.call(this, data, _); }; // Provide the compiled source as a convenience for precompilation. var argument = settings.variable || 'obj'; template.source = 'function(' + argument + '){\n' + source + '}'; return template; } // Traverses the children of `obj` along `path`. If a child is a function, it // is invoked with its parent as context. Returns the value of the final // child, or `fallback` if any child is undefined. function result(obj, path, fallback) { path = toPath$1(path); var length = path.length; if (!length) { return isFunction$1(fallback) ? fallback.call(obj) : fallback; } for (var i = 0; i < length; i++) { var prop = obj == null ? void 0 : obj[path[i]]; if (prop === void 0) { prop = fallback; i = length; // Ensure we don't continue iterating. } obj = isFunction$1(prop) ? prop.call(obj) : prop; } return obj; } // Generate a unique integer id (unique within the entire client session). // Useful for temporary DOM ids. var idCounter = 0; function uniqueId(prefix) { var id = ++idCounter + ''; return prefix ? prefix + id : id; } // Start chaining a wrapped Underscore object. function chain(obj) { var instance = _(obj); instance._chain = true; return instance; } // Internal function to execute `sourceFunc` bound to `context` with optional // `args`. Determines whether to execute a function as a constructor or as a // normal function. function executeBound(sourceFunc, boundFunc, context, callingContext, args) { if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); var self = baseCreate(sourceFunc.prototype); var result = sourceFunc.apply(self, args); if (isObject(result)) return result; return self; } // Partially apply a function by creating a version that has had some of its // arguments pre-filled, without changing its dynamic `this` context. `_` acts // as a placeholder by default, allowing any combination of arguments to be // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. var partial = restArguments(function(func, boundArgs) { var placeholder = partial.placeholder; var bound = function() { var position = 0, length = boundArgs.length; var args = Array(length); for (var i = 0; i < length; i++) { args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; } while (position < arguments.length) args.push(arguments[position++]); return executeBound(func, bound, this, this, args); }; return bound; }); partial.placeholder = _; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). var bind = restArguments(function(func, context, args) { if (!isFunction$1(func)) throw new TypeError('Bind must be called on a function'); var bound = restArguments(function(callArgs) { return executeBound(func, bound, context, this, args.concat(callArgs)); }); return bound; }); // Internal helper for collection methods to determine whether a collection // should be iterated as an array or as an object. // Related: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var isArrayLike = createSizePropertyCheck(getLength); // Internal implementation of a recursive `flatten` function. function flatten(input, depth, strict, output) { output = output || []; if (!depth && depth !== 0) { depth = Infinity; } else if (depth <= 0) { return output.concat(input); } var idx = output.length; for (var i = 0, length = getLength(input); i < length; i++) { var value = input[i]; if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { // Flatten current level of array or arguments object. if (depth > 1) { flatten(value, depth - 1, strict, output); idx = output.length; } else { var j = 0, len = value.length; while (j < len) output[idx++] = value[j++]; } } else if (!strict) { output[idx++] = value; } } return output; } // Bind a number of an object's methods to that object. Remaining arguments // are the method names to be bound. Useful for ensuring that all callbacks // defined on an object belong to it. var bindAll = restArguments(function(obj, keys) { keys = flatten(keys, false, false); var index = keys.length; if (index < 1) throw new Error('bindAll must be passed function names'); while (index--) { var key = keys[index]; obj[key] = bind(obj[key], obj); } return obj; }); // Memoize an expensive function by storing its results. function memoize(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!has(cache, address)) cache[address] = func.apply(this, arguments); return cache[address]; }; memoize.cache = {}; return memoize; } // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. var delay = restArguments(function(func, wait, args) { return setTimeout(function() { return func.apply(null, args); }, wait); }); // Defers a function, scheduling it to run after the current call stack has // cleared. var defer = partial(delay, _, 1); // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `{leading: false}`. To disable execution on the trailing edge, ditto. function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var _now = now(); if (!previous && options.leading === false) previous = _now; var remaining = wait - (_now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = _now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; } // When a sequence of calls of the returned function ends, the argument // function is triggered. The end of a sequence is defined by the `wait` // parameter. If `immediate` is passed, the argument function will be // triggered at the beginning of the sequence instead of at the end. function debounce(func, wait, immediate) { var timeout, result; var later = function(context, args) { timeout = null; if (args) result = func.apply(context, args); }; var debounced = restArguments(function(args) { if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(later, wait); if (callNow) result = func.apply(this, args); } else { timeout = delay(later, wait, this, args); } return result; }); debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; } // Returns the first function passed as an argument to the second, // allowing you to adjust arguments, run code before and after, and // conditionally execute the original function. function wrap(func, wrapper) { return partial(wrapper, func); } // Returns a negated version of the passed-in predicate. function negate(predicate) { return function() { return !predicate.apply(this, arguments); }; } // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. function compose() { var args = arguments; var start = args.length - 1; return function() { var i = start; var result = args[start].apply(this, arguments); while (i--) result = args[i].call(this, result); return result; }; } // Returns a function that will only be executed on and after the Nth call. function after(times, func) { return function() { if (--times < 1) { return func.apply(this, arguments); } }; } // Returns a function that will only be executed up to (but not including) the // Nth call. function before(times, func) { var memo; return function() { if (--times > 0) { memo = func.apply(this, arguments); } if (times <= 1) func = null; return memo; }; } // Returns a function that will be executed at most one time, no matter how // often you call it. Useful for lazy initialization. var once = partial(before, 2); // Returns the first key on an object that passes a truth test. function findKey(obj, predicate, context) { predicate = cb(predicate, context); var _keys = keys(obj), key; for (var i = 0, length = _keys.length; i < length; i++) { key = _keys[i]; if (predicate(obj[key], key, obj)) return key; } } // Internal function to generate `_.findIndex` and `_.findLastIndex`. function createPredicateIndexFinder(dir) { return function(array, predicate, context) { predicate = cb(predicate, context); var length = getLength(array); var index = dir > 0 ? 0 : length - 1; for (; index >= 0 && index < length; index += dir) { if (predicate(array[index], index, array)) return index; } return -1; }; } // Returns the first index on an array-like that passes a truth test. var findIndex = createPredicateIndexFinder(1); // Returns the last index on an array-like that passes a truth test. var findLastIndex = createPredicateIndexFinder(-1); // Use a comparator function to figure out the smallest index at which // an object should be inserted so as to maintain order. Uses binary search. function sortedIndex(array, obj, iteratee, context) { iteratee = cb(iteratee, context, 1); var value = iteratee(obj); var low = 0, high = getLength(array); while (low < high) { var mid = Math.floor((low + high) / 2); if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; } return low; } // Internal function to generate the `_.indexOf` and `_.lastIndexOf` functions. function createIndexFinder(dir, predicateFind, sortedIndex) { return function(array, item, idx) { var i = 0, length = getLength(array); if (typeof idx == 'number') { if (dir > 0) { i = idx >= 0 ? idx : Math.max(idx + length, i); } else { length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; } } else if (sortedIndex && idx && length) { idx = sortedIndex(array, item); return array[idx] === item ? idx : -1; } if (item !== item) { idx = predicateFind(slice.call(array, i, length), isNaN$1); return idx >= 0 ? idx + i : -1; } for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { if (array[idx] === item) return idx; } return -1; }; } // Return the position of the first occurrence of an item in an array, // or -1 if the item is not included in the array. // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. var indexOf = createIndexFinder(1, findIndex, sortedIndex); // Return the position of the last occurrence of an item in an array, // or -1 if the item is not included in the array. var lastIndexOf = createIndexFinder(-1, findLastIndex); // Return the first value which passes a truth test. function find(obj, predicate, context) { var keyFinder = isArrayLike(obj) ? findIndex : findKey; var key = keyFinder(obj, predicate, context); if (key !== void 0 && key !== -1) return obj[key]; } // Convenience version of a common use case of `_.find`: getting the first // object containing specific `key:value` pairs. function findWhere(obj, attrs) { return find(obj, matcher(attrs)); } // The cornerstone for collection functions, an `each` // implementation, aka `forEach`. // Handles raw objects in addition to array-likes. Treats all // sparse array-likes as if they were dense. function each(obj, iteratee, context) { iteratee = optimizeCb(iteratee, context); var i, length; if (isArrayLike(obj)) { for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj); } } else { var _keys = keys(obj); for (i = 0, length = _keys.length; i < length; i++) { iteratee(obj[_keys[i]], _keys[i], obj); } } return obj; } // Return the results of applying the iteratee to each element. function map(obj, iteratee, context) { iteratee = cb(iteratee, context); var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; } // Internal helper to create a reducing function, iterating left or right. function createReduce(dir) { // Wrap code that reassigns argument variables in a separate function than // the one that accesses `arguments.length` to avoid a perf hit. (#1991) var reducer = function(obj, iteratee, memo, initial) { var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length, index = dir > 0 ? 0 : length - 1; if (!initial) { memo = obj[_keys ? _keys[index] : index]; index += dir; } for (; index >= 0 && index < length; index += dir) { var currentKey = _keys ? _keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; }; return function(obj, iteratee, memo, context) { var initial = arguments.length >= 3; return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); }; } // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. var reduce = createReduce(1); // The right-associative version of reduce, also known as `foldr`. var reduceRight = createReduce(-1); // Return all the elements that pass a truth test. function filter(obj, predicate, context) { var results = []; predicate = cb(predicate, context); each(obj, function(value, index, list) { if (predicate(value, index, list)) results.push(value); }); return results; } // Return all the elements for which a truth test fails. function reject(obj, predicate, context) { return filter(obj, negate(cb(predicate)), context); } // Determine whether all of the elements pass a truth test. function every(obj, predicate, context) { predicate = cb(predicate, context); var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; if (!predicate(obj[currentKey], currentKey, obj)) return false; } return true; } // Determine if at least one element in the object passes a truth test. function some(obj, predicate, context) { predicate = cb(predicate, context); var _keys = !isArrayLike(obj) && keys(obj), length = (_keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = _keys ? _keys[index] : index; if (predicate(obj[currentKey], currentKey, obj)) return true; } return false; } // Determine if the array or object contains a given item (using `===`). function contains(obj, item, fromIndex, guard) { if (!isArrayLike(obj)) obj = values(obj); if (typeof fromIndex != 'number' || guard) fromIndex = 0; return indexOf(obj, item, fromIndex) >= 0; } // Invoke a method (with arguments) on every item in a collection. var invoke = restArguments(function(obj, path, args) { var contextPath, func; if (isFunction$1(path)) { func = path; } else { path = toPath$1(path); contextPath = path.slice(0, -1); path = path[path.length - 1]; } return map(obj, function(context) { var method = func; if (!method) { if (contextPath && contextPath.length) { context = deepGet(context, contextPath); } if (context == null) return void 0; method = context[path]; } return method == null ? method : method.apply(context, args); }); }); // Convenience version of a common use case of `_.map`: fetching a property. function pluck(obj, key) { return map(obj, property(key)); } // Convenience version of a common use case of `_.filter`: selecting only // objects containing specific `key:value` pairs. function where(obj, attrs) { return filter(obj, matcher(attrs)); } // Return the maximum element (or element-based computation). function max(obj, iteratee, context) { var result = -Infinity, lastComputed = -Infinity, value, computed; if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { obj = isArrayLike(obj) ? obj : values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value != null && value > result) { result = value; } } } else { iteratee = cb(iteratee, context); each(obj, function(v, index, list) { computed = iteratee(v, index, list); if (computed > lastComputed || computed === -Infinity && result === -Infinity) { result = v; lastComputed = computed; } }); } return result; } // Return the minimum element (or element-based computation). function min(obj, iteratee, context) { var result = Infinity, lastComputed = Infinity, value, computed; if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { obj = isArrayLike(obj) ? obj : values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value != null && value < result) { result = value; } } } else { iteratee = cb(iteratee, context); each(obj, function(v, index, list) { computed = iteratee(v, index, list); if (computed < lastComputed || computed === Infinity && result === Infinity) { result = v; lastComputed = computed; } }); } return result; } // Sample **n** random values from a collection using the modern version of the // [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle). // If **n** is not specified, returns a single random element. // The internal `guard` argument allows it to work with `_.map`. function sample(obj, n, guard) { if (n == null || guard) { if (!isArrayLike(obj)) obj = values(obj); return obj[random(obj.length - 1)]; } var sample = isArrayLike(obj) ? clone(obj) : values(obj); var length = getLength(sample); n = Math.max(Math.min(n, length), 0); var last = length - 1; for (var index = 0; index < n; index++) { var rand = random(index, last); var temp = sample[index]; sample[index] = sample[rand]; sample[rand] = temp; } return sample.slice(0, n); } // Shuffle a collection. function shuffle(obj) { return sample(obj, Infinity); } // Sort the object's values by a criterion produced by an iteratee. function sortBy(obj, iteratee, context) { var index = 0; iteratee = cb(iteratee, context); return pluck(map(obj, function(value, key, list) { return { value: value, index: index++, criteria: iteratee(value, key, list) }; }).sort(function(left, right) { var a = left.criteria; var b = right.criteria; if (a !== b) { if (a > b || a === void 0) return 1; if (a < b || b === void 0) return -1; } return left.index - right.index; }), 'value'); } // An internal function used for aggregate "group by" operations. function group(behavior, partition) { return function(obj, iteratee, context) { var result = partition ? [[], []] : {}; iteratee = cb(iteratee, context); each(obj, function(value, index) { var key = iteratee(value, index, obj); behavior(result, value, key); }); return result; }; } // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. var groupBy = group(function(result, value, key) { if (has(result, key)) result[key].push(value); else result[key] = [value]; }); // Indexes the object's values by a criterion, similar to `_.groupBy`, but for // when you know that your index values will be unique. var indexBy = group(function(result, value, key) { result[key] = value; }); // Counts instances of an object that group by a certain criterion. Pass // either a string attribute to count by, or a function that returns the // criterion. var countBy = group(function(result, value, key) { if (has(result, key)) result[key]++; else result[key] = 1; }); // Split a collection into two arrays: one whose elements all pass the given // truth test, and one whose elements all do not pass the truth test. var partition = group(function(result, value, pass) { result[pass ? 0 : 1].push(value); }, true); // Safely create a real, live array from anything iterable. var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; function toArray(obj) { if (!obj) return []; if (isArray(obj)) return slice.call(obj); if (isString(obj)) { // Keep surrogate pair characters together. return obj.match(reStrSymbol); } if (isArrayLike(obj)) return map(obj, identity); return values(obj); } // Return the number of elements in a collection. function size(obj) { if (obj == null) return 0; return isArrayLike(obj) ? obj.length : keys(obj).length; } // Internal `_.pick` helper function to determine whether `key` is an enumerable // property name of `obj`. function keyInObj(value, key, obj) { return key in obj; } // Return a copy of the object only containing the allowed properties. var pick = restArguments(function(obj, keys) { var result = {}, iteratee = keys[0]; if (obj == null) return result; if (isFunction$1(iteratee)) { if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); keys = allKeys(obj); } else { iteratee = keyInObj; keys = flatten(keys, false, false); obj = Object(obj); } for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; var value = obj[key]; if (iteratee(value, key, obj)) result[key] = value; } return result; }); // Return a copy of the object without the disallowed properties. var omit = restArguments(function(obj, keys) { var iteratee = keys[0], context; if (isFunction$1(iteratee)) { iteratee = negate(iteratee); if (keys.length > 1) context = keys[1]; } else { keys = map(flatten(keys, false, false), String); iteratee = function(value, key) { return !contains(keys, key); }; } return pick(obj, iteratee, context); }); // Returns everything but the last entry of the array. Especially useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. function initial(array, n, guard) { return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); } // Get the first element of an array. Passing **n** will return the first N // values in the array. The **guard** check allows it to work with `_.map`. function first(array, n, guard) { if (array == null || array.length < 1) return n == null || guard ? void 0 : []; if (n == null || guard) return array[0]; return initial(array, array.length - n); } // Returns everything but the first entry of the `array`. Especially useful on // the `arguments` object. Passing an **n** will return the rest N values in the // `array`. function rest(array, n, guard) { return slice.call(array, n == null || guard ? 1 : n); } // Get the last element of an array. Passing **n** will return the last N // values in the array. function last(array, n, guard) { if (array == null || array.length < 1) return n == null || guard ? void 0 : []; if (n == null || guard) return array[array.length - 1]; return rest(array, Math.max(0, array.length - n)); } // Trim out all falsy values from an array. function compact(array) { return filter(array, Boolean); } // Flatten out an array, either recursively (by default), or up to `depth`. // Passing `true` or `false` as `depth` means `1` or `Infinity`, respectively. function flatten$1(array, depth) { return flatten(array, depth, false); } // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. var difference = restArguments(function(array, rest) { rest = flatten(rest, true, true); return filter(array, function(value){ return !contains(rest, value); }); }); // Return a version of the array that does not contain the specified value(s). var without = restArguments(function(array, otherArrays) { return difference(array, otherArrays); }); // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // The faster algorithm will not work with an iteratee if the iteratee // is not a one-to-one function, so providing an iteratee will disable // the faster algorithm. function uniq(array, isSorted, iteratee, context) { if (!isBoolean(isSorted)) { context = iteratee; iteratee = isSorted; isSorted = false; } if (iteratee != null) iteratee = cb(iteratee, context); var result = []; var seen = []; for (var i = 0, length = getLength(array); i < length; i++) { var value = array[i], computed = iteratee ? iteratee(value, i, array) : value; if (isSorted && !iteratee) { if (!i || seen !== computed) result.push(value); seen = computed; } else if (iteratee) { if (!contains(seen, computed)) { seen.push(computed); result.push(value); } } else if (!contains(result, value)) { result.push(value); } } return result; } // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. var union = restArguments(function(arrays) { return uniq(flatten(arrays, true, true)); }); // Produce an array that contains every item shared between all the // passed-in arrays. function intersection(array) { var result = []; var argsLength = arguments.length; for (var i = 0, length = getLength(array); i < length; i++) { var item = array[i]; if (contains(result, item)) continue; var j; for (j = 1; j < argsLength; j++) { if (!contains(arguments[j], item)) break; } if (j === argsLength) result.push(item); } return result; } // Complement of zip. Unzip accepts an array of arrays and groups // each array's elements on shared indices. function unzip(array) { var length = array && max(array, getLength).length || 0; var result = Array(length); for (var index = 0; index < length; index++) { result[index] = pluck(array, index); } return result; } // Zip together multiple lists into a single array -- elements that share // an index go together. var zip = restArguments(unzip); // Converts lists into objects. Pass either a single array of `[key, value]` // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. Passing by pairs is the reverse of `_.pairs`. function object(list, values) { var result = {}; for (var i = 0, length = getLength(list); i < length; i++) { if (values) { result[list[i]] = values[i]; } else { result[list[i][0]] = list[i][1]; } } return result; } // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](https://docs.python.org/library/functions.html#range). function range(start, stop, step) { if (stop == null) { stop = start || 0; start = 0; } if (!step) { step = stop < start ? -1 : 1; } var length = Math.max(Math.ceil((stop - start) / step), 0); var range = Array(length); for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start; } return range; } // Chunk a single array into multiple arrays, each containing `count` or fewer // items. function chunk(array, count) { if (count == null || count < 1) return []; var result = []; var i = 0, length = array.length; while (i < length) { result.push(slice.call(array, i, i += count)); } return result; } // Helper function to continue chaining intermediate results. function chainResult(instance, obj) { return instance._chain ? _(obj).chain() : obj; } // Add your own custom functions to the Underscore object. function mixin(obj) { each(functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); return chainResult(this, func.apply(_, args)); }; }); return _; } // Add all mutator `Array` functions to the wrapper. each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; if (obj != null) { method.apply(obj, arguments); if ((name === 'shift' || name === 'splice') && obj.length === 0) { delete obj[0]; } } return chainResult(this, obj); }; }); // Add all accessor `Array` functions to the wrapper. each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; if (obj != null) obj = method.apply(obj, arguments); return chainResult(this, obj); }; }); // Named Exports var allExports = { __proto__: null, VERSION: VERSION, restArguments: restArguments, isObject: isObject, isNull: isNull, isUndefined: isUndefined, isBoolean: isBoolean, isElement: isElement, isString: isString, isNumber: isNumber, isDate: isDate, isRegExp: isRegExp, isError: isError, isSymbol: isSymbol, isArrayBuffer: isArrayBuffer, isDataView: isDataView$1, isArray: isArray, isFunction: isFunction$1, isArguments: isArguments$1, isFinite: isFinite$1, isNaN: isNaN$1, isTypedArray: isTypedArray$1, isEmpty: isEmpty, isMatch: isMatch, isEqual: isEqual, isMap: isMap, isWeakMap: isWeakMap, isSet: isSet, isWeakSet: isWeakSet, keys: keys, allKeys: allKeys, values: values, pairs: pairs, invert: invert, functions: functions, methods: functions, extend: extend, extendOwn: extendOwn, assign: extendOwn, defaults: defaults, create: create, clone: clone, tap: tap, get: get, has: has$1, mapObject: mapObject, identity: identity, constant: constant, noop: noop, toPath: toPath, property: property, propertyOf: propertyOf, matcher: matcher, matches: matcher, times: times, random: random, now: now, escape: _escape, unescape: _unescape, templateSettings: templateSettings, template: template, result: result, uniqueId: uniqueId, chain: chain, iteratee: iteratee, partial: partial, bind: bind, bindAll: bindAll, memoize: memoize, delay: delay, defer: defer, throttle: throttle, debounce: debounce, wrap: wrap, negate: negate, compose: compose, after: after, before: before, once: once, findKey: findKey, findIndex: findIndex, findLastIndex: findLastIndex, sortedIndex: sortedIndex, indexOf: indexOf, lastIndexOf: lastIndexOf, find: find, detect: find, findWhere: findWhere, each: each, forEach: each, map: map, collect: map, reduce: reduce, foldl: reduce, inject: reduce, reduceRight: reduceRight, foldr: reduceRight, filter: filter, select: filter, reject: reject, every: every, all: every, some: some, any: some, contains: contains, includes: contains, include: contains, invoke: invoke, pluck: pluck, where: where, max: max, min: min, shuffle: shuffle, sample: sample, sortBy: sortBy, groupBy: groupBy, indexBy: indexBy, countBy: countBy, partition: partition, toArray: toArray, size: size, pick: pick, omit: omit, first: first, head: first, take: first, initial: initial, last: last, rest: rest, tail: rest, drop: rest, compact: compact, flatten: flatten$1, without: without, uniq: uniq, unique: uniq, union: union, intersection: intersection, difference: difference, unzip: unzip, transpose: unzip, zip: zip, object: object, range: range, chunk: chunk, mixin: mixin, 'default': _ }; // Default Export // Add all of the Underscore functions to the wrapper object. var _$1 = mixin(allExports); // Legacy Node.js API. _$1._ = _$1; return _$1; }))); }).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],3:[function(require,module,exports){ require('jsonform'); },{"jsonform":1}]},{},[3]);