UNPKG

jsonform

Version:

Client-side JavaScript library that generates HTML forms from structured data models expressed using a JSON schema, possibly completed by a form layout description.

1,213 lines (1,147 loc) 137 kB
/* Copyright (c) 2012 Joshfire - MIT license */ /** * @fileoverview Core of the JSON Form client-side library. * * Generates an HTML form from a structured data model and a layout description. * * The library may also validate inputs entered by the user against the data model * upon form submission and create the structured data object initialized with the * values that were submitted. * * The library depends on: * - jQuery * - the underscore library * - a JSON parser/serializer. Nothing to worry about in modern browsers. * - the JSONFormValidation library (in jsv.js) for validation purpose * * See documentation at: * http://developer.joshfire.com/doc/dev/ref/jsonform * * The library creates and maintains an internal data tree along with the DOM. * That structure is necessary to handle arrays (and nested arrays!) that are * dynamic by essence. */ /*global window*/ (function(serverside, global, $, _, JSON) { if (serverside && !_) { _ = require('underscore'); } /** * Regular expressions used to extract array indexes in input field names */ var reArray = /\[([0-9]*)\](?=\[|\.|$)/g; /** * Template settings for form views */ var fieldTemplateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /<%=([\s\S]+?)%>/g }; /** * Template settings for value replacement */ var valueTemplateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; /** * Returns true if given value is neither "undefined" nor null */ var isSet = function (value) { return !(_.isUndefined(value) || _.isNull(value)); }; /** * Returns true if given property is directly property of an object */ var hasOwnProperty = function (obj, prop) { return typeof obj === 'object' && obj.hasOwnProperty(prop); } /** * The jsonform object whose methods will be exposed to the window object */ var jsonform = {util:{}}; // From backbonejs var escapeHTML = function (string) { if (!isSet(string)) { return ''; } string = '' + string; if (!string) { return ''; } return string .replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#x27;') .replace(/\//g, '&#x2F;'); }; /** * Escapes selector name for use with jQuery * * All meta-characters listed in jQuery doc are escaped: * http://api.jquery.com/category/selectors/ * * @function * @param {String} selector The jQuery selector to escape * @return {String} The escaped selector. */ var escapeSelector = function (selector) { return selector.replace(/([ \!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '\\$1'); }; /** * * Slugifies a string by replacing spaces with _. Used to create * valid classnames and ids for the form. * * @function * @param {String} str The string to slugify * @return {String} The slugified string. */ var slugify = function(str) { return str.replace(/\ /g, '_'); } /** * Initializes tabular sections in forms. Such sections are generated by the * 'selectfieldset' type of elements in JSON Form. * * Input fields that are not visible are automatically disabled * not to appear in the submitted form. That's on purpose, as tabs * are meant to convey an alternative (and not a sequence of steps). * * The tabs menu is not rendered as tabs but rather as a select field because * it's easier to grasp that it's an alternative. * * Code based on bootstrap-tabs.js, updated to: * - react to option selection instead of tab click * - disable input fields in non visible tabs * - disable the possibility to have dropdown menus (no meaning here) * - act as a regular function instead of as a jQuery plug-in. * * @function * @param {Object} tabs jQuery object that contains the tabular sections * to initialize. The object may reference more than one element. */ var initializeTabs = function (tabs) { var activate = function (element, container) { container .find('> .active') .removeClass('active'); element.addClass('active'); }; var enableFields = function ($target, targetIndex) { // Enable all fields in the targeted tab $target.find('input, textarea, select').removeAttr('disabled'); // Disable all fields in other tabs $target.parent() .children(':not([data-idx=' + targetIndex + '])') .find('input, textarea, select') .attr('disabled', 'disabled'); }; var optionSelected = function (e) { var $option = $("option:selected", $(this)), $select = $(this), // do not use .attr() as it sometimes unexplicably fails targetIdx = $option.get(0).getAttribute('data-idx') || $option.attr('value'), $target; e.preventDefault(); if ($option.hasClass('active')) { return; } $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']'); activate($option, $select); activate($target, $target.parent()); enableFields($target, targetIdx); }; var tabClicked = function (e) { var $a = $('a', $(this)); var $content = $(this).parents('.tabbable').first() .find('.tab-content').first(); var targetIdx = $(this).index(); // The `>` here is to prevent activating selectfieldsets inside a tabarray var $target = $content.find('> [data-idx=' + targetIdx + ']'); e.preventDefault(); activate($(this), $(this).parent()); activate($target, $target.parent()); if ($(this).parent().hasClass('jsonform-alternative')) { enableFields($target, targetIdx); } }; tabs.each(function () { $(this).delegate('select.nav', 'change', optionSelected); $(this).find('select.nav').each(function () { $(this).val($(this).find('.active').attr('value')); // do not use .attr() as it sometimes unexplicably fails var targetIdx = $(this).find('option:selected').get(0).getAttribute('data-idx') || $(this).find('option:selected').attr('value'); var $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']'); enableFields($target, targetIdx); }); $(this).delegate('ul.nav li', 'click', tabClicked); $(this).find('ul.nav li.active').click(); }); }; // Twitter bootstrap-friendly HTML boilerplate for standard inputs jsonform.fieldTemplate = function(inner) { return '<div ' + '<% for(var key in elt.htmlMetaData) {%>' + '<%= key %>="<%= elt.htmlMetaData[key] %>" ' + '<% }%>' + 'class="form-group jsonform-error-<%= keydash %>' + '<%= elt.htmlClass ? " " + elt.htmlClass : "" %>' + '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " jsonform-required" : "") %>' + '<%= (node.readOnly ? " jsonform-readonly" : "") %>' + '<%= (node.disabled ? " jsonform-disabled" : "") %>' + '">' + '<% if (!elt.notitle) { %>' + '<label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label>' + '<% } %>' + '<div class="controls">' + '<% if (node.prepend || node.append) { %>' + '<div class="<% if (node.prepend) { %>input-group<% } %>' + '<% if (node.append) { %> input-group<% } %>">' + '<% if (node.prepend) { %>' + '<span class="input-group-addon"><%= node.prepend %></span>' + '<% } %>' + '<% } %>' + inner + '<% if (node.append) { %>' + '<span class="input-group-addon"><%= node.append %></span>' + '<% } %>' + '<% if (node.prepend || node.append) { %>' + '</div>' + '<% } %>' + '<% if (node.description) { %>' + '<span class="help-block"><%= node.description %></span>' + '<% } %>' + '<span class="help-block jsonform-errortext" style="display:none;"></span>' + '</div></div>'; }; var fileDisplayTemplate = '<div class="_jsonform-preview">' + '<% if (value.type=="image") { %>' + '<img class="jsonform-preview" id="jsonformpreview-<%= id %>" src="<%= value.url %>" />' + '<% } else { %>' + '<a href="<%= value.url %>"><%= value.name %></a> (<%= Math.ceil(value.size/1024) %>kB)' + '<% } %>' + '</div>' + '<a href="#" class="btn btn-default _jsonform-delete"><i class="glyphicon glyphicon-remove" title="Remove"></i></a> '; var inputFieldTemplate = function (type) { return { 'template': '<input type="' + type + '" ' + 'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' + 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' + ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' + '<%= (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.minLength ? " minlength=\'" + node.schemaElement.minLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' + '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' + ' />', 'fieldtemplate': true, 'inputfield': true } }; jsonform.elementTypes = { 'none': { 'template': '' }, 'root': { 'template': '<div><%= children %></div>' }, 'text': inputFieldTemplate('text'), 'password': inputFieldTemplate('password'), 'date': inputFieldTemplate('date'), 'datetime': inputFieldTemplate('datetime'), 'datetime-local': inputFieldTemplate('datetime-local'), 'email': inputFieldTemplate('email'), 'month': inputFieldTemplate('month'), 'number': inputFieldTemplate('number'), 'search': inputFieldTemplate('search'), 'tel': inputFieldTemplate('tel'), 'time': inputFieldTemplate('time'), 'url': inputFieldTemplate('url'), 'week': inputFieldTemplate('week'), 'range': { 'template': '<div class="range"><input type="range" ' + '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' + 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' + ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' + '<%= (node.disabled? " disabled" : "")%>' + ' min=<%= range.min %>' + ' max=<%= range.max %>' + ' step=<%= range.step %>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + ' /><% if (range.indicator) { %><span class="range-value" rel="<%= id %>"><%= escape(value) %></span><% } %></div>', 'fieldtemplate': true, 'inputfield': true, 'onInput': function(evt, elt) { const valueIndicator = document.querySelector('span.range-value[rel="' + elt.id + '"]'); if (valueIndicator) { valueIndicator.innerText = evt.target.value; } }, 'onBeforeRender': function (data, node) { data.range = { min: 1, max: 100, step: 1, indicator: false }; if (!node || !node.schemaElement) return; if (node.formElement && node.formElement.step) { data.range.step = node.formElement.step; } if (node.formElement && node.formElement.indicator) { data.range.indicator = node.formElement.indicator; } if (typeof node.schemaElement.minimum !== 'undefined') { if (node.schemaElement.exclusiveMinimum) { data.range.min = node.schemaElement.minimum + data.range.step; } else { data.range.min = node.schemaElement.minimum; } } if (typeof node.schemaElement.maximum !== 'undefined') { if (node.schemaElement.exclusiveMaximum) { data.range.max = node.schemaElement.maximum - data.range.step; } else { data.range.max = node.schemaElement.maximum; } } } }, 'color':{ 'template':'<input type="text" ' + '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' + 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' + ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + ' />', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function(evt, node) { $(node.el).find('#' + escapeSelector(node.id)).spectrum({ preferredFormat: "hex", showInput: true }); } }, 'textarea':{ 'template':'<textarea id="<%= id %>" name="<%= node.name %>" ' + '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' + 'style="height:<%= elt.height || "150px" %>;width:<%= elt.width || "100%" %>;"' + ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.minLength ? " minlength=\'" + node.schemaElement.minLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' + '><%= value %></textarea>', 'fieldtemplate': true, 'inputfield': true }, 'wysihtml5':{ 'template':'<textarea id="<%= id %>" name="<%= node.name %>" style="height:<%= elt.height || "300px" %>;width:<%= elt.width || "100%" %>;"' + ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' + '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.minLength ? " minlength=\'" + node.schemaElement.minLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' + '><%= value %></textarea>', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function (evt, node) { var setup = function () { //protect from double init if ($(node.el).data("wysihtml5")) return; $(node.el).data("wysihtml5_loaded",true); $(node.el).find('#' + escapeSelector(node.id)).wysihtml5({ "html": true, "link": true, "font-styles":true, "image": false, "events": { "load": function () { // In chrome, if an element is required and hidden, it leads to // the error 'An invalid form control with name='' is not focusable' // See http://stackoverflow.com/questions/7168645/invalid-form-control-only-in-google-chrome $(this.textareaElement).removeAttr('required'); } } }); }; // Is there a setup hook? if (window.jsonform_wysihtml5_setup) { window.jsonform_wysihtml5_setup(setup); return; } // Wait until wysihtml5 is loaded var itv = window.setInterval(function() { if (window.wysihtml5) { window.clearInterval(itv); setup(); } },1000); } }, 'ace':{ 'template':'<div id="<%= id %>" style="position:relative;height:<%= elt.height || "300px" %>;"><div id="<%= id %>__ace" style="width:<%= elt.width || "100%" %>;height:<%= elt.height || "300px" %>;"></div><input type="hidden" name="<%= node.name %>" id="<%= id %>__hidden" value="<%= escape(value) %>"/></div>', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function (evt, node) { var setup = function () { var formElement = node.formElement || {}; var ace = window.ace; var editor = ace.edit($(node.el).find('#' + escapeSelector(node.id) + '__ace').get(0)); var idSelector = '#' + escapeSelector(node.id) + '__hidden'; // Force editor to use "\n" for new lines, not to bump into ACE "\r" conversion issue // (ACE is ok with "\r" on pasting but fails to return "\r" when value is extracted) editor.getSession().setNewLineMode('unix'); editor.renderer.setShowPrintMargin(false); editor.setTheme("ace/theme/"+(formElement.aceTheme||"twilight")); if (formElement.aceMode) { editor.getSession().setMode("ace/mode/"+formElement.aceMode); } editor.getSession().setTabSize(2); // Set the contents of the initial manifest file editor.getSession().setValue(node.value||""); //TODO: this is clearly sub-optimal // 'Lazily' bind to the onchange 'ace' event to give // priority to user edits var lazyChanged = _.debounce(function () { $(node.el).find(idSelector).val(editor.getSession().getValue()); $(node.el).find(idSelector).change(); }, 600); editor.getSession().on('change', lazyChanged); editor.on('blur', function() { $(node.el).find(idSelector).change(); $(node.el).find(idSelector).trigger("blur"); }); editor.on('focus', function() { $(node.el).find(idSelector).trigger("focus"); }); }; // Is there a setup hook? if (window.jsonform_ace_setup) { window.jsonform_ace_setup(setup); return; } // Wait until ACE is loaded var itv = window.setInterval(function() { if (window.ace) { window.clearInterval(itv); setup(); } },1000); } }, 'checkbox':{ 'template': '<div class="checkbox"><label><input type="checkbox" id="<%= id %>" ' + '<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %>' + 'name="<%= node.name %>" value="1" <% if (value) {%>checked<% } %>' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' + ' /><%= node.inlinetitle || "" %>' + '</label></div>', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); } }, 'file':{ 'template':'<input class="input-file" id="<%= id %>" name="<%= node.name %>" type="file" ' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + '<%= (node.formElement && node.formElement.accept ? (" accept=\'" + node.formElement.accept + "\'") : "") %>' + '/>', 'fieldtemplate': true, 'inputfield': true }, 'file-hosted-public':{ 'template':'<span><% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %><input class="input-file" id="_transloadit_<%= id %>" type="file" name="<%= transloaditname %>" /><input data-transloadit-name="_transloadit_<%= transloaditname %>" type="hidden" id="<%= id %>" name="<%= node.name %>" value=\'<%= escape(JSON.stringify(node.value)) %>\' /></span>', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); }, 'onBeforeRender': function (data, node) { if (!node.ownerTree._transloadit_generic_public_index) { node.ownerTree._transloadit_generic_public_index=1; } else { node.ownerTree._transloadit_generic_public_index++; } data.transloaditname = "_transloadit_jsonform_genericupload_public_"+node.ownerTree._transloadit_generic_public_index; if (!node.ownerTree._transloadit_generic_elts) node.ownerTree._transloadit_generic_elts = {}; node.ownerTree._transloadit_generic_elts[data.transloaditname] = node; }, 'onChange': function(evt,elt) { // The "transloadit" function should be called only once to enable // the service when the form is submitted. Has it already been done? if (elt.ownerTree._transloadit_bound) { return false; } elt.ownerTree._transloadit_bound = true; // Call the "transloadit" function on the form element var formElt = $(elt.ownerTree.domRoot); formElt.transloadit({ autoSubmit: false, wait: true, onSuccess: function (assembly) { // Image has been uploaded. Check the "results" property that // contains the list of files that Transloadit produced. There // should be one image per file input in the form at most. 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 () { elt.ownerTree.submit(); }, 10); }, onError: function (assembly) { // TODO: report the error to the user console.log('assembly error', assembly); } }); }, 'onInsert': function (evt, node) { $(node.el).find('a._jsonform-delete').on('click', function (evt) { $(node.el).find('._jsonform-preview').remove(); $(node.el).find('a._jsonform-delete').remove(); $(node.el).find('#' + escapeSelector(node.id)).val(''); evt.preventDefault(); return false; }); }, 'onSubmit':function(evt, elt) { if (elt.ownerTree._transloadit_bound) { return false; } return true; } }, 'file-transloadit': { 'template': '<span><% if (value && (value.type||value.url)) { %>'+fileDisplayTemplate+'<% } %><input class="input-file" id="_transloadit_<%= id %>" type="file" name="_transloadit_<%= node.name %>" /><input type="hidden" id="<%= id %>" name="<%= node.name %>" value=\'<%= escape(JSON.stringify(node.value)) %>\' /></span>', 'fieldtemplate': true, 'inputfield': true, 'getElement': function (el) { return $(el).parent().get(0); }, 'onChange': function (evt, elt) { // The "transloadit" function should be called only once to enable // the service when the form is submitted. Has it already been done? if (elt.ownerTree._transloadit_bound) { return false; } elt.ownerTree._transloadit_bound = true; // Call the "transloadit" function on the form element var formElt = $(elt.ownerTree.domRoot); formElt.transloadit({ autoSubmit: false, wait: true, onSuccess: function (assembly) { // Image has been uploaded. Check the "results" property that // contains the list of files that Transloadit produced. Note // JSONForm only supports 1-to-1 associations, meaning it // expects the "results" property to contain only one image // per file input in the form. 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 () { elt.ownerTree.submit(); }, 10); }, onError: function (assembly) { // TODO: report the error to the user console.log('assembly error', assembly); } }); }, 'onInsert': function (evt, node) { $(node.el).find('a._jsonform-delete').on('click', function (evt) { $(node.el).find('._jsonform-preview').remove(); $(node.el).find('a._jsonform-delete').remove(); $(node.el).find('#' + escapeSelector(node.id)).val(''); evt.preventDefault(); return false; }); }, 'onSubmit': function (evt, elt) { if (elt.ownerTree._transloadit_bound) { return false; } return true; } }, 'select':{ 'template':'<select name="<%= node.name %>" id="<%= id %>"' + 'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' + '<%= (node.schemaElement && node.schemaElement.disabled? " disabled" : "")%>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + '> ' + '<% _.each(node.options, function(key, val) { if(key instanceof Object) { if (value === key.value) { %> <option selected value="<%= key.value %>"><%= key.title %></option> <% } else { %> <option value="<%= key.value %>"><%= key.title %></option> <% }} else { if (value === key) { %> <option selected value="<%= key %>"><%= key %></option> <% } else { %><option value="<%= key %>"><%= key %></option> <% }}}); %> ' + '</select>', 'fieldtemplate': true, 'inputfield': true }, 'imageselect': { 'template': '<div>' + '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' + '<div class="dropdown">' + '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } else { %> btn-default<% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' + '<% if (node.value) { %><img src="<% if (!node.value.match(/^https?:/)) { %><%= prefix %><% } %><%= node.value %><%= suffix %>" alt="" /><% } else { %><%= buttonTitle %><% } %>' + '</a>' + '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' + '<div>' + '<% _.each(node.options, function(key, idx) { if ((idx > 0) && ((idx % columns) === 0)) { %></div><div><% } %><a class="btn<% if (buttonClass) { %> <%= buttonClass %><% } else { %> btn-default<% } %>" style="max-width:<%= width %>px;max-height:<%= height %>px"><% if (key instanceof Object) { %><img src="<% if (!key.value.match(/^https?:/)) { %><%= prefix %><% } %><%= key.value %><%= suffix %>" alt="<%= key.title %>" /></a><% } else { %><img src="<% if (!key.match(/^https?:/)) { %><%= prefix %><% } %><%= key %><%= suffix %>" alt="" /><% } %></a> <% }); %>' + '</div>' + '<div class="pagination-right"><a class="btn btn-default">Reset</a></div>' + '</div>' + '</div>' + '</div>', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { var elt = node.formElement || {}; var nbRows = null; var maxColumns = elt.imageSelectorColumns || 5; data.buttonTitle = elt.imageSelectorTitle || 'Select...'; data.prefix = elt.imagePrefix || ''; data.suffix = elt.imageSuffix || ''; data.width = elt.imageWidth || 32; data.height = elt.imageHeight || 32; data.buttonClass = elt.imageButtonClass || false; if (node.options.length > maxColumns) { nbRows = Math.ceil(node.options.length / maxColumns); data.columns = Math.ceil(node.options.length / nbRows); } else { data.columns = maxColumns; } }, 'getElement': function (el) { return $(el).parent().get(0); }, 'onInsert': function (evt, node) { $(node.el).on('click', '.dropdown-menu a', function (evt) { evt.preventDefault(); evt.stopPropagation(); var img = (evt.target.nodeName.toLowerCase() === 'img') ? $(evt.target) : $(evt.target).find('img'); var value = img.attr('src'); var elt = node.formElement || {}; var prefix = elt.imagePrefix || ''; var suffix = elt.imageSuffix || ''; var width = elt.imageWidth || 32; var height = elt.imageHeight || 32; if (value) { if (value.indexOf(prefix) === 0) { value = value.substring(prefix.length); } value = value.substring(0, value.length - suffix.length); $(node.el).find('input').attr('value', value); $(node.el).find('a[data-toggle="dropdown"]') .addClass(elt.imageButtonClass) .attr('style', 'max-width:' + width + 'px;max-height:' + height + 'px') .html('<img src="' + (!value.match(/^https?:/) ? prefix : '') + value + suffix + '" alt="" />'); } else { $(node.el).find('input').attr('value', ''); $(node.el).find('a[data-toggle="dropdown"]') .removeClass(elt.imageButtonClass) .removeAttr('style') .html(elt.imageSelectorTitle || 'Select...'); } }); } }, 'iconselect': { 'template': '<div>' + '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' + '<div class="dropdown">' + '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' + '<% if (node.value) { %><i class="icon-<%= node.value %>" /><% } else { %><%= buttonTitle %><% } %>' + '</a>' + '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' + '<div>' + '<% _.each(node.options, function(key, idx) { if ((idx > 0) && ((idx % columns) === 0)) { %></div><div><% } %><a class="btn<% if (buttonClass) { %> <%= buttonClass %><% } %>" ><% if (key instanceof Object) { %><i class="icon-<%= key.value %>" alt="<%= key.title %>" /></a><% } else { %><i class="icon-<%= key %>" alt="" /><% } %></a> <% }); %>' + '</div>' + '<div class="pagination-right"><a class="btn">Reset</a></div>' + '</div>' + '</div>' + '</div>', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { var elt = node.formElement || {}; var nbRows = null; var maxColumns = elt.imageSelectorColumns || 5; data.buttonTitle = elt.imageSelectorTitle || 'Select...'; data.buttonClass = elt.imageButtonClass || false; if (node.options.length > maxColumns) { nbRows = Math.ceil(node.options.length / maxColumns); data.columns = Math.ceil(node.options.length / nbRows); } else { data.columns = maxColumns; } }, 'getElement': function (el) { return $(el).parent().get(0); }, 'onInsert': function (evt, node) { $(node.el).on('click', '.dropdown-menu a', function (evt) { evt.preventDefault(); evt.stopPropagation(); var i = (evt.target.nodeName.toLowerCase() === 'i') ? $(evt.target) : $(evt.target).find('i'); var value = i.attr('class'); var elt = node.formElement || {}; if (value) { value = value; $(node.el).find('input').attr('value', value); $(node.el).find('a[data-toggle="dropdown"]') .addClass(elt.imageButtonClass) .html('<i class="'+ value +'" alt="" />'); } else { $(node.el).find('input').attr('value', ''); $(node.el).find('a[data-toggle="dropdown"]') .removeClass(elt.imageButtonClass) .html(elt.imageSelectorTitle || 'Select...'); } }); } }, 'radios':{ 'template': '<div id="<%= node.id %>"><% _.each(node.options, function(key, val) { %><div class="radio"><label><input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" <% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>"' + '<%= (node.disabled? " disabled" : "")%>' + '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' + '/><%= (key instanceof Object ? key.title : key) %></label></div> <% }); %></div>', 'fieldtemplate': true, 'inputfield': true }, 'radiobuttons': { 'template': '<div id="<%= node.id %>">' + '<% _.each(node.options, function(key, val) { %>' + '<label class="btn btn-default <% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %>active btn-success<% } %>">' + '<input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" style="position:absolute;left:-9999px;" ' + '<% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>" />' + '<span><%= (key instanceof Object ? key.title : key) %></span></label> ' + '<% }); %>' + '</div>', 'fieldtemplate': true, 'inputfield': true, 'onInsert': function (evt, node) { var activeClass = 'active'; var elt = node.formElement || {}; if (elt.activeClass) { activeClass += ' ' + elt.activeClass; } $(node.el).find('label').on('click', function () { $(this).parent().find('label').removeClass(activeClass); $(this).addClass(activeClass); }); } }, 'checkboxes':{ 'template': '<div><%= choiceshtml %></div>', 'fieldtemplate': true, 'inputfield': true, 'onBeforeRender': function (data, node) { // Build up choices from the enumeration list var choices = null; var choiceshtml = null; var template = '<div class="checkbox"><label>' + '<input type="checkbox" <% if (value) { %> checked="checked" <% } %> name="<%= name %>" value="1"' + '<%= (node.disabled? " disabled" : "")%>' + '/><%= title %></label></div>'; if (!node || !node.schemaElement) return; if (node.schemaElement.items) { choices = node.schemaElement.items["enum"] || node.schemaElement.items[0]["enum"]; } else { choices = node.schemaElement["enum"]; } if (!choices) return; choiceshtml = ''; _.each(choices, function (choice, idx) { choiceshtml += _.template(template, fieldTemplateSettings)({ name: node.key + '[' + idx + ']', value: _.include(node.value, choice), title: hasOwnProperty(node.formElement.titleMap, choice) ? node.formElement.titleMap[choice] : choice, node: node }); }); data.choiceshtml = choiceshtml; } }, 'array': { 'template': '<div id="<%= id %>"><ul class="_jsonform-array-ul" style="list-style-type:none;"><%= children %></ul>' + '<span class="_jsonform-array-buttons">' + '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' + '<a href="#" class="btn btn-default _jsonform-array-deletelast"><i class="glyphicon glyphicon-minus-sign" title="Delete last"></i></a>' + '</span>' + '</div>', 'fieldtemplate': true, 'array': true, 'childTemplate': function (inner, enableDrag) { if ($('').sortable) { // Insert a "draggable" icon // floating to the left of the main element return '<li data-idx="<%= node.childPos %>">' + // only allow drag of children if enabled (enableDrag ? '<span class="draggable line"><i class="glyphicon glyphicon-list" title="Move item"></i></span>' : '') + inner + '</li>'; } else { return '<li data-idx="<%= node.childPos %>">' + inner + '</li>'; } }, 'onInsert': function (evt, node) { var $nodeid = $(node.el).find('#' + escapeSelector(node.id)); var boundaries = node.getArrayBoundaries(); // Switch two nodes in an array var moveNodeTo = function (fromIdx, toIdx) { // Note "switchValuesWith" extracts values from the DOM since field // values are not synchronized with the tree data structure, so calls // to render are needed at each step to force values down to the DOM // before next move. // TODO: synchronize field values and data structure completely and // call render only once to improve efficiency. if (fromIdx === toIdx) return; var incr = (fromIdx < toIdx) ? 1: -1; var i = 0; var parentEl = $('> ul', $nodeid); for (i = fromIdx; i !== toIdx; i += incr) { node.children[i].switchValuesWith(node.children[i + incr]); node.children[i].render(parentEl.get(0)); node.children[i + incr].render(parentEl.get(0)); } // No simple way to prevent DOM reordering with jQuery UI Sortable, // so we're going to need to move sorted DOM elements back to their // origin position in the DOM ourselves (we switched values but not // DOM elements) var fromEl = $(node.children[fromIdx].el); var toEl = $(node.children[toIdx].el); fromEl.detach(); toEl.detach(); if (fromIdx < toIdx) { if (fromIdx === 0) parentEl.prepend(fromEl); else $(node.children[fromIdx-1].el).after(fromEl); $(node.children[toIdx-1].el).after(toEl); } else { if (toIdx === 0) parentEl.prepend(toEl); else $(node.children[toIdx-1].el).after(toEl); $(node.children[fromIdx-1].el).after(fromEl); } }; $('> span > a._jsonform-array-addmore', $nodeid).click(function (evt) { evt.preventDefault(); evt.stopPropagation(); var idx = node.children.length; if (boundaries.maxItems >= 0) { if (node.children.length > boundaries.maxItems - 2) { $nodeid.find('> span > a._jsonform-array-addmore') .addClass('disabled'); } if (node.children.length > boundaries.maxItems - 1) { return false; } } node.insertArrayItem(idx, $('> ul', $nodeid).get(0)); if ((boundaries.minItems <= 0) || ((boundaries.minItems > 0) && (node.children.length > boundaries.minItems - 1))) { $nodeid.find('> span > a._jsonform-array-deletelast') .removeClass('disabled'); } }); //Simulate Users click to setup the form with its minItems var curItems = $('> ul > li', $nodeid).length; if ((boundaries.minItems > 0) && (curItems < boundaries.minItems)) { for (var i = 0; i < (boundaries.minItems - 1) && ($nodeid.find('> ul > li').length < boundaries.minItems); i++) { 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'); } }); // only allow drag if default or enabled if (!isSet(node.formElement.draggable) || node.formElement.draggable) { if ($(node.el).sortable) { $('> ul', $nodeid).sortable(); $('> ul', $nodeid).bind('sortstop', function (event, ui) { var idx = $(ui.item).data('idx'); var newIdx = $(ui.item).index(); moveNodeTo(idx, newIdx); }); } } } }, 'tabarray': { 'template': '<div id="<%= id %>"><div class="tabbable tabs-left">' + '<ul class="nav nav-tabs">' + '<%= tabs %>' + '</ul>' + '<div class="tab-content">' + '<%= children %>' + '</div>' + '</div>' + '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' + '<a href="#" class="btn btn-default _jsonform-array-deleteitem"><i class="glyphicon glyphicon-minus-sign" title="Delete item"></i></a></div>', 'fieldtemplate': true, 'array': true, 'childTemplate': function (inner) { return '<div data-idx="<%= node.childPos %>" class="tab-pane">' + inner + '</div>'; }, 'onBeforeRender': function (data, node) { // Generate the initial 'tabs' from the children var tabs = ''; _.each(node.children, function (child, idx) { var title = child.legend || child.title || ('Item ' + (idx+1)); tabs += '<li data-idx="' + idx + '"' + ((idx === 0) ? ' class="active"' : '') + '><a class="draggable tab" data-toggle="tab" rel="' + escape(title) + '">' + escapeHTML(title) + '</a></li>'; }); data.tabs = tabs; }, 'onInsert': function (evt, node) { var $nodeid = $(node.el).find('#' + escapeSelector(node.id)); var boundaries = node.getArrayBoundaries(); var moveNodeTo = function (fromIdx, toIdx) { // Note "switchValuesWith" extracts values from the DOM since field // values are not synchronized with the tree data structure, so calls // to render are needed at each step to force values down to the DOM // before next move. // TODO: synchronize field values and data structure completely and // call render only once to improve efficiency. if (fromIdx === toIdx) return; var incr = (fromIdx < toIdx) ? 1: -1; var i = 0; var tabEl = $('> .tabbable > .tab-content', $nodeid).get(0); for (i = fromIdx; i !== toIdx; i += incr) { node.children[i].switchValuesWith(node.children[i + incr]); node.children[i].render(tabEl); node.children[i + incr].render(tabEl); } }; // Refreshes the list of tabs var updateTabs = function (selIdx) { var tabs = ''; var activateFirstTab = false; if (selIdx === undefined) { selIdx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx'); if (selIdx) { selIdx = parseInt(selIdx, 10); } else { activateFirstTab = true; selIdx = 0; } } if (selIdx >= node.children.length) { selIdx = node.children.length - 1; } _.each(node.children, function (child, idx) { $('> .tabbable > .tab-content > [data-idx="' + idx + '"] > fieldset > legend', $nodeid).html(child.legend); var title = child.legend || child.title || ('Item ' + (idx+1)); tabs += '<li data-idx="' + idx + '">' + '<a class="draggable tab" data-toggle="tab" rel="' + escape(title) + '">' + escapeHTML(title) + '</a></li>'; }); $('> .tabbable > .nav-tabs', $nodeid).html(tabs); if (activateFirstTab) { $('> .tabbable > .nav-tabs [data-idx="0"]', $nodeid).addClass('active'); } $('> .tabbable > .nav-tabs [data-toggle="tab"]', $nodeid).eq(selIdx).click(); }; $('> a._jsonform-array-deleteitem', $nodeid).click(function (evt) { var idx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx'); evt.preventDefault(); evt.stopPropagation(); if (boundaries.minItems > 0) { if (node.children.length < boundaries.minItems + 1) { $nodeid.find('> a._jsonform-array-deleteitem') .addClass('disabled'); } if (node.children.length <= boundaries.minItems) return false; } node.deleteArrayItem(idx); updateTabs(); if ((node.children.length < boundaries.minItems + 1) || (node.children.length === 0)) { $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled'); } if ((boundaries.maxItems >= 0) && (node.children.length <= boundaries.maxItems)) { $nodeid.find('> a._jsonform-array-addmore').removeClass('disabled'); } }); $('> a._jsonform-array-addmore', $nodeid).click(function (evt) { var idx = node.children.length; if (boundaries.maxItems>=0) { if (node.children.length>boundaries.maxItems-2) { $('> a._jsonform-array-addmore', $nodeid).addClass("disabled"); } if (node.children.length > boundaries.maxItems - 1) { return false; } } evt.preventDefault(); evt.stopPropagation(); node.insertArrayItem(idx, $nodeid.find('> .tabbable > .tab-content').get(0)); updateTabs(idx); if ((boundaries.minItems <= 0) || ((boundaries.minItems > 0) && (idx > boundaries.minItems - 1))) { $nodeid.find('> a._jsonform-array-deleteitem').removeClass('disabled'); } }); $(node.el).on('legendUpdated', function (evt) { updateTabs(); evt.preventDefault(); evt.stopPropagation(); }); // only allow drag if default or enabled if (!isSet(node.formElement.draggable) || node.formElement.draggable) { if ($(node.el).sortable) { $('> .tabbable > .nav-tabs', $nodeid).sortable({ containment: node.el, tolerance: 'pointer' }); $('> .tabbable > .nav-tabs', $nodeid).bind('sortstop', function (event, ui) { var idx = $(ui.item).data('idx'); var newIdx = $(ui.item).index(); moveNodeTo(idx, newIdx); updateTabs(newIdx); }); } } // Simulate User's click to setup the form with its minItems if ((boundaries.minItems >= 0) && (node.children.length <= boundaries.minItems)) { for (var i = 0; i < (boundaries.minItems - 1); i++) { $nodeid.find('> a._jsonform-array-addmore').click(); } $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled'); updateTabs(); } if ((boundaries.maxItems >= 0) && (node.children.length >= boundaries.maxItems)) { $nodeid.find('> a._jsonform-array-addmore').addClass('disabled'); } if ((boundaries.minItems >= 0) && (node.children.length <= boundaries.minItems)) { $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled'); } } }, 'help': { 'template':'<span class="help-block" style="padding-top:5px"><%= elt.helpvalue %></span>', 'fieldtemplate': true }, 'msg': { 'template': '<%= elt.msg %>' }, 'fieldset': { 'template': '<fieldset class="form-group jsonform-error-<%= keydash %> <% if (elt.expandable) { %>expandable<% } %> <%= elt.htmlClass?elt.htmlClass:"" %>" ' + '<% if (id) { %> id="<%= id %>"<% } %>' + '>' + '<% if (node.title || node.legend) { %><legend role="treeitem" aria-expanded="false"><%= node.title || node.legend %></legend><% } %>' + '<% if (elt.expandable) { %><div class="form-group"><% } %>' + '<%= children %>' + '<% if (elt.expandable) { %></div><% } %>' + '</fieldset>', onInsert: function (evt, node) { if (node.el !== null) { $('.expandable > div, .expandable > fieldset', node.el).hide(); // See #233 $(".expandable", node.el).removeClass("expanded"); } } }, 'advancedfieldset': { 'template': '<fieldset' + '<% if (id) { %> id="<%= id %>"<% } %>' + ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' + '<legend role="treeitem" aria-expanded="false"><%= (node.title || node.legend) ? (node.title || node.legend) : "Advanced options" %></legend>' + '<div class="form-group">' +