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
JavaScript
/* 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, '&')
.replace(/</g, '<')
.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 '<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">' +