pywebexec 1.9.14__py3-none-any.whl → 1.9.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3857 @@
1
+ /* Copyright (c) 2012 Joshfire - MIT license */
2
+ /**
3
+ * @fileoverview Core of the JSON Form client-side library.
4
+ *
5
+ * Generates an HTML form from a structured data model and a layout description.
6
+ *
7
+ * The library may also validate inputs entered by the user against the data model
8
+ * upon form submission and create the structured data object initialized with the
9
+ * values that were submitted.
10
+ *
11
+ * The library depends on:
12
+ * - jQuery
13
+ * - the underscore library
14
+ * - a JSON parser/serializer. Nothing to worry about in modern browsers.
15
+ * - the JSONFormValidation library (in jsv.js) for validation purpose
16
+ *
17
+ * See documentation at:
18
+ * http://developer.joshfire.com/doc/dev/ref/jsonform
19
+ *
20
+ * The library creates and maintains an internal data tree along with the DOM.
21
+ * That structure is necessary to handle arrays (and nested arrays!) that are
22
+ * dynamic by essence.
23
+ */
24
+
25
+ /*global window*/
26
+
27
+ (function(serverside, global, $, _, JSON) {
28
+ if (serverside && !_) {
29
+ _ = require('underscore');
30
+ }
31
+
32
+ /**
33
+ * Regular expressions used to extract array indexes in input field names
34
+ */
35
+ var reArray = /\[([0-9]*)\](?=\[|\.|$)/g;
36
+
37
+ /**
38
+ * Template settings for form views
39
+ */
40
+ var fieldTemplateSettings = {
41
+ evaluate : /<%([\s\S]+?)%>/g,
42
+ interpolate : /<%=([\s\S]+?)%>/g
43
+ };
44
+
45
+ /**
46
+ * Template settings for value replacement
47
+ */
48
+ var valueTemplateSettings = {
49
+ evaluate : /\{\[([\s\S]+?)\]\}/g,
50
+ interpolate : /\{\{([\s\S]+?)\}\}/g
51
+ };
52
+
53
+ /**
54
+ * Returns true if given value is neither "undefined" nor null
55
+ */
56
+ var isSet = function (value) {
57
+ return !(_.isUndefined(value) || _.isNull(value));
58
+ };
59
+
60
+ /**
61
+ * Returns true if given property is directly property of an object
62
+ */
63
+ var hasOwnProperty = function (obj, prop) {
64
+ return typeof obj === 'object' && obj.hasOwnProperty(prop);
65
+ }
66
+
67
+ /**
68
+ * The jsonform object whose methods will be exposed to the window object
69
+ */
70
+ var jsonform = {util:{}};
71
+
72
+
73
+ // From backbonejs
74
+ var escapeHTML = function (string) {
75
+ if (!isSet(string)) {
76
+ return '';
77
+ }
78
+ string = '' + string;
79
+ if (!string) {
80
+ return '';
81
+ }
82
+ return string
83
+ .replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&amp;')
84
+ .replace(/</g, '&lt;')
85
+ .replace(/>/g, '&gt;')
86
+ .replace(/"/g, '&quot;')
87
+ .replace(/'/g, '&#x27;')
88
+ .replace(/\//g, '&#x2F;');
89
+ };
90
+
91
+ /**
92
+ * Escapes selector name for use with jQuery
93
+ *
94
+ * All meta-characters listed in jQuery doc are escaped:
95
+ * http://api.jquery.com/category/selectors/
96
+ *
97
+ * @function
98
+ * @param {String} selector The jQuery selector to escape
99
+ * @return {String} The escaped selector.
100
+ */
101
+ var escapeSelector = function (selector) {
102
+ return selector.replace(/([ \!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '\\$1');
103
+ };
104
+
105
+ /**
106
+ *
107
+ * Slugifies a string by replacing spaces with _. Used to create
108
+ * valid classnames and ids for the form.
109
+ *
110
+ * @function
111
+ * @param {String} str The string to slugify
112
+ * @return {String} The slugified string.
113
+ */
114
+ var slugify = function(str) {
115
+ return str.replace(/\ /g, '_');
116
+ }
117
+
118
+ /**
119
+ * Initializes tabular sections in forms. Such sections are generated by the
120
+ * 'selectfieldset' type of elements in JSON Form.
121
+ *
122
+ * Input fields that are not visible are automatically disabled
123
+ * not to appear in the submitted form. That's on purpose, as tabs
124
+ * are meant to convey an alternative (and not a sequence of steps).
125
+ *
126
+ * The tabs menu is not rendered as tabs but rather as a select field because
127
+ * it's easier to grasp that it's an alternative.
128
+ *
129
+ * Code based on bootstrap-tabs.js, updated to:
130
+ * - react to option selection instead of tab click
131
+ * - disable input fields in non visible tabs
132
+ * - disable the possibility to have dropdown menus (no meaning here)
133
+ * - act as a regular function instead of as a jQuery plug-in.
134
+ *
135
+ * @function
136
+ * @param {Object} tabs jQuery object that contains the tabular sections
137
+ * to initialize. The object may reference more than one element.
138
+ */
139
+ var initializeTabs = function (tabs) {
140
+ var activate = function (element, container) {
141
+ container
142
+ .find('> .active')
143
+ .removeClass('active');
144
+ element.addClass('active');
145
+ };
146
+
147
+ var enableFields = function ($target, targetIndex) {
148
+ // Enable all fields in the targeted tab
149
+ $target.find('input, textarea, select').removeAttr('disabled');
150
+
151
+ // Disable all fields in other tabs
152
+ $target.parent()
153
+ .children(':not([data-idx=' + targetIndex + '])')
154
+ .find('input, textarea, select')
155
+ .attr('disabled', 'disabled');
156
+ };
157
+
158
+ var optionSelected = function (e) {
159
+ var $option = $("option:selected", $(this)),
160
+ $select = $(this),
161
+ // do not use .attr() as it sometimes unexplicably fails
162
+ targetIdx = $option.get(0).getAttribute('data-idx') || $option.attr('value'),
163
+ $target;
164
+
165
+ e.preventDefault();
166
+ if ($option.hasClass('active')) {
167
+ return;
168
+ }
169
+
170
+ $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']');
171
+
172
+ activate($option, $select);
173
+ activate($target, $target.parent());
174
+ enableFields($target, targetIdx);
175
+ };
176
+
177
+ var tabClicked = function (e) {
178
+ var $a = $('a', $(this));
179
+ var $content = $(this).parents('.tabbable').first()
180
+ .find('.tab-content').first();
181
+ var targetIdx = $(this).index();
182
+ // The `>` here is to prevent activating selectfieldsets inside a tabarray
183
+ var $target = $content.find('> [data-idx=' + targetIdx + ']');
184
+
185
+ e.preventDefault();
186
+ activate($(this), $(this).parent());
187
+ activate($target, $target.parent());
188
+ if ($(this).parent().hasClass('jsonform-alternative')) {
189
+ enableFields($target, targetIdx);
190
+ }
191
+ };
192
+
193
+ tabs.each(function () {
194
+ $(this).delegate('select.nav', 'change', optionSelected);
195
+ $(this).find('select.nav').each(function () {
196
+ $(this).val($(this).find('.active').attr('value'));
197
+ // do not use .attr() as it sometimes unexplicably fails
198
+ var targetIdx = $(this).find('option:selected').get(0).getAttribute('data-idx') ||
199
+ $(this).find('option:selected').attr('value');
200
+ var $target = $(this).parents('.tabbable').eq(0).find('> .tab-content > [data-idx=' + targetIdx + ']');
201
+ enableFields($target, targetIdx);
202
+ });
203
+
204
+ $(this).delegate('ul.nav li', 'click', tabClicked);
205
+ $(this).find('ul.nav li.active').click();
206
+ });
207
+ };
208
+
209
+
210
+ // Twitter bootstrap-friendly HTML boilerplate for standard inputs
211
+ jsonform.fieldTemplate = function(inner) {
212
+ return '<div ' +
213
+ '<% for(var key in elt.htmlMetaData) {%>' +
214
+ '<%= key %>="<%= elt.htmlMetaData[key] %>" ' +
215
+ '<% }%>' +
216
+ 'class="form-group jsonform-error-<%= keydash %>' +
217
+ '<%= elt.htmlClass ? " " + elt.htmlClass : "" %>' +
218
+ '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " jsonform-required" : "") %>' +
219
+ '<%= (node.readOnly ? " jsonform-readonly" : "") %>' +
220
+ '<%= (node.disabled ? " jsonform-disabled" : "") %>' +
221
+ '">' +
222
+ '<% if (!elt.notitle) { %>' +
223
+ '<label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label>' +
224
+ '<% } %>' +
225
+ '<div class="controls">' +
226
+ '<% if (node.prepend || node.append) { %>' +
227
+ '<div class="<% if (node.prepend) { %>input-group<% } %>' +
228
+ '<% if (node.append) { %> input-group<% } %>">' +
229
+ '<% if (node.prepend) { %>' +
230
+ '<span class="input-group-addon"><%= node.prepend %></span>' +
231
+ '<% } %>' +
232
+ '<% } %>' +
233
+ inner +
234
+ '<% if (node.append) { %>' +
235
+ '<span class="input-group-addon"><%= node.append %></span>' +
236
+ '<% } %>' +
237
+ '<% if (node.prepend || node.append) { %>' +
238
+ '</div>' +
239
+ '<% } %>' +
240
+ '<% if (node.description) { %>' +
241
+ '<span class="help-block"><%= node.description %></span>' +
242
+ '<% } %>' +
243
+ '<span class="help-block jsonform-errortext" style="display:none;"></span>' +
244
+ '</div></div>';
245
+ };
246
+
247
+ var fileDisplayTemplate = '<div class="_jsonform-preview">' +
248
+ '<% if (value.type=="image") { %>' +
249
+ '<img class="jsonform-preview" id="jsonformpreview-<%= id %>" src="<%= value.url %>" />' +
250
+ '<% } else { %>' +
251
+ '<a href="<%= value.url %>"><%= value.name %></a> (<%= Math.ceil(value.size/1024) %>kB)' +
252
+ '<% } %>' +
253
+ '</div>' +
254
+ '<a href="#" class="btn btn-default _jsonform-delete"><i class="glyphicon glyphicon-remove" title="Remove"></i></a> ';
255
+
256
+ var inputFieldTemplate = function (type) {
257
+ return {
258
+ 'template': '<input type="' + type + '" ' +
259
+ 'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' +
260
+ 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
261
+ ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' +
262
+ '<%= (node.disabled? " disabled" : "")%>' +
263
+ '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
264
+ '<%= (node.schemaElement && (node.schemaElement.step > 0 || node.schemaElement.step == "any") ? " step=\'" + node.schemaElement.step + "\'" : "") %>' +
265
+ '<%= (node.schemaElement && node.schemaElement.minLength ? " minlength=\'" + node.schemaElement.minLength + "\'" : "") %>' +
266
+ '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
267
+ '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' +
268
+ '<%= (node.schemaElement && (node.schemaElement.type === "number") && !isNaN(node.schemaElement.maximum) ? " max=" + \'"\' + node.schemaElement.maximum + \'"\' : "")%>' +
269
+ '<%= (node.schemaElement && (node.schemaElement.type === "number") && !isNaN(node.schemaElement.minimum) ? " min=" + \'"\' + node.schemaElement.minimum + \'"\' : "")%>' +
270
+ '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
271
+ ' />',
272
+ 'fieldtemplate': true,
273
+ 'inputfield': true
274
+ }
275
+ };
276
+
277
+ jsonform.elementTypes = {
278
+ 'none': {
279
+ 'template': ''
280
+ },
281
+ 'root': {
282
+ 'template': '<div><%= children %></div>'
283
+ },
284
+ 'text': inputFieldTemplate('text'),
285
+ 'password': inputFieldTemplate('password'),
286
+ 'date': inputFieldTemplate('date'),
287
+ 'datetime': inputFieldTemplate('datetime'),
288
+ 'datetime-local': inputFieldTemplate('datetime-local'),
289
+ 'email': inputFieldTemplate('email'),
290
+ 'month': inputFieldTemplate('month'),
291
+ 'number': inputFieldTemplate('number'),
292
+ 'search': inputFieldTemplate('search'),
293
+ 'tel': inputFieldTemplate('tel'),
294
+ 'time': inputFieldTemplate('time'),
295
+ 'url': inputFieldTemplate('url'),
296
+ 'week': inputFieldTemplate('week'),
297
+ 'range': {
298
+ 'template': '<div class="range"><input type="range" ' +
299
+ '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
300
+ 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
301
+ ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' +
302
+ '<%= (node.disabled? " disabled" : "")%>' +
303
+ ' min=<%= range.min %>' +
304
+ ' max=<%= range.max %>' +
305
+ ' step=<%= range.step %>' +
306
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
307
+ ' /><% if (range.indicator) { %><span class="range-value" rel="<%= id %>"><%= escape(value) %></span><% } %></div>',
308
+ 'fieldtemplate': true,
309
+ 'inputfield': true,
310
+ 'onInput': function(evt, elt) {
311
+ const valueIndicator = document.querySelector('span.range-value[rel="' + elt.id + '"]');
312
+ if (valueIndicator) {
313
+ valueIndicator.innerText = evt.target.value;
314
+ }
315
+ },
316
+ 'onBeforeRender': function (data, node) {
317
+ data.range = {
318
+ min: 1,
319
+ max: 100,
320
+ step: 1,
321
+ indicator: false
322
+ };
323
+ if (!node || !node.schemaElement) return;
324
+ if (node.formElement && node.formElement.step) {
325
+ data.range.step = node.formElement.step;
326
+ }
327
+ if (node.formElement && node.formElement.indicator) {
328
+ data.range.indicator = node.formElement.indicator;
329
+ }
330
+ if (typeof node.schemaElement.minimum !== 'undefined') {
331
+ if (node.schemaElement.exclusiveMinimum) {
332
+ data.range.min = node.schemaElement.minimum + data.range.step;
333
+ }
334
+ else {
335
+ data.range.min = node.schemaElement.minimum;
336
+ }
337
+ }
338
+ if (typeof node.schemaElement.maximum !== 'undefined') {
339
+ if (node.schemaElement.exclusiveMaximum) {
340
+ data.range.max = node.schemaElement.maximum - data.range.step;
341
+ }
342
+ else {
343
+ data.range.max = node.schemaElement.maximum;
344
+ }
345
+ }
346
+ }
347
+ },
348
+ 'color':{
349
+ 'template':'<input type="text" ' +
350
+ '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
351
+ 'name="<%= node.name %>" value="<%= escape(value) %>" id="<%= id %>"' +
352
+ ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' +
353
+ '<%= (node.disabled? " disabled" : "")%>' +
354
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
355
+ ' />',
356
+ 'fieldtemplate': true,
357
+ 'inputfield': true,
358
+ 'onInsert': function(evt, node) {
359
+ $(node.el).find('#' + escapeSelector(node.id)).spectrum({
360
+ preferredFormat: "hex",
361
+ showInput: true
362
+ });
363
+ }
364
+ },
365
+ 'textarea':{
366
+ 'template':'<textarea id="<%= id %>" name="<%= node.name %>" ' +
367
+ '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
368
+ 'style="height:<%= elt.height || "150px" %>;width:<%= elt.width || "100%" %>;"' +
369
+ ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' +
370
+ '<%= (node.disabled? " disabled" : "")%>' +
371
+ '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
372
+ '<%= (node.schemaElement && node.schemaElement.minLength ? " minlength=\'" + node.schemaElement.minLength + "\'" : "") %>' +
373
+ '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
374
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
375
+ '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
376
+ '><%= value %></textarea>',
377
+ 'fieldtemplate': true,
378
+ 'inputfield': true
379
+ },
380
+ 'wysihtml5':{
381
+ 'template':'<textarea id="<%= id %>" name="<%= node.name %>" style="height:<%= elt.height || "300px" %>;width:<%= elt.width || "100%" %>;"' +
382
+ ' aria-label="<%= node.title ? escape(node.title) : node.name %>"' +
383
+ '<%= (fieldHtmlClass ? "class=\'" + fieldHtmlClass + "\' " : "") %>' +
384
+ '<%= (node.disabled? " disabled" : "")%>' +
385
+ '<%= (node.readOnly ? " readonly=\'readonly\'" : "") %>' +
386
+ '<%= (node.schemaElement && node.schemaElement.minLength ? " minlength=\'" + node.schemaElement.minLength + "\'" : "") %>' +
387
+ '<%= (node.schemaElement && node.schemaElement.maxLength ? " maxlength=\'" + node.schemaElement.maxLength + "\'" : "") %>' +
388
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
389
+ '<%= (node.placeholder? " placeholder=" + \'"\' + escape(node.placeholder) + \'"\' : "")%>' +
390
+ '><%= value %></textarea>',
391
+ 'fieldtemplate': true,
392
+ 'inputfield': true,
393
+ 'onInsert': function (evt, node) {
394
+ var setup = function () {
395
+ //protect from double init
396
+ if ($(node.el).data("wysihtml5")) return;
397
+ $(node.el).data("wysihtml5_loaded",true);
398
+
399
+ $(node.el).find('#' + escapeSelector(node.id)).wysihtml5({
400
+ "html": true,
401
+ "link": true,
402
+ "font-styles":true,
403
+ "image": false,
404
+ "events": {
405
+ "load": function () {
406
+ // In chrome, if an element is required and hidden, it leads to
407
+ // the error 'An invalid form control with name='' is not focusable'
408
+ // See http://stackoverflow.com/questions/7168645/invalid-form-control-only-in-google-chrome
409
+ $(this.textareaElement).removeAttr('required');
410
+ }
411
+ }
412
+ });
413
+ };
414
+
415
+ // Is there a setup hook?
416
+ if (window.jsonform_wysihtml5_setup) {
417
+ window.jsonform_wysihtml5_setup(setup);
418
+ return;
419
+ }
420
+
421
+ // Wait until wysihtml5 is loaded
422
+ var itv = window.setInterval(function() {
423
+ if (window.wysihtml5) {
424
+ window.clearInterval(itv);
425
+ setup();
426
+ }
427
+ },1000);
428
+ }
429
+ },
430
+ 'ace':{
431
+ '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>',
432
+ 'fieldtemplate': true,
433
+ 'inputfield': true,
434
+ 'onInsert': function (evt, node) {
435
+ var setup = function () {
436
+ var formElement = node.formElement || {};
437
+ var ace = window.ace;
438
+ var editor = ace.edit($(node.el).find('#' + escapeSelector(node.id) + '__ace').get(0));
439
+ var idSelector = '#' + escapeSelector(node.id) + '__hidden';
440
+ // Force editor to use "\n" for new lines, not to bump into ACE "\r" conversion issue
441
+ // (ACE is ok with "\r" on pasting but fails to return "\r" when value is extracted)
442
+ editor.getSession().setNewLineMode('unix');
443
+ editor.renderer.setShowPrintMargin(false);
444
+ editor.setTheme("ace/theme/"+(formElement.aceTheme||"twilight"));
445
+
446
+ if (formElement.aceMode) {
447
+ editor.getSession().setMode("ace/mode/"+formElement.aceMode);
448
+ }
449
+ editor.getSession().setTabSize(2);
450
+
451
+ // Set the contents of the initial manifest file
452
+ editor.getSession().setValue(node.value||"");
453
+
454
+ //TODO: this is clearly sub-optimal
455
+ // 'Lazily' bind to the onchange 'ace' event to give
456
+ // priority to user edits
457
+ var lazyChanged = _.debounce(function () {
458
+ $(node.el).find(idSelector).val(editor.getSession().getValue());
459
+ $(node.el).find(idSelector).change();
460
+ }, 600);
461
+ editor.getSession().on('change', lazyChanged);
462
+
463
+ editor.on('blur', function() {
464
+ $(node.el).find(idSelector).change();
465
+ $(node.el).find(idSelector).trigger("blur");
466
+ });
467
+ editor.on('focus', function() {
468
+ $(node.el).find(idSelector).trigger("focus");
469
+ });
470
+ };
471
+
472
+ // Is there a setup hook?
473
+ if (window.jsonform_ace_setup) {
474
+ window.jsonform_ace_setup(setup);
475
+ return;
476
+ }
477
+
478
+ // Wait until ACE is loaded
479
+ var itv = window.setInterval(function() {
480
+ if (window.ace) {
481
+ window.clearInterval(itv);
482
+ setup();
483
+ }
484
+ },1000);
485
+ }
486
+ },
487
+ 'checkbox':{
488
+ 'template': '<div class="checkbox"><label><input type="checkbox" id="<%= id %>" ' +
489
+ '<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %>' +
490
+ 'name="<%= node.name %>" value="1" <% if (value) {%>checked<% } %>' +
491
+ '<%= (node.disabled? " disabled" : "")%>' +
492
+ '<%= (node.schemaElement && node.schemaElement.required && (node.schemaElement.type !== "boolean") ? " required=\'required\'" : "") %>' +
493
+ ' /><%= node.inlinetitle || "" %>' +
494
+ '</label></div>',
495
+ 'fieldtemplate': true,
496
+ 'inputfield': true,
497
+ 'getElement': function (el) {
498
+ return $(el).parent().get(0);
499
+ }
500
+ },
501
+ 'file':{
502
+ 'template':'<input class="input-file" id="<%= id %>" name="<%= node.name %>" type="file" ' +
503
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
504
+ '<%= (node.formElement && node.formElement.accept ? (" accept=\'" + node.formElement.accept + "\'") : "") %>' +
505
+ '/>',
506
+ 'fieldtemplate': true,
507
+ 'inputfield': true
508
+ },
509
+ 'file-hosted-public':{
510
+ '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>',
511
+ 'fieldtemplate': true,
512
+ 'inputfield': true,
513
+ 'getElement': function (el) {
514
+ return $(el).parent().get(0);
515
+ },
516
+ 'onBeforeRender': function (data, node) {
517
+
518
+ if (!node.ownerTree._transloadit_generic_public_index) {
519
+ node.ownerTree._transloadit_generic_public_index=1;
520
+ } else {
521
+ node.ownerTree._transloadit_generic_public_index++;
522
+ }
523
+
524
+ data.transloaditname = "_transloadit_jsonform_genericupload_public_"+node.ownerTree._transloadit_generic_public_index;
525
+
526
+ if (!node.ownerTree._transloadit_generic_elts) node.ownerTree._transloadit_generic_elts = {};
527
+ node.ownerTree._transloadit_generic_elts[data.transloaditname] = node;
528
+ },
529
+ 'onChange': function(evt,elt) {
530
+ // The "transloadit" function should be called only once to enable
531
+ // the service when the form is submitted. Has it already been done?
532
+ if (elt.ownerTree._transloadit_bound) {
533
+ return false;
534
+ }
535
+ elt.ownerTree._transloadit_bound = true;
536
+
537
+ // Call the "transloadit" function on the form element
538
+ var formElt = $(elt.ownerTree.domRoot);
539
+ formElt.transloadit({
540
+ autoSubmit: false,
541
+ wait: true,
542
+ onSuccess: function (assembly) {
543
+ // Image has been uploaded. Check the "results" property that
544
+ // contains the list of files that Transloadit produced. There
545
+ // should be one image per file input in the form at most.
546
+ var results = _.values(assembly.results);
547
+ results = _.flatten(results);
548
+ _.each(results, function (result) {
549
+ // Save the assembly result in the right hidden input field
550
+ var id = elt.ownerTree._transloadit_generic_elts[result.field].id;
551
+ var input = formElt.find('#' + escapeSelector(id));
552
+ var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {
553
+ return !!isSet(result.meta[key]);
554
+ });
555
+ result.meta = _.pick(result.meta, nonEmptyKeys);
556
+ input.val(JSON.stringify(result));
557
+ });
558
+
559
+ // Unbind transloadit from the form
560
+ elt.ownerTree._transloadit_bound = false;
561
+ formElt.unbind('submit.transloadit');
562
+
563
+ // Submit the form on next tick
564
+ _.delay(function () {
565
+ elt.ownerTree.submit();
566
+ }, 10);
567
+ },
568
+ onError: function (assembly) {
569
+ // TODO: report the error to the user
570
+ console.log('assembly error', assembly);
571
+ }
572
+ });
573
+ },
574
+ 'onInsert': function (evt, node) {
575
+ $(node.el).find('a._jsonform-delete').on('click', function (evt) {
576
+ $(node.el).find('._jsonform-preview').remove();
577
+ $(node.el).find('a._jsonform-delete').remove();
578
+ $(node.el).find('#' + escapeSelector(node.id)).val('');
579
+ evt.preventDefault();
580
+ return false;
581
+ });
582
+ },
583
+ 'onSubmit':function(evt, elt) {
584
+ if (elt.ownerTree._transloadit_bound) {
585
+ return false;
586
+ }
587
+ return true;
588
+ }
589
+
590
+ },
591
+ 'file-transloadit': {
592
+ '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>',
593
+ 'fieldtemplate': true,
594
+ 'inputfield': true,
595
+ 'getElement': function (el) {
596
+ return $(el).parent().get(0);
597
+ },
598
+ 'onChange': function (evt, elt) {
599
+ // The "transloadit" function should be called only once to enable
600
+ // the service when the form is submitted. Has it already been done?
601
+ if (elt.ownerTree._transloadit_bound) {
602
+ return false;
603
+ }
604
+ elt.ownerTree._transloadit_bound = true;
605
+
606
+ // Call the "transloadit" function on the form element
607
+ var formElt = $(elt.ownerTree.domRoot);
608
+ formElt.transloadit({
609
+ autoSubmit: false,
610
+ wait: true,
611
+ onSuccess: function (assembly) {
612
+ // Image has been uploaded. Check the "results" property that
613
+ // contains the list of files that Transloadit produced. Note
614
+ // JSONForm only supports 1-to-1 associations, meaning it
615
+ // expects the "results" property to contain only one image
616
+ // per file input in the form.
617
+ var results = _.values(assembly.results);
618
+ results = _.flatten(results);
619
+ _.each(results, function (result) {
620
+ // Save the assembly result in the right hidden input field
621
+ var input = formElt.find('input[name="' +
622
+ result.field.replace(/^_transloadit_/, '') +
623
+ '"]');
624
+ var nonEmptyKeys = _.filter(_.keys(result.meta), function (key) {
625
+ return !!isSet(result.meta[key]);
626
+ });
627
+ result.meta = _.pick(result.meta, nonEmptyKeys);
628
+ input.val(JSON.stringify(result));
629
+ });
630
+
631
+ // Unbind transloadit from the form
632
+ elt.ownerTree._transloadit_bound = false;
633
+ formElt.unbind('submit.transloadit');
634
+
635
+ // Submit the form on next tick
636
+ _.delay(function () {
637
+ elt.ownerTree.submit();
638
+ }, 10);
639
+ },
640
+ onError: function (assembly) {
641
+ // TODO: report the error to the user
642
+ console.log('assembly error', assembly);
643
+ }
644
+ });
645
+ },
646
+ 'onInsert': function (evt, node) {
647
+ $(node.el).find('a._jsonform-delete').on('click', function (evt) {
648
+ $(node.el).find('._jsonform-preview').remove();
649
+ $(node.el).find('a._jsonform-delete').remove();
650
+ $(node.el).find('#' + escapeSelector(node.id)).val('');
651
+ evt.preventDefault();
652
+ return false;
653
+ });
654
+ },
655
+ 'onSubmit': function (evt, elt) {
656
+ if (elt.ownerTree._transloadit_bound) {
657
+ return false;
658
+ }
659
+ return true;
660
+ }
661
+ },
662
+ 'select':{
663
+ 'template':'<select name="<%= node.name %>" id="<%= id %>"' +
664
+ 'class=\'form-control<%= (fieldHtmlClass ? " " + fieldHtmlClass : "") %>\'' +
665
+ '<%= (node.schemaElement && node.schemaElement.disabled? " disabled" : "")%>' +
666
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
667
+ '> ' +
668
+ '<% _.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> <% }}}); %> ' +
669
+ '</select>',
670
+ 'fieldtemplate': true,
671
+ 'inputfield': true
672
+ },
673
+ 'imageselect': {
674
+ 'template': '<div>' +
675
+ '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' +
676
+ '<div class="dropdown">' +
677
+ '<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"<% } %>>' +
678
+ '<% if (node.value) { %><img src="<% if (!node.value.match(/^https?:/)) { %><%= prefix %><% } %><%= node.value %><%= suffix %>" alt="" /><% } else { %><%= buttonTitle %><% } %>' +
679
+ '</a>' +
680
+ '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' +
681
+ '<div>' +
682
+ '<% _.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> <% }); %>' +
683
+ '</div>' +
684
+ '<div class="pagination-right"><a class="btn btn-default">Reset</a></div>' +
685
+ '</div>' +
686
+ '</div>' +
687
+ '</div>',
688
+ 'fieldtemplate': true,
689
+ 'inputfield': true,
690
+ 'onBeforeRender': function (data, node) {
691
+ var elt = node.formElement || {};
692
+ var nbRows = null;
693
+ var maxColumns = elt.imageSelectorColumns || 5;
694
+ data.buttonTitle = elt.imageSelectorTitle || 'Select...';
695
+ data.prefix = elt.imagePrefix || '';
696
+ data.suffix = elt.imageSuffix || '';
697
+ data.width = elt.imageWidth || 32;
698
+ data.height = elt.imageHeight || 32;
699
+ data.buttonClass = elt.imageButtonClass || false;
700
+ if (node.options.length > maxColumns) {
701
+ nbRows = Math.ceil(node.options.length / maxColumns);
702
+ data.columns = Math.ceil(node.options.length / nbRows);
703
+ }
704
+ else {
705
+ data.columns = maxColumns;
706
+ }
707
+ },
708
+ 'getElement': function (el) {
709
+ return $(el).parent().get(0);
710
+ },
711
+ 'onInsert': function (evt, node) {
712
+ $(node.el).on('click', '.dropdown-menu a', function (evt) {
713
+ evt.preventDefault();
714
+ evt.stopPropagation();
715
+ var img = (evt.target.nodeName.toLowerCase() === 'img') ?
716
+ $(evt.target) :
717
+ $(evt.target).find('img');
718
+ var value = img.attr('src');
719
+ var elt = node.formElement || {};
720
+ var prefix = elt.imagePrefix || '';
721
+ var suffix = elt.imageSuffix || '';
722
+ var width = elt.imageWidth || 32;
723
+ var height = elt.imageHeight || 32;
724
+ if (value) {
725
+ if (value.indexOf(prefix) === 0) {
726
+ value = value.substring(prefix.length);
727
+ }
728
+ value = value.substring(0, value.length - suffix.length);
729
+ $(node.el).find('input').attr('value', value);
730
+ $(node.el).find('a[data-toggle="dropdown"]')
731
+ .addClass(elt.imageButtonClass)
732
+ .attr('style', 'max-width:' + width + 'px;max-height:' + height + 'px')
733
+ .html('<img src="' + (!value.match(/^https?:/) ? prefix : '') + value + suffix + '" alt="" />');
734
+ }
735
+ else {
736
+ $(node.el).find('input').attr('value', '');
737
+ $(node.el).find('a[data-toggle="dropdown"]')
738
+ .removeClass(elt.imageButtonClass)
739
+ .removeAttr('style')
740
+ .html(elt.imageSelectorTitle || 'Select...');
741
+ }
742
+ });
743
+ }
744
+ },
745
+ 'iconselect': {
746
+ 'template': '<div>' +
747
+ '<input type="hidden" name="<%= node.name %>" id="<%= node.id %>" value="<%= value %>" />' +
748
+ '<div class="dropdown">' +
749
+ '<a class="btn<% if (buttonClass && node.value) { %> <%= buttonClass %><% } %>" data-toggle="dropdown" href="#"<% if (node.value) { %> style="max-width:<%= width %>px;max-height:<%= height %>px"<% } %>>' +
750
+ '<% if (node.value) { %><i class="icon-<%= node.value %>" /><% } else { %><%= buttonTitle %><% } %>' +
751
+ '</a>' +
752
+ '<div class="dropdown-menu navbar" id="<%= node.id %>_dropdown">' +
753
+ '<div>' +
754
+ '<% _.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> <% }); %>' +
755
+ '</div>' +
756
+ '<div class="pagination-right"><a class="btn">Reset</a></div>' +
757
+ '</div>' +
758
+ '</div>' +
759
+ '</div>',
760
+ 'fieldtemplate': true,
761
+ 'inputfield': true,
762
+ 'onBeforeRender': function (data, node) {
763
+ var elt = node.formElement || {};
764
+ var nbRows = null;
765
+ var maxColumns = elt.imageSelectorColumns || 5;
766
+ data.buttonTitle = elt.imageSelectorTitle || 'Select...';
767
+ data.buttonClass = elt.imageButtonClass || false;
768
+ if (node.options.length > maxColumns) {
769
+ nbRows = Math.ceil(node.options.length / maxColumns);
770
+ data.columns = Math.ceil(node.options.length / nbRows);
771
+ }
772
+ else {
773
+ data.columns = maxColumns;
774
+ }
775
+ },
776
+ 'getElement': function (el) {
777
+ return $(el).parent().get(0);
778
+ },
779
+ 'onInsert': function (evt, node) {
780
+ $(node.el).on('click', '.dropdown-menu a', function (evt) {
781
+ evt.preventDefault();
782
+ evt.stopPropagation();
783
+ var i = (evt.target.nodeName.toLowerCase() === 'i') ?
784
+ $(evt.target) :
785
+ $(evt.target).find('i');
786
+ var value = i.attr('class');
787
+ var elt = node.formElement || {};
788
+ if (value) {
789
+ value = value;
790
+ $(node.el).find('input').attr('value', value);
791
+ $(node.el).find('a[data-toggle="dropdown"]')
792
+ .addClass(elt.imageButtonClass)
793
+ .html('<i class="'+ value +'" alt="" />');
794
+ }
795
+ else {
796
+ $(node.el).find('input').attr('value', '');
797
+ $(node.el).find('a[data-toggle="dropdown"]')
798
+ .removeClass(elt.imageButtonClass)
799
+ .html(elt.imageSelectorTitle || 'Select...');
800
+ }
801
+ });
802
+ }
803
+ },
804
+ 'radios':{
805
+ '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) %>"' +
806
+ '<%= (node.disabled? " disabled" : "")%>' +
807
+ '<%= (node.schemaElement && node.schemaElement.required ? " required=\'required\'" : "") %>' +
808
+ '/><%= (key instanceof Object ? key.title : key) %></label></div> <% }); %></div>',
809
+ 'fieldtemplate': true,
810
+ 'inputfield': true
811
+ },
812
+ 'radiobuttons': {
813
+ 'template': '<div id="<%= node.id %>">' +
814
+ '<% _.each(node.options, function(key, val) { %>' +
815
+ '<label class="btn btn-default">' +
816
+ '<input<%= (fieldHtmlClass ? " class=\'" + fieldHtmlClass + "\'": "") %> type="radio" style="position:absolute;left:-9999px;" ' +
817
+ '<% if (((key instanceof Object) && (value === key.value)) || (value === key)) { %> checked="checked" <% } %> name="<%= node.name %>" value="<%= (key instanceof Object ? key.value : key) %>" />' +
818
+ '<span><%= (key instanceof Object ? key.title : key) %></span></label> ' +
819
+ '<% }); %>' +
820
+ '</div>',
821
+ 'fieldtemplate': true,
822
+ 'inputfield': true,
823
+ 'onInsert': function (evt, node) {
824
+ var activeClass = 'active';
825
+ var elt = node.formElement || {};
826
+ if (elt.activeClass) {
827
+ activeClass += ' ' + elt.activeClass;
828
+ }
829
+ $(node.el).find('label').on('click', function () {
830
+ $(this).parent().find('label').removeClass(activeClass);
831
+ $(this).addClass(activeClass);
832
+ });
833
+ // Set active on insert
834
+ $(node.el).find('input:checked').parent().addClass(activeClass)
835
+ }
836
+ },
837
+ 'checkboxes':{
838
+ 'template': '<div><%= choiceshtml %></div>',
839
+ 'fieldtemplate': true,
840
+ 'inputfield': true,
841
+ 'onBeforeRender': function (data, node) {
842
+ // Build up choices from the enumeration list
843
+ var choices = null;
844
+ var choiceshtml = null;
845
+ var template = '<div class="checkbox"><label>' +
846
+ '<input type="checkbox" <% if (value) { %> checked="checked" <% } %> name="<%= name %>" value="1"' +
847
+ '<%= (node.disabled? " disabled" : "")%>' +
848
+ '/><%= title %></label></div>';
849
+ if (!node || !node.schemaElement) return;
850
+
851
+ if (node.schemaElement.items) {
852
+ choices =
853
+ node.schemaElement.items["enum"] ||
854
+ node.schemaElement.items[0]["enum"];
855
+ } else {
856
+ choices = node.schemaElement["enum"];
857
+ }
858
+ if (!choices) return;
859
+
860
+ choiceshtml = '';
861
+ _.each(choices, function (choice, idx) {
862
+ choiceshtml += _.template(template, fieldTemplateSettings)({
863
+ name: node.key + '[' + idx + ']',
864
+ value: _.include(node.value, choice),
865
+ title: hasOwnProperty(node.formElement.titleMap, choice) ? node.formElement.titleMap[choice] : choice,
866
+ node: node
867
+ });
868
+ });
869
+
870
+ data.choiceshtml = choiceshtml;
871
+ }
872
+ },
873
+ 'array': {
874
+ 'template': '<div id="<%= id %>"><ul class="_jsonform-array-ul" style="list-style-type:none;"><%= children %></ul>' +
875
+ '<span class="_jsonform-array-buttons">' +
876
+ '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' +
877
+ '<a href="#" class="btn btn-default _jsonform-array-deletelast"><i class="glyphicon glyphicon-minus-sign" title="Delete last"></i></a>' +
878
+ '</span>' +
879
+ '</div>',
880
+ 'fieldtemplate': true,
881
+ 'array': true,
882
+ 'childTemplate': function (inner, enableDrag) {
883
+ if ($('').sortable) {
884
+ // Insert a "draggable" icon
885
+ // floating to the left of the main element
886
+ return '<li data-idx="<%= node.childPos %>">' +
887
+ // only allow drag of children if enabled
888
+ (enableDrag ? '<span class="draggable line"><i class="glyphicon glyphicon-list" title="Move item"></i></span>' : '') +
889
+ inner +
890
+ '<span class="_jsonform-array-buttons">' +
891
+ '<a href="#" class="btn btn-default _jsonform-array-deletecurrent"><i class="glyphicon glyphicon-minus-sign" title="Delete current"></i></a>' +
892
+ '</span>' +
893
+ '</li>';
894
+ }
895
+ else {
896
+ return '<li data-idx="<%= node.childPos %>">' +
897
+ inner +
898
+ '<span class="_jsonform-array-buttons">' +
899
+ '<a href="#" class="btn btn-default _jsonform-array-deletecurrent"><i class="glyphicon glyphicon-minus-sign" title="Delete current"></i></a>' +
900
+ '</span>' +
901
+ '</li>';
902
+ }
903
+ },
904
+ 'onInsert': function (evt, node) {
905
+ var $nodeid = $(node.el).find('#' + escapeSelector(node.id));
906
+ var boundaries = node.getArrayBoundaries();
907
+
908
+ // Switch two nodes in an array
909
+ var moveNodeTo = function (fromIdx, toIdx) {
910
+ // Note "switchValuesWith" extracts values from the DOM since field
911
+ // values are not synchronized with the tree data structure, so calls
912
+ // to render are needed at each step to force values down to the DOM
913
+ // before next move.
914
+ // TODO: synchronize field values and data structure completely and
915
+ // call render only once to improve efficiency.
916
+ if (fromIdx === toIdx) return;
917
+ var incr = (fromIdx < toIdx) ? 1: -1;
918
+ var i = 0;
919
+ var parentEl = $('> ul', $nodeid);
920
+ for (i = fromIdx; i !== toIdx; i += incr) {
921
+ node.children[i].switchValuesWith(node.children[i + incr]);
922
+ node.children[i].render(parentEl.get(0));
923
+ node.children[i + incr].render(parentEl.get(0));
924
+ }
925
+
926
+ // No simple way to prevent DOM reordering with jQuery UI Sortable,
927
+ // so we're going to need to move sorted DOM elements back to their
928
+ // origin position in the DOM ourselves (we switched values but not
929
+ // DOM elements)
930
+ var fromEl = $(node.children[fromIdx].el);
931
+ var toEl = $(node.children[toIdx].el);
932
+ fromEl.detach();
933
+ toEl.detach();
934
+ if (fromIdx < toIdx) {
935
+ if (fromIdx === 0) parentEl.prepend(fromEl);
936
+ else $(node.children[fromIdx-1].el).after(fromEl);
937
+ $(node.children[toIdx-1].el).after(toEl);
938
+ }
939
+ else {
940
+ if (toIdx === 0) parentEl.prepend(toEl);
941
+ else $(node.children[toIdx-1].el).after(toEl);
942
+ $(node.children[fromIdx-1].el).after(fromEl);
943
+ }
944
+ };
945
+
946
+ $('> span > a._jsonform-array-addmore', $nodeid).click(function (evt) {
947
+ evt.preventDefault();
948
+ evt.stopPropagation();
949
+ var idx = node.children.length;
950
+ if (boundaries.maxItems >= 0) {
951
+ if (node.children.length > boundaries.maxItems - 2) {
952
+ $nodeid.find('> span > a._jsonform-array-addmore')
953
+ .addClass('disabled');
954
+ }
955
+ if (node.children.length > boundaries.maxItems - 1) {
956
+ return false;
957
+ }
958
+ }
959
+ node.insertArrayItem(idx, $('> ul', $nodeid).get(0));
960
+ if ((boundaries.minItems <= 0) ||
961
+ ((boundaries.minItems > 0) &&
962
+ (node.children.length > boundaries.minItems - 1))) {
963
+ $nodeid.find('a._jsonform-array-deletecurrent')
964
+ .removeClass('disabled');
965
+ $nodeid.find('> span > a._jsonform-array-deletelast')
966
+ .removeClass('disabled');
967
+ }
968
+ });
969
+
970
+ //Simulate Users click to setup the form with its minItems
971
+ var curItems = $('> ul > li', $nodeid).length;
972
+ if ((boundaries.minItems > 0) &&
973
+ (curItems < boundaries.minItems)) {
974
+ for (var i = 0; i < (boundaries.minItems - 1) && ($nodeid.find('> ul > li').length < boundaries.minItems); i++) {
975
+ node.insertArrayItem(curItems, $nodeid.find('> ul').get(0));
976
+ }
977
+ }
978
+ if ((boundaries.minItems > 0) &&
979
+ (node.children.length <= boundaries.minItems)) {
980
+ $nodeid.find('a._jsonform-array-deletecurrent')
981
+ .addClass('disabled');
982
+ $nodeid.find('> span > a._jsonform-array-deletelast')
983
+ .addClass('disabled');
984
+ }
985
+
986
+ function deleteArrayItem (idx) {
987
+ if (boundaries.minItems > 0) {
988
+ if (node.children.length < boundaries.minItems + 2) {
989
+ $nodeid.find('> span > a._jsonform-array-deletelast')
990
+ .addClass('disabled');
991
+ }
992
+ if (node.children.length <= boundaries.minItems) {
993
+ return false;
994
+ }
995
+ }
996
+ else if (node.children.length === 1) {
997
+ $nodeid.find('a._jsonform-array-deletecurrent')
998
+ .addClass('disabled');
999
+ $nodeid.find('> span > a._jsonform-array-deletelast')
1000
+ .addClass('disabled');
1001
+ }
1002
+ node.deleteArrayItem(idx);
1003
+ if (boundaries.minItems > 0) {
1004
+ if (node.children.length < boundaries.minItems + 1) {
1005
+ $nodeid.find('a._jsonform-array-deletecurrent')
1006
+ .addClass('disabled');
1007
+ }
1008
+ }
1009
+ if ((boundaries.maxItems >= 0) && (idx <= boundaries.maxItems - 1)) {
1010
+ $nodeid.find('> span > a._jsonform-array-addmore')
1011
+ .removeClass('disabled');
1012
+ }
1013
+ }
1014
+
1015
+ $($nodeid).on('click', 'a._jsonform-array-deletecurrent', function (evt) {
1016
+ var idx = $(this).closest('[data-idx]').data('idx')
1017
+ evt.preventDefault();
1018
+ evt.stopPropagation();
1019
+ return deleteArrayItem(idx);
1020
+ });
1021
+
1022
+ $('> span > a._jsonform-array-deletelast', $nodeid).click(function (evt) {
1023
+ var idx = node.children.length - 1;
1024
+ evt.preventDefault();
1025
+ evt.stopPropagation();
1026
+ return deleteArrayItem(idx);
1027
+ });
1028
+
1029
+ // only allow drag if default or enabled
1030
+ if (!isSet(node.formElement.draggable) || node.formElement.draggable) {
1031
+ if ($(node.el).sortable) {
1032
+ $('> ul', $nodeid).sortable();
1033
+ $('> ul', $nodeid).bind('sortstop', function (event, ui) {
1034
+ var idx = $(ui.item).data('idx');
1035
+ var newIdx = $(ui.item).index();
1036
+ moveNodeTo(idx, newIdx);
1037
+ });
1038
+ }
1039
+ }
1040
+ }
1041
+ },
1042
+ 'tabarray': {
1043
+ 'template': '<div id="<%= id %>"><div class="tabbable tabs-left">' +
1044
+ '<ul class="nav nav-tabs">' +
1045
+ '<%= tabs %>' +
1046
+ '</ul>' +
1047
+ '<div class="tab-content">' +
1048
+ '<%= children %>' +
1049
+ '</div>' +
1050
+ '</div>' +
1051
+ '<a href="#" class="btn btn-default _jsonform-array-addmore"><i class="glyphicon glyphicon-plus-sign" title="Add new"></i></a> ' +
1052
+ '<a href="#" class="btn btn-default _jsonform-array-deleteitem"><i class="glyphicon glyphicon-minus-sign" title="Delete item"></i></a></div>',
1053
+ 'fieldtemplate': true,
1054
+ 'array': true,
1055
+ 'childTemplate': function (inner) {
1056
+ return '<div data-idx="<%= node.childPos %>" class="tab-pane">' +
1057
+ inner +
1058
+ '</div>';
1059
+ },
1060
+ 'onBeforeRender': function (data, node) {
1061
+ // Generate the initial 'tabs' from the children
1062
+ var tabs = '';
1063
+ _.each(node.children, function (child, idx) {
1064
+ var title = child.legend ||
1065
+ child.title ||
1066
+ ('Item ' + (idx+1));
1067
+ tabs += '<li data-idx="' + idx + '"' +
1068
+ ((idx === 0) ? ' class="active"' : '') +
1069
+ '><a class="draggable tab" data-toggle="tab" rel="' + escape(title) + '">' +
1070
+ escapeHTML(title) +
1071
+ '</a></li>';
1072
+ });
1073
+ data.tabs = tabs;
1074
+ },
1075
+ 'onInsert': function (evt, node) {
1076
+ var $nodeid = $(node.el).find('#' + escapeSelector(node.id));
1077
+ var boundaries = node.getArrayBoundaries();
1078
+
1079
+ var moveNodeTo = function (fromIdx, toIdx) {
1080
+ // Note "switchValuesWith" extracts values from the DOM since field
1081
+ // values are not synchronized with the tree data structure, so calls
1082
+ // to render are needed at each step to force values down to the DOM
1083
+ // before next move.
1084
+ // TODO: synchronize field values and data structure completely and
1085
+ // call render only once to improve efficiency.
1086
+ if (fromIdx === toIdx) return;
1087
+ var incr = (fromIdx < toIdx) ? 1: -1;
1088
+ var i = 0;
1089
+ var tabEl = $('> .tabbable > .tab-content', $nodeid).get(0);
1090
+ for (i = fromIdx; i !== toIdx; i += incr) {
1091
+ node.children[i].switchValuesWith(node.children[i + incr]);
1092
+ node.children[i].render(tabEl);
1093
+ node.children[i + incr].render(tabEl);
1094
+ }
1095
+ };
1096
+
1097
+
1098
+ // Refreshes the list of tabs
1099
+ var updateTabs = function (selIdx) {
1100
+ var tabs = '';
1101
+ var activateFirstTab = false;
1102
+ if (selIdx === undefined) {
1103
+ selIdx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx');
1104
+ if (selIdx) {
1105
+ selIdx = parseInt(selIdx, 10);
1106
+ }
1107
+ else {
1108
+ activateFirstTab = true;
1109
+ selIdx = 0;
1110
+ }
1111
+ }
1112
+ if (selIdx >= node.children.length) {
1113
+ selIdx = node.children.length - 1;
1114
+ }
1115
+ _.each(node.children, function (child, idx) {
1116
+ $('> .tabbable > .tab-content > [data-idx="' + idx + '"] > fieldset > legend', $nodeid).html(child.legend);
1117
+ var title = child.legend || child.title || ('Item ' + (idx+1));
1118
+ tabs += '<li data-idx="' + idx + '">' +
1119
+ '<a class="draggable tab" data-toggle="tab" rel="' + escape(title) + '">' +
1120
+ escapeHTML(title) +
1121
+ '</a></li>';
1122
+ });
1123
+ $('> .tabbable > .nav-tabs', $nodeid).html(tabs);
1124
+ if (activateFirstTab) {
1125
+ $('> .tabbable > .nav-tabs [data-idx="0"]', $nodeid).addClass('active');
1126
+ }
1127
+ $('> .tabbable > .nav-tabs [data-toggle="tab"]', $nodeid).eq(selIdx).click();
1128
+ };
1129
+
1130
+ $('> a._jsonform-array-deleteitem', $nodeid).click(function (evt) {
1131
+ var idx = $('> .tabbable > .nav-tabs .active', $nodeid).data('idx');
1132
+ evt.preventDefault();
1133
+ evt.stopPropagation();
1134
+ if (boundaries.minItems > 0) {
1135
+ if (node.children.length < boundaries.minItems + 1) {
1136
+ $nodeid.find('> a._jsonform-array-deleteitem')
1137
+ .addClass('disabled');
1138
+ }
1139
+ if (node.children.length <= boundaries.minItems) return false;
1140
+ }
1141
+ node.deleteArrayItem(idx);
1142
+ updateTabs();
1143
+ if ((node.children.length < boundaries.minItems + 1) ||
1144
+ (node.children.length === 0)) {
1145
+ $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
1146
+ }
1147
+ if ((boundaries.maxItems >= 0) &&
1148
+ (node.children.length <= boundaries.maxItems)) {
1149
+ $nodeid.find('> a._jsonform-array-addmore').removeClass('disabled');
1150
+ }
1151
+ });
1152
+
1153
+ $('> a._jsonform-array-addmore', $nodeid).click(function (evt) {
1154
+ var idx = node.children.length;
1155
+ if (boundaries.maxItems>=0) {
1156
+ if (node.children.length>boundaries.maxItems-2) {
1157
+ $('> a._jsonform-array-addmore', $nodeid).addClass("disabled");
1158
+ }
1159
+ if (node.children.length > boundaries.maxItems - 1) {
1160
+ return false;
1161
+ }
1162
+ }
1163
+ evt.preventDefault();
1164
+ evt.stopPropagation();
1165
+ node.insertArrayItem(idx,
1166
+ $nodeid.find('> .tabbable > .tab-content').get(0));
1167
+ updateTabs(idx);
1168
+ if ((boundaries.minItems <= 0) ||
1169
+ ((boundaries.minItems > 0) && (idx > boundaries.minItems - 1))) {
1170
+ $nodeid.find('> a._jsonform-array-deleteitem').removeClass('disabled');
1171
+ }
1172
+ });
1173
+
1174
+ $(node.el).on('legendUpdated', function (evt) {
1175
+ updateTabs();
1176
+ evt.preventDefault();
1177
+ evt.stopPropagation();
1178
+ });
1179
+
1180
+ // only allow drag if default or enabled
1181
+ if (!isSet(node.formElement.draggable) || node.formElement.draggable) {
1182
+ if ($(node.el).sortable) {
1183
+ $('> .tabbable > .nav-tabs', $nodeid).sortable({
1184
+ containment: node.el,
1185
+ tolerance: 'pointer'
1186
+ });
1187
+ $('> .tabbable > .nav-tabs', $nodeid).bind('sortstop', function (event, ui) {
1188
+ var idx = $(ui.item).data('idx');
1189
+ var newIdx = $(ui.item).index();
1190
+ moveNodeTo(idx, newIdx);
1191
+ updateTabs(newIdx);
1192
+ });
1193
+ }
1194
+ }
1195
+
1196
+ // Simulate User's click to setup the form with its minItems
1197
+ if ((boundaries.minItems >= 0) &&
1198
+ (node.children.length <= boundaries.minItems)) {
1199
+ for (var i = 0; i < (boundaries.minItems - 1); i++) {
1200
+ $nodeid.find('> a._jsonform-array-addmore').click();
1201
+ }
1202
+ $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
1203
+ updateTabs();
1204
+ }
1205
+
1206
+ if ((boundaries.maxItems >= 0) &&
1207
+ (node.children.length >= boundaries.maxItems)) {
1208
+ $nodeid.find('> a._jsonform-array-addmore').addClass('disabled');
1209
+ }
1210
+ if ((boundaries.minItems >= 0) &&
1211
+ (node.children.length <= boundaries.minItems)) {
1212
+ $nodeid.find('> a._jsonform-array-deleteitem').addClass('disabled');
1213
+ }
1214
+ }
1215
+ },
1216
+ 'help': {
1217
+ 'template':'<span class="help-block" style="padding-top:5px"><%= elt.helpvalue %></span>',
1218
+ 'fieldtemplate': true
1219
+ },
1220
+ 'msg': {
1221
+ 'template': '<%= elt.msg %>'
1222
+ },
1223
+ 'fieldset': {
1224
+ 'template': '<fieldset class="form-group jsonform-error-<%= keydash %> <% if (elt.expandable) { %>expandable<% } %> <%= elt.htmlClass?elt.htmlClass:"" %>" ' +
1225
+ '<% if (id) { %> id="<%= id %>"<% } %>' +
1226
+ '>' +
1227
+ '<% if (node.title || node.legend) { %><legend role="treeitem" aria-expanded="false"><%= node.title || node.legend %></legend><% } %>' +
1228
+ '<% if (elt.expandable) { %><div class="form-group"><% } %>' +
1229
+ '<%= children %>' +
1230
+ '<% if (elt.expandable) { %></div><% } %>' +
1231
+ '</fieldset>',
1232
+ onInsert: function (evt, node) {
1233
+ if (node.el !== null) {
1234
+ $('.expandable > div, .expandable > fieldset', node.el).hide();
1235
+ // See #233
1236
+ $(".expandable", node.el).removeClass("expanded");
1237
+ }
1238
+ }
1239
+ },
1240
+ 'advancedfieldset': {
1241
+ 'template': '<fieldset' +
1242
+ '<% if (id) { %> id="<%= id %>"<% } %>' +
1243
+ ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' +
1244
+ '<legend role="treeitem" aria-expanded="false"><%= (node.title || node.legend) ? (node.title || node.legend) : "Advanced options" %></legend>' +
1245
+ '<div class="form-group">' +
1246
+ '<%= children %>' +
1247
+ '</div>' +
1248
+ '</fieldset>',
1249
+ onInsert: function (evt, node) {
1250
+ if (node.el !== null) {
1251
+ $('.expandable > div, .expandable > fieldset', node.el).hide();
1252
+ // See #233
1253
+ $(".expandable", node.el).removeClass("expanded");
1254
+ }
1255
+ }
1256
+ },
1257
+ 'authfieldset': {
1258
+ 'template': '<fieldset' +
1259
+ '<% if (id) { %> id="<%= id %>"<% } %>' +
1260
+ ' class="expandable <%= elt.htmlClass?elt.htmlClass:"" %>">' +
1261
+ '<legend role="treeitem" aria-expanded="false"><%= (node.title || node.legend) ? (node.title || node.legend) : "Authentication settings" %></legend>' +
1262
+ '<div class="form-group">' +
1263
+ '<%= children %>' +
1264
+ '</div>' +
1265
+ '</fieldset>',
1266
+ onInsert: function (evt, node) {
1267
+ if (node.el !== null) {
1268
+ $('.expandable > div, .expandable > fieldset', node.el).hide();
1269
+ // See #233
1270
+ $(".expandable", node.el).removeClass("expanded");
1271
+ }
1272
+ }
1273
+ },
1274
+ 'submit':{
1275
+ 'template':'<input type="submit" <% if (id) { %> id="<%= id %>" <% } %> class="btn btn-primary <%= elt.htmlClass?elt.htmlClass:"" %>" value="<%= value || node.title %>"<%= (node.disabled? " disabled" : "")%>/>'
1276
+ },
1277
+ 'button':{
1278
+ 'template':' <button type="button" <% if (id) { %> id="<%= id %>" <% } %> class="btn btn-default <%= elt.htmlClass?elt.htmlClass:"" %>"><%= node.title %></button> '
1279
+ },
1280
+ 'actions':{
1281
+ 'template':'<div class="<%= elt.htmlClass?elt.htmlClass:"" %>"><%= children %></div>'
1282
+ },
1283
+ 'hidden':{
1284
+ 'template':'<input type="hidden" id="<%= id %>" name="<%= node.name %>" value="<%= escape(value) %>" />',
1285
+ 'inputfield': true
1286
+ },
1287
+ 'tabs':{
1288
+ 'template':'<ul class="nav nav-tabs <%= elt.htmlClass?elt.htmlClass:"" %>"' +
1289
+ '<% if (elt.id) { %> id="<%= elt.id %>"<% } %>' +
1290
+ '><%=tab_list%></ul><div class="tab-content" <% if (elt.id) { %> data-tabset="<%= elt.id %>"<% } %>><%=children%></div>',
1291
+ 'getElement': function (el) {
1292
+ return $(el).parent().get(0);
1293
+ },
1294
+ 'onBeforeRender': function (data, node) {
1295
+ // Generate the initial 'tabs' from the children
1296
+ var parentID = escapeHTML(node.id ? node.id + "-" : "")
1297
+ var tab_list = '';
1298
+ _.each(node.children, function (child, idx) {
1299
+ var title = escapeHTML(child.title || ('Item ' + (idx+1)));
1300
+ var title_escaped = title.replace(/ /g,"_");
1301
+ tab_list += '<li class="nav-item' +
1302
+ ((idx === 0) ? ' active' : '') + '">' +
1303
+ '<a href="#'+ parentID + title_escaped +'" class="nav-link"' +
1304
+ ' data-tab="' + parentID + title_escaped + '"' +
1305
+ ' data-toggle="tab">' + title +
1306
+ '</a></li>';
1307
+ });
1308
+ data.tab_list = tab_list;
1309
+ return data;
1310
+ },
1311
+ 'onInsert': function(evt, node){
1312
+ $("#"+node.id+">li.nav-item").on("click", function(e){
1313
+ e.preventDefault();
1314
+ $(node.el).find("div[data-tabset='"+node.id+"']>div.tab-pane.active").each(function(){
1315
+ $(this).removeClass("active");
1316
+ })
1317
+ var tab_id = $(this).find('a').attr('data-tab');
1318
+ $("#"+tab_id).addClass("active");
1319
+ });
1320
+ }
1321
+ },
1322
+ 'tab':{
1323
+ 'template': '<div class="tab-pane' +
1324
+ '<% if (elt.htmlClass) { %> <%= elt.htmlClass %> <% } %>' +
1325
+ //Set the first tab as active
1326
+ '<% if (node.childPos === 0) { %> active<% } %>' +
1327
+ '"' + //Finish end quote of class tag
1328
+ '<% if (node.title) { %> id="<%= node.parentNode.id %>-<%= node.title.replace(/ /g,"_") %>"<% } %>' +
1329
+ '><%= children %></div>'
1330
+ },
1331
+ 'selectfieldset': {
1332
+ 'template': '<fieldset class="tab-container <%= elt.htmlClass?elt.htmlClass:"" %>">' +
1333
+ '<% if (node.legend) { %><legend role="treeitem" aria-expanded="false"><%= node.legend %></legend><% } %>' +
1334
+ '<% if (node.formElement.key) { %><input type="hidden" id="<%= node.id %>" name="<%= node.name %>" value="<%= escape(value) %>" /><% } else { %>' +
1335
+ '<a id="<%= node.id %>"></a><% } %>' +
1336
+ '<div class="tabbable">' +
1337
+ '<div class="form-group<%= node.formElement.hideMenu ? " hide" : "" %>">' +
1338
+ '<% if (!elt.notitle) { %><label for="<%= node.id %>"><%= node.title ? node.title : node.name %></label><% } %>' +
1339
+ '<div class="controls"><%= tabs %></div>' +
1340
+ '</div>' +
1341
+ '<div class="tab-content">' +
1342
+ '<%= children %>' +
1343
+ '</div>' +
1344
+ '</div>' +
1345
+ '</fieldset>',
1346
+ 'inputfield': true,
1347
+ 'getElement': function (el) {
1348
+ return $(el).parent().get(0);
1349
+ },
1350
+ 'childTemplate': function (inner) {
1351
+ return '<div data-idx="<%= node.childPos %>" class="tab-pane' +
1352
+ '<% if (node.active) { %> active<% } %>">' +
1353
+ inner +
1354
+ '</div>';
1355
+ },
1356
+ 'onBeforeRender': function (data, node) {
1357
+ // Before rendering, this function ensures that:
1358
+ // 1. direct children have IDs (used to show/hide the tabs contents)
1359
+ // 2. the tab to active is flagged accordingly. The active tab is
1360
+ // the first one, except if form values are available, in which case
1361
+ // it's the first tab for which there is some value available (or back
1362
+ // to the first one if there are none)
1363
+ // 3. the HTML of the select field used to select tabs is exposed in the
1364
+ // HTML template data as "tabs"
1365
+
1366
+ var children = null;
1367
+ var choices = [];
1368
+ if (node.schemaElement) {
1369
+ choices = node.schemaElement['enum'] || [];
1370
+ }
1371
+ if (node.options) {
1372
+ children = _.map(node.options, function (option, idx) {
1373
+ var child = node.children[idx];
1374
+ child.childPos = idx; // When nested the childPos is always 0.
1375
+ if (option instanceof Object) {
1376
+ option = _.extend({ node: child }, option);
1377
+ option.title = option.title ||
1378
+ child.legend ||
1379
+ child.title ||
1380
+ ('Option ' + (child.childPos+1));
1381
+ option.value = isSet(option.value) ? option.value :
1382
+ isSet(choices[idx]) ? choices[idx] : idx;
1383
+ return option;
1384
+ }
1385
+ else {
1386
+ return {
1387
+ title: option,
1388
+ value: isSet(choices[child.childPos]) ?
1389
+ choices[child.childPos] :
1390
+ child.childPos,
1391
+ node: child
1392
+ };
1393
+ }
1394
+ });
1395
+ }
1396
+ else {
1397
+ children = _.map(node.children, function (child, idx) {
1398
+ child.childPos = idx; // When nested the childPos is always 0.
1399
+ return {
1400
+ title: child.legend || child.title || ('Option ' + (child.childPos+1)),
1401
+ value: choices[child.childPos] || child.value || child.childPos,
1402
+ node: child
1403
+ };
1404
+ });
1405
+ }
1406
+
1407
+ // Reset each children to inactive so that they are not shown on insert
1408
+ // The active one will then be shown later one. This is useful when sorting
1409
+ // arrays with selectfieldset, otherwise both fields could be active at the
1410
+ // same time.
1411
+ _.each(children, function (child, idx) {
1412
+ child.node.active = false
1413
+ });
1414
+
1415
+ var activeChild = null;
1416
+ if (data.value) {
1417
+ activeChild = _.find(children, function (child) {
1418
+ return (child.value === node.value);
1419
+ });
1420
+ }
1421
+ if (!activeChild) {
1422
+ activeChild = _.find(children, function (child) {
1423
+ return child.node.hasNonDefaultValue();
1424
+ });
1425
+ }
1426
+ if (!activeChild) {
1427
+ activeChild = children[0];
1428
+ }
1429
+ activeChild.node.active = true;
1430
+ data.value = activeChild.value;
1431
+
1432
+ var elt = node.formElement;
1433
+ var tabs = '<select class="nav form-control"' +
1434
+ (node.disabled ? ' disabled' : '') +
1435
+ '>';
1436
+ _.each(children, function (child, idx) {
1437
+ tabs += '<option data-idx="' + idx + '" value="' + child.value + '"' +
1438
+ (child.node.active ? ' selected="selected" class="active"' : '') +
1439
+ '>' +
1440
+ escapeHTML(child.title) +
1441
+ '</option>';
1442
+ });
1443
+ tabs += '</select>';
1444
+
1445
+ data.tabs = tabs;
1446
+ return data;
1447
+ },
1448
+ 'onInsert': function (evt, node) {
1449
+ $(node.el).find('select.nav').first().on('change', function (evt) {
1450
+ var $option = $(this).find('option:selected');
1451
+ $(node.el).find('input[type="hidden"]').first().val($option.attr('value'));
1452
+ });
1453
+ }
1454
+ },
1455
+ 'optionfieldset': {
1456
+ 'template': '<div' +
1457
+ '<% if (node.id) { %> id="<%= node.id %>"<% } %>' +
1458
+ '>' +
1459
+ '<%= children %>' +
1460
+ '</div>'
1461
+ },
1462
+ 'section': {
1463
+ 'template': '<div' +
1464
+ '<% if (elt.htmlClass) { %> class="<%= elt.htmlClass %>"<% } %>' +
1465
+ '<% if (node.id) { %> id="<%= node.id %>"<% } %>' +
1466
+ '><%= children %></div>'
1467
+ },
1468
+
1469
+ /**
1470
+ * A "questions" field renders a series of question fields and binds the
1471
+ * result to the value of a schema key.
1472
+ */
1473
+ 'questions': {
1474
+ 'template': '<div>' +
1475
+ '<input type="hidden" id="<%= node.id %>" name="<%= node.name %>" value="<%= escape(value) %>" />' +
1476
+ '<%= children %>' +
1477
+ '</div>',
1478
+ 'fieldtemplate': true,
1479
+ 'inputfield': true,
1480
+ 'getElement': function (el) {
1481
+ return $(el).parent().get(0);
1482
+ },
1483
+ 'onInsert': function (evt, node) {
1484
+ if (!node.children || (node.children.length === 0)) return;
1485
+ _.each(node.children, function (child) {
1486
+ $(child.el).hide();
1487
+ });
1488
+ $(node.children[0].el).show();
1489
+ }
1490
+ },
1491
+
1492
+ /**
1493
+ * A "question" field lets user choose a response among possible choices.
1494
+ * The field is not associated with any schema key. A question should be
1495
+ * part of a "questions" field that binds a series of questions to a
1496
+ * schema key.
1497
+ */
1498
+ 'question': {
1499
+ 'template': '<div id="<%= node.id %>"><% _.each(node.options, function(key, val) { %><label class="<%= (node.formElement.optionsType === "radiobuttons") ? "btn btn-default" : "" %><%= ((key instanceof Object && key.htmlClass) ? " " + key.htmlClass : "") %>"><input type="radio" <% if (node.formElement.optionsType === "radiobuttons") { %> style="position:absolute;left:-9999px;" <% } %>name="<%= node.id %>" value="<%= val %>"<%= (node.disabled? " disabled" : "")%>/><span><%= (key instanceof Object ? key.title : key) %></span></label> <% }); %></div>',
1500
+ 'fieldtemplate': true,
1501
+ 'onInsert': function (evt, node) {
1502
+ var activeClass = 'active';
1503
+ var elt = node.formElement || {};
1504
+ if (elt.activeClass) {
1505
+ activeClass += ' ' + elt.activeClass;
1506
+ }
1507
+
1508
+ // Bind to change events on radio buttons
1509
+ $(node.el).find('input[type="radio"]').on('change', function (evt) {
1510
+ var questionNode = null;
1511
+ var option = node.options[$(this).val()];
1512
+ if (!node.parentNode || !node.parentNode.el) return;
1513
+
1514
+ $(this).parent().parent().find('label').removeClass(activeClass);
1515
+ $(this).parent().addClass(activeClass);
1516
+ $(node.el).nextAll().hide();
1517
+ $(node.el).nextAll().find('input[type="radio"]').prop('checked', false);
1518
+
1519
+ // Execute possible actions (set key value, form submission, open link,
1520
+ // move on to next question)
1521
+ if (option.value) {
1522
+ // Set the key of the 'Questions' parent
1523
+ $(node.parentNode.el).find('input[type="hidden"]').val(option.value);
1524
+ }
1525
+ if (option.next) {
1526
+ questionNode = _.find(node.parentNode.children, function (child) {
1527
+ return (child.formElement && (child.formElement.qid === option.next));
1528
+ });
1529
+ $(questionNode.el).show();
1530
+ $(questionNode.el).nextAll().hide();
1531
+ $(questionNode.el).nextAll().find('input[type="radio"]').prop('checked', false);
1532
+ }
1533
+ if (option.href) {
1534
+ if (option.target) {
1535
+ window.open(option.href, option.target);
1536
+ }
1537
+ else {
1538
+ window.location = option.href;
1539
+ }
1540
+ }
1541
+ if (option.submit) {
1542
+ setTimeout(function () {
1543
+ node.ownerTree.submit();
1544
+ }, 0);
1545
+ }
1546
+ });
1547
+ }
1548
+ }
1549
+ };
1550
+
1551
+
1552
+ //Allow to access subproperties by splitting "."
1553
+ /**
1554
+ * Retrieves the key identified by a path selector in the structured object.
1555
+ *
1556
+ * Levels in the path are separated by a dot. Array items are marked
1557
+ * with [x]. For instance:
1558
+ * foo.bar[3].baz
1559
+ *
1560
+ * @function
1561
+ * @param {Object} obj Structured object to parse
1562
+ * @param {String} key Path to the key to retrieve
1563
+ * @param {boolean} ignoreArrays True to use first element in an array when
1564
+ * stucked on a property. This parameter is basically only useful when
1565
+ * parsing a JSON schema for which the "items" property may either be an
1566
+ * object or an array with one object (only one because JSON form does not
1567
+ * support mix of items for arrays).
1568
+ * @return {Object} The key's value.
1569
+ */
1570
+ jsonform.util.getObjKey = function (obj, key, ignoreArrays) {
1571
+ var innerobj = obj;
1572
+ var keyparts = key.split(".");
1573
+ var subkey = null;
1574
+ var arrayMatch = null;
1575
+ var prop = null;
1576
+
1577
+ for (var i = 0; i < keyparts.length; i++) {
1578
+ if ((innerobj === null) || (typeof innerobj !== "object")) return null;
1579
+ subkey = keyparts[i];
1580
+ prop = subkey.replace(reArray, '');
1581
+ reArray.lastIndex = 0;
1582
+ arrayMatch = reArray.exec(subkey);
1583
+ if (arrayMatch) {
1584
+ while (true) {
1585
+ if (prop && !_.isArray(innerobj[prop])) return null;
1586
+ innerobj = prop ? innerobj[prop][parseInt(arrayMatch[1])] : innerobj[parseInt(arrayMatch[1])];
1587
+ arrayMatch = reArray.exec(subkey);
1588
+ if (!arrayMatch) break;
1589
+ // In the case of multidimensional arrays,
1590
+ // we should not take innerobj[prop][0] anymore,
1591
+ // but innerobj[0] directly
1592
+ prop = null;
1593
+ }
1594
+ } else if (ignoreArrays &&
1595
+ !innerobj[prop] &&
1596
+ _.isArray(innerobj) &&
1597
+ innerobj[0]) {
1598
+ innerobj = innerobj[0][prop];
1599
+ } else {
1600
+ innerobj = innerobj[prop];
1601
+ }
1602
+ }
1603
+
1604
+ if (ignoreArrays && _.isArray(innerobj) && innerobj[0]) {
1605
+ return innerobj[0];
1606
+ } else {
1607
+ return innerobj;
1608
+ }
1609
+ };
1610
+
1611
+
1612
+ /**
1613
+ * Sets the key identified by a path selector to the given value.
1614
+ *
1615
+ * Levels in the path are separated by a dot. Array items are marked
1616
+ * with [x]. For instance:
1617
+ * foo.bar[3].baz
1618
+ *
1619
+ * The hierarchy is automatically created if it does not exist yet.
1620
+ *
1621
+ * @function
1622
+ * @param {Object} obj The object to build
1623
+ * @param {String} key The path to the key to set where each level
1624
+ * is separated by a dot, and array items are flagged with [x].
1625
+ * @param {Object} value The value to set, may be of any type.
1626
+ */
1627
+ jsonform.util.setObjKey = function(obj,key,value) {
1628
+ var innerobj = obj;
1629
+ var keyparts = key.split(".");
1630
+ var subkey = null;
1631
+ var arrayMatch = null;
1632
+ var prop = null;
1633
+
1634
+ for (var i = 0; i < keyparts.length-1; i++) {
1635
+ subkey = keyparts[i];
1636
+ prop = subkey.replace(reArray, '');
1637
+ reArray.lastIndex = 0;
1638
+ arrayMatch = reArray.exec(subkey);
1639
+ if (arrayMatch) {
1640
+ // Subkey is part of an array
1641
+ while (true) {
1642
+ if (!_.isArray(innerobj[prop])) {
1643
+ innerobj[prop] = [];
1644
+ }
1645
+ innerobj = innerobj[prop];
1646
+ prop = parseInt(arrayMatch[1], 10);
1647
+ arrayMatch = reArray.exec(subkey);
1648
+ if (!arrayMatch) break;
1649
+ }
1650
+ if ((typeof innerobj[prop] !== 'object') ||
1651
+ (innerobj[prop] === null)) {
1652
+ innerobj[prop] = {};
1653
+ }
1654
+ innerobj = innerobj[prop];
1655
+ }
1656
+ else {
1657
+ // "Normal" subkey
1658
+ if ((typeof innerobj[prop] !== 'object') ||
1659
+ (innerobj[prop] === null)) {
1660
+ innerobj[prop] = {};
1661
+ }
1662
+ innerobj = innerobj[prop];
1663
+ }
1664
+ }
1665
+
1666
+ // Set the final value
1667
+ subkey = keyparts[keyparts.length - 1];
1668
+ prop = subkey.replace(reArray, '');
1669
+ reArray.lastIndex = 0;
1670
+ arrayMatch = reArray.exec(subkey);
1671
+ if (arrayMatch) {
1672
+ while (true) {
1673
+ if (!_.isArray(innerobj[prop])) {
1674
+ innerobj[prop] = [];
1675
+ }
1676
+ innerobj = innerobj[prop];
1677
+ prop = parseInt(arrayMatch[1], 10);
1678
+ arrayMatch = reArray.exec(subkey);
1679
+ if (!arrayMatch) break;
1680
+ }
1681
+ innerobj[prop] = value;
1682
+ }
1683
+ else {
1684
+ innerobj[prop] = value;
1685
+ }
1686
+ };
1687
+
1688
+
1689
+ /**
1690
+ * Retrieves the key definition from the given schema.
1691
+ *
1692
+ * The key is identified by the path that leads to the key in the
1693
+ * structured object that the schema would generate. Each level is
1694
+ * separated by a '.'. Array levels are marked with []. For instance:
1695
+ * foo.bar[].baz
1696
+ * ... to retrieve the definition of the key at the following location
1697
+ * in the JSON schema (using a dotted path notation):
1698
+ * foo.properties.bar.items.properties.baz
1699
+ *
1700
+ * @function
1701
+ * @param {Object} schema The JSON schema to retrieve the key from
1702
+ * @param {String} key The path to the key, each level being separated
1703
+ * by a dot and array items being flagged with [].
1704
+ * @return {Object} The key definition in the schema, null if not found.
1705
+ */
1706
+ var getSchemaKey = function(schema,key) {
1707
+ var schemaKey = key
1708
+ .replace(/\./g, '.properties.')
1709
+ .replace(/\[[0-9]*\]/g, '.items');
1710
+ var schemaDef = jsonform.util.getObjKey(schema, schemaKey, true);
1711
+ if (schemaDef && schemaDef.$ref) {
1712
+ throw new Error('JSONForm does not yet support schemas that use the ' +
1713
+ '$ref keyword. See: https://github.com/joshfire/jsonform/issues/54');
1714
+ }
1715
+ return schemaDef;
1716
+ };
1717
+
1718
+
1719
+ /**
1720
+ * Truncates the key path to the requested depth.
1721
+ *
1722
+ * For instance, if the key path is:
1723
+ * foo.bar[].baz.toto[].truc[].bidule
1724
+ * and the requested depth is 1, the returned key will be:
1725
+ * foo.bar[].baz.toto
1726
+ *
1727
+ * Note the function includes the path up to the next depth level.
1728
+ *
1729
+ * @function
1730
+ * @param {String} key The path to the key in the schema, each level being
1731
+ * separated by a dot and array items being flagged with [].
1732
+ * @param {Number} depth The array depth
1733
+ * @return {String} The path to the key truncated to the given depth.
1734
+ */
1735
+ var truncateToArrayDepth = function (key, arrayDepth) {
1736
+ var depth = 0;
1737
+ var pos = 0;
1738
+ if (!key) return null;
1739
+
1740
+ if (arrayDepth > 0) {
1741
+ while (depth < arrayDepth) {
1742
+ pos = key.indexOf('[]', pos);
1743
+ if (pos === -1) {
1744
+ // Key path is not "deep" enough, simply return the full key
1745
+ return key;
1746
+ }
1747
+ pos = pos + 2;
1748
+ depth += 1;
1749
+ }
1750
+ }
1751
+
1752
+ // Move one step further to the right without including the final []
1753
+ pos = key.indexOf('[]', pos);
1754
+ if (pos === -1) return key;
1755
+ else return key.substring(0, pos);
1756
+ };
1757
+
1758
+ /**
1759
+ * Applies the array path to the key path.
1760
+ *
1761
+ * For instance, if the key path is:
1762
+ * foo.bar[].baz.toto[].truc[].bidule
1763
+ * and the arrayPath [4, 2], the returned key will be:
1764
+ * foo.bar[4].baz.toto[2].truc[].bidule
1765
+ *
1766
+ * @function
1767
+ * @param {String} key The path to the key in the schema, each level being
1768
+ * separated by a dot and array items being flagged with [].
1769
+ * @param {Array(Number)} arrayPath The array path to apply, e.g. [4, 2]
1770
+ * @return {String} The path to the key that matches the array path.
1771
+ */
1772
+ var applyArrayPath = function (key, arrayPath) {
1773
+ var depth = 0;
1774
+ if (!key) return null;
1775
+ if (!arrayPath || (arrayPath.length === 0)) return key;
1776
+ var newKey = key.replace(reArray, function (str, p1) {
1777
+ // Note this function gets called as many times as there are [x] in the ID,
1778
+ // from left to right in the string. The goal is to replace the [x] with
1779
+ // the appropriate index in the new array path, if defined.
1780
+ var newIndex = str;
1781
+ if (isSet(arrayPath[depth])) {
1782
+ newIndex = '[' + arrayPath[depth] + ']';
1783
+ }
1784
+ depth += 1;
1785
+ return newIndex;
1786
+ });
1787
+ return newKey;
1788
+ };
1789
+
1790
+
1791
+ /**
1792
+ * Returns the initial value that a field identified by its key
1793
+ * should take.
1794
+ *
1795
+ * The "initial" value is defined as:
1796
+ * 1. the previously submitted value if already submitted
1797
+ * 2. the default value defined in the layout of the form
1798
+ * 3. the default value defined in the schema
1799
+ *
1800
+ * The "value" returned is intended for rendering purpose,
1801
+ * meaning that, for fields that define a titleMap property,
1802
+ * the function returns the label, and not the intrinsic value.
1803
+ *
1804
+ * The function handles values that contains template strings,
1805
+ * e.g. {{values.foo[].bar}} or {{idx}}.
1806
+ *
1807
+ * When the form is a string, the function truncates the resulting string
1808
+ * to meet a potential "maxLength" constraint defined in the schema, using
1809
+ * "..." to mark the truncation. Note it does not validate the resulting
1810
+ * string against other constraints (e.g. minLength, pattern) as it would
1811
+ * be hard to come up with an automated course of action to "fix" the value.
1812
+ *
1813
+ * @function
1814
+ * @param {Object} formObject The JSON Form object
1815
+ * @param {String} key The generic key path (e.g. foo[].bar.baz[])
1816
+ * @param {Array(Number)} arrayPath The array path that identifies
1817
+ * the unique value in the submitted form (e.g. [1, 3])
1818
+ * @param {Object} tpldata Template data object
1819
+ * @param {Boolean} usePreviousValues true to use previously submitted values
1820
+ * if defined.
1821
+ */
1822
+ var getInitialValue = function (formObject, key, arrayPath, tpldata, usePreviousValues) {
1823
+ var value = null;
1824
+
1825
+ // Complete template data for template function
1826
+ tpldata = tpldata || {};
1827
+ tpldata.idx = tpldata.idx ||
1828
+ (arrayPath ? arrayPath[arrayPath.length-1] : 1);
1829
+ tpldata.value = isSet(tpldata.value) ? tpldata.value : '';
1830
+ tpldata.getValue = tpldata.getValue || function (key) {
1831
+ return getInitialValue(formObject, key, arrayPath, tpldata, usePreviousValues);
1832
+ };
1833
+
1834
+ // Helper function that returns the form element that explicitly
1835
+ // references the given key in the schema.
1836
+ var getFormElement = function (elements, key) {
1837
+ var formElement = null;
1838
+ if (!elements || !elements.length) return null;
1839
+ _.each(elements, function (elt) {
1840
+ if (formElement) return;
1841
+ if (elt === key) {
1842
+ formElement = { key: elt };
1843
+ return;
1844
+ }
1845
+ if (_.isString(elt)) return;
1846
+ if (elt.key === key) {
1847
+ formElement = elt;
1848
+ }
1849
+ else if (elt.items) {
1850
+ formElement = getFormElement(elt.items, key);
1851
+ }
1852
+ });
1853
+ return formElement;
1854
+ };
1855
+ var formElement = getFormElement(formObject.form || [], key);
1856
+ var schemaElement = getSchemaKey(formObject.schema.properties, key);
1857
+
1858
+ if (usePreviousValues && formObject.value) {
1859
+ // If values were previously submitted, use them directly if defined
1860
+ value = jsonform.util.getObjKey(formObject.value, applyArrayPath(key, arrayPath));
1861
+ }
1862
+ if (!isSet(value)) {
1863
+ if (formElement && (typeof formElement['value'] !== 'undefined')) {
1864
+ // Extract the definition of the form field associated with
1865
+ // the key as it may override the schema's default value
1866
+ // (note a "null" value overrides a schema default value as well)
1867
+ value = formElement['value'];
1868
+ }
1869
+ else if (schemaElement) {
1870
+ // Simply extract the default value from the schema
1871
+ if (isSet(schemaElement['default'])) {
1872
+ value = schemaElement['default'];
1873
+ }
1874
+ }
1875
+ if (value && value.indexOf('{{values.') !== -1) {
1876
+ // This label wants to use the value of another input field.
1877
+ // Convert that construct into {{getValue(key)}} for
1878
+ // Underscore to call the appropriate function of formData
1879
+ // when template gets called (note calling a function is not
1880
+ // exactly Mustache-friendly but is supported by Underscore).
1881
+ value = value.replace(
1882
+ /\{\{values\.([^\}]+)\}\}/g,
1883
+ '{{getValue("$1")}}');
1884
+ }
1885
+ if (value) {
1886
+ value = _.template(value, valueTemplateSettings)(tpldata);
1887
+ }
1888
+ }
1889
+
1890
+ // TODO: handle on the formElement.options, because user can setup it too.
1891
+ // Apply titleMap if needed
1892
+ if (isSet(value) && formElement && hasOwnProperty(formElement.titleMap, value)) {
1893
+ value = _.template(formElement.titleMap[value], valueTemplateSettings)(tpldata);
1894
+ }
1895
+
1896
+ // Check maximum length of a string
1897
+ if (value && _.isString(value) &&
1898
+ schemaElement && schemaElement.maxLength) {
1899
+ if (value.length > schemaElement.maxLength) {
1900
+ // Truncate value to maximum length, adding continuation dots
1901
+ value = value.substr(0, schemaElement.maxLength - 1) + '…';
1902
+ }
1903
+ }
1904
+
1905
+ if (!isSet(value)) {
1906
+ return null;
1907
+ }
1908
+ else {
1909
+ return value;
1910
+ }
1911
+ };
1912
+
1913
+
1914
+ /**
1915
+ * Represents a node in the form.
1916
+ *
1917
+ * Nodes that have an ID are linked to the corresponding DOM element
1918
+ * when rendered
1919
+ *
1920
+ * Note the form element and the schema elements that gave birth to the
1921
+ * node may be shared among multiple nodes (in the case of arrays).
1922
+ *
1923
+ * @class
1924
+ */
1925
+ var formNode = function () {
1926
+ /**
1927
+ * The node's ID (may not be set)
1928
+ */
1929
+ this.id = null;
1930
+
1931
+ /**
1932
+ * The node's key path (may not be set)
1933
+ */
1934
+ this.key = null;
1935
+
1936
+ /**
1937
+ * DOM element associated witht the form element.
1938
+ *
1939
+ * The DOM element is set when the form element is rendered.
1940
+ */
1941
+ this.el = null;
1942
+
1943
+ /**
1944
+ * Link to the form element that describes the node's layout
1945
+ * (note the form element is shared among nodes in arrays)
1946
+ */
1947
+ this.formElement = null;
1948
+
1949
+ /**
1950
+ * Link to the schema element that describes the node's value constraints
1951
+ * (note the schema element is shared among nodes in arrays)
1952
+ */
1953
+ this.schemaElement = null;
1954
+
1955
+ /**
1956
+ * Pointer to the "view" associated with the node, typically the right
1957
+ * object in jsonform.elementTypes
1958
+ */
1959
+ this.view = null;
1960
+
1961
+ /**
1962
+ * Node's subtree (if one is defined)
1963
+ */
1964
+ this.children = [];
1965
+
1966
+ /**
1967
+ * A pointer to the form tree the node is attached to
1968
+ */
1969
+ this.ownerTree = null;
1970
+
1971
+ /**
1972
+ * A pointer to the parent node of the node in the tree
1973
+ */
1974
+ this.parentNode = null;
1975
+
1976
+ /**
1977
+ * Child template for array-like nodes.
1978
+ *
1979
+ * The child template gets cloned to create new array items.
1980
+ */
1981
+ this.childTemplate = null;
1982
+
1983
+
1984
+ /**
1985
+ * Direct children of array-like containers may use the value of a
1986
+ * specific input field in their subtree as legend. The link to the
1987
+ * legend child is kept here and initialized in computeInitialValues
1988
+ * when a child sets "valueInLegend"
1989
+ */
1990
+ this.legendChild = null;
1991
+
1992
+
1993
+ /**
1994
+ * The path of indexes that lead to the current node when the
1995
+ * form element is not at the root array level.
1996
+ *
1997
+ * Note a form element may well be nested element and still be
1998
+ * at the root array level. That's typically the case for "fieldset"
1999
+ * elements. An array level only gets created when a form element
2000
+ * is of type "array" (or a derivated type such as "tabarray").
2001
+ *
2002
+ * The array path of a form element linked to the foo[2].bar.baz[3].toto
2003
+ * element in the submitted values is [2, 3] for instance.
2004
+ *
2005
+ * The array path is typically used to compute the right ID for input
2006
+ * fields. It is also used to update positions when an array item is
2007
+ * created, moved around or suppressed.
2008
+ *
2009
+ * @type {Array(Number)}
2010
+ */
2011
+ this.arrayPath = [];
2012
+
2013
+ /**
2014
+ * Position of the node in the list of children of its parents
2015
+ */
2016
+ this.childPos = 0;
2017
+ };
2018
+
2019
+
2020
+ /**
2021
+ * Clones a node
2022
+ *
2023
+ * @function
2024
+ * @param {formNode} New parent node to attach the node to
2025
+ * @return {formNode} Cloned node
2026
+ */
2027
+ formNode.prototype.clone = function (parentNode) {
2028
+ var node = new formNode();
2029
+ node.arrayPath = _.clone(this.arrayPath);
2030
+ node.ownerTree = this.ownerTree;
2031
+ node.parentNode = parentNode || this.parentNode;
2032
+ node.formElement = this.formElement;
2033
+ node.schemaElement = this.schemaElement;
2034
+ node.view = this.view;
2035
+ node.children = _.map(this.children, function (child) {
2036
+ return child.clone(node);
2037
+ });
2038
+ if (this.childTemplate) {
2039
+ node.childTemplate = this.childTemplate.clone(node);
2040
+ }
2041
+ return node;
2042
+ };
2043
+
2044
+
2045
+ /**
2046
+ * Returns true if the subtree that starts at the current node
2047
+ * has some non empty value attached to it
2048
+ */
2049
+ formNode.prototype.hasNonDefaultValue = function () {
2050
+
2051
+ // hidden elements don't count because they could make the wrong selectfieldset element active
2052
+ if (this.formElement && this.formElement.type=="hidden") {
2053
+ return false;
2054
+ }
2055
+
2056
+ if (this.value && !this.defaultValue) {
2057
+ return true;
2058
+ }
2059
+ var child = _.find(this.children, function (child) {
2060
+ return child.hasNonDefaultValue();
2061
+ });
2062
+ return !!child;
2063
+ };
2064
+
2065
+
2066
+ /**
2067
+ * Attaches a child node to the current node.
2068
+ *
2069
+ * The child node is appended to the end of the list.
2070
+ *
2071
+ * @function
2072
+ * @param {formNode} node The child node to append
2073
+ * @return {formNode} The inserted node (same as the one given as parameter)
2074
+ */
2075
+ formNode.prototype.appendChild = function (node) {
2076
+ node.parentNode = this;
2077
+ node.childPos = this.children.length;
2078
+ this.children.push(node);
2079
+ return node;
2080
+ };
2081
+
2082
+
2083
+ /**
2084
+ * Removes the last child of the node.
2085
+ *
2086
+ * @function
2087
+ */
2088
+ formNode.prototype.removeChild = function () {
2089
+ var child = this.children[this.children.length-1];
2090
+ if (!child) return;
2091
+
2092
+ // Remove the child from the DOM
2093
+ $(child.el).remove();
2094
+
2095
+ // Remove the child from the array
2096
+ return this.children.pop();
2097
+ };
2098
+
2099
+
2100
+ /**
2101
+ * Moves the user entered values set in the current node's subtree to the
2102
+ * given node's subtree.
2103
+ *
2104
+ * The target node must follow the same structure as the current node
2105
+ * (typically, they should have been generated from the same node template)
2106
+ *
2107
+ * The current node MUST be rendered in the DOM.
2108
+ *
2109
+ * TODO: when current node is not in the DOM, extract values from formNode.value
2110
+ * properties, so that the function be available even when current node is not
2111
+ * in the DOM.
2112
+ *
2113
+ * Moving values around allows to insert/remove array items at arbitrary
2114
+ * positions.
2115
+ *
2116
+ * @function
2117
+ * @param {formNode} node Target node.
2118
+ */
2119
+ formNode.prototype.moveValuesTo = function (node) {
2120
+ var values = this.getFormValues(node.arrayPath);
2121
+ node.resetValues();
2122
+ node.computeInitialValues(values, true);
2123
+ };
2124
+
2125
+
2126
+ /**
2127
+ * Switches nodes user entered values.
2128
+ *
2129
+ * The target node must follow the same structure as the current node
2130
+ * (typically, they should have been generated from the same node template)
2131
+ *
2132
+ * Both nodes MUST be rendered in the DOM.
2133
+ *
2134
+ * TODO: update getFormValues to work even if node is not rendered, using
2135
+ * formNode's "value" property.
2136
+ *
2137
+ * @function
2138
+ * @param {formNode} node Target node
2139
+ */
2140
+ formNode.prototype.switchValuesWith = function (node) {
2141
+ var values = this.getFormValues(node.arrayPath);
2142
+ var nodeValues = node.getFormValues(this.arrayPath);
2143
+ node.resetValues();
2144
+ node.computeInitialValues(values, true);
2145
+ this.resetValues();
2146
+ this.computeInitialValues(nodeValues, true);
2147
+ };
2148
+
2149
+
2150
+ /**
2151
+ * Resets all DOM values in the node's subtree.
2152
+ *
2153
+ * This operation also drops all array item nodes.
2154
+ * Note values are not reset to their default values, they are rather removed!
2155
+ *
2156
+ * @function
2157
+ */
2158
+ formNode.prototype.resetValues = function () {
2159
+ var params = null;
2160
+ var idx = 0;
2161
+
2162
+ // Reset value
2163
+ this.value = null;
2164
+
2165
+ // Propagate the array path from the parent node
2166
+ // (adding the position of the child for nodes that are direct
2167
+ // children of array-like nodes)
2168
+ if (this.parentNode) {
2169
+ this.arrayPath = _.clone(this.parentNode.arrayPath);
2170
+ if (this.parentNode.view && this.parentNode.view.array) {
2171
+ this.arrayPath.push(this.childPos);
2172
+ }
2173
+ }
2174
+ else {
2175
+ this.arrayPath = [];
2176
+ }
2177
+
2178
+ if (this.view && this.view.inputfield) {
2179
+ // Simple input field, extract the value from the origin,
2180
+ // set the target value and reset the origin value
2181
+ params = $(':input', this.el).serializeArray();
2182
+ _.each(params, function (param) {
2183
+ // TODO: check this, there may exist corner cases with this approach
2184
+ // (with multiple checkboxes for instance)
2185
+ $('[name="' + escapeSelector(param.name) + '"]', $(this.el)).val('');
2186
+ }, this);
2187
+ }
2188
+ else if (this.view && this.view.array) {
2189
+ // The current node is an array, drop all children
2190
+ while (this.children.length > 0) {
2191
+ this.removeChild();
2192
+ }
2193
+ }
2194
+
2195
+ // Recurse down the tree
2196
+ _.each(this.children, function (child) {
2197
+ child.resetValues();
2198
+ });
2199
+ };
2200
+
2201
+
2202
+ /**
2203
+ * Sets the child template node for the current node.
2204
+ *
2205
+ * The child template node is used to create additional children
2206
+ * in an array-like form element. The template is never rendered.
2207
+ *
2208
+ * @function
2209
+ * @param {formNode} node The child template node to set
2210
+ */
2211
+ formNode.prototype.setChildTemplate = function (node) {
2212
+ this.childTemplate = node;
2213
+ node.parentNode = this;
2214
+ };
2215
+
2216
+
2217
+ /**
2218
+ * Recursively sets values to all nodes of the current subtree
2219
+ * based on previously submitted values, or based on default
2220
+ * values when the submitted values are not enough
2221
+ *
2222
+ * The function should be called once in the lifetime of a node
2223
+ * in the tree. It expects its parent's arrayPath to be up to date.
2224
+ *
2225
+ * Three cases may arise:
2226
+ * 1. if the form element is a simple input field, the value is
2227
+ * extracted from previously submitted values of from default values
2228
+ * defined in the schema.
2229
+ * 2. if the form element is an array-like node, the child template
2230
+ * is used to create as many children as possible (and at least one).
2231
+ * 3. the function simply recurses down the node's subtree otherwise
2232
+ * (this happens when the form element is a fieldset-like element).
2233
+ *
2234
+ * @function
2235
+ * @param {Object} values Previously submitted values for the form
2236
+ * @param {Boolean} ignoreDefaultValues Ignore default values defined in the
2237
+ * schema when set.
2238
+ */
2239
+ formNode.prototype.computeInitialValues = function (values, ignoreDefaultValues) {
2240
+ var self = this;
2241
+ var node = null;
2242
+ var nbChildren = 1;
2243
+ var i = 0;
2244
+ var formData = this.ownerTree.formDesc.tpldata || {};
2245
+
2246
+ // Propagate the array path from the parent node
2247
+ // (adding the position of the child for nodes that are direct
2248
+ // children of array-like nodes)
2249
+ if (this.parentNode) {
2250
+ this.arrayPath = _.clone(this.parentNode.arrayPath);
2251
+ if (this.parentNode.view && this.parentNode.view.array) {
2252
+ this.arrayPath.push(this.childPos);
2253
+ }
2254
+ }
2255
+ else {
2256
+ this.arrayPath = [];
2257
+ }
2258
+
2259
+ // Prepare special data param "idx" for templated values
2260
+ // (is is the index of the child in its wrapping array, starting
2261
+ // at 1 since that's more human-friendly than a zero-based index)
2262
+ formData.idx = (this.arrayPath.length > 0) ?
2263
+ this.arrayPath[this.arrayPath.length-1] + 1 :
2264
+ this.childPos + 1;
2265
+
2266
+ // Prepare special data param "value" for templated values
2267
+ formData.value = '';
2268
+
2269
+ // Prepare special function to compute the value of another field
2270
+ formData.getValue = function (key) {
2271
+ if (!values) {
2272
+ return '';
2273
+ }
2274
+ var returnValue = values;
2275
+ var listKey = key.split('[].');
2276
+ var i;
2277
+ for (i = 0; i < listKey.length - 1; i++) {
2278
+ returnValue = returnValue[listKey[i]][self.arrayPath[i]];
2279
+ }
2280
+ return returnValue[listKey[i]];
2281
+ };
2282
+
2283
+ if (this.formElement) {
2284
+ // Compute the ID of the field (if needed)
2285
+ if (this.formElement.id) {
2286
+ this.id = applyArrayPath(this.formElement.id, this.arrayPath);
2287
+ }
2288
+ else if (this.view && this.view.array) {
2289
+ this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
2290
+ '-elt-counter-' + _.uniqueId();
2291
+ }
2292
+ else if (this.parentNode && this.parentNode.view &&
2293
+ this.parentNode.view.array) {
2294
+ // Array items need an array to associate the right DOM element
2295
+ // to the form node when the parent is rendered.
2296
+ this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
2297
+ '-elt-counter-' + _.uniqueId();
2298
+ }
2299
+ else if ((this.formElement.type === 'button') ||
2300
+ (this.formElement.type === 'selectfieldset') ||
2301
+ (this.formElement.type === 'question') ||
2302
+ (this.formElement.type === 'buttonquestion')) {
2303
+ // Buttons do need an id for "onClick" purpose
2304
+ this.id = escapeSelector(this.ownerTree.formDesc.prefix) +
2305
+ '-elt-counter-' + _.uniqueId();
2306
+ }
2307
+
2308
+ // Compute the actual key (the form element's key is index-free,
2309
+ // i.e. it looks like foo[].bar.baz[].truc, so we need to apply
2310
+ // the array path of the node to get foo[4].bar.baz[2].truc)
2311
+ if (this.formElement.key) {
2312
+ this.key = applyArrayPath(this.formElement.key, this.arrayPath);
2313
+ this.keydash = slugify(this.key.replace(/\./g, '---'));
2314
+ }
2315
+
2316
+ // Same idea for the field's name
2317
+ this.name = applyArrayPath(this.formElement.name, this.arrayPath);
2318
+
2319
+ // Consider that label values are template values and apply the
2320
+ // form's data appropriately (note we also apply the array path
2321
+ // although that probably doesn't make much sense for labels...)
2322
+ _.each([
2323
+ 'title',
2324
+ 'legend',
2325
+ 'description',
2326
+ 'append',
2327
+ 'prepend',
2328
+ 'inlinetitle',
2329
+ 'helpvalue',
2330
+ 'value',
2331
+ 'disabled',
2332
+ 'placeholder',
2333
+ 'readOnly'
2334
+ ], function (prop) {
2335
+ if (_.isString(this.formElement[prop])) {
2336
+ if (this.formElement[prop].indexOf('{{values.') !== -1) {
2337
+ // This label wants to use the value of another input field.
2338
+ // Convert that construct into {{jsonform.getValue(key)}} for
2339
+ // Underscore to call the appropriate function of formData
2340
+ // when template gets called (note calling a function is not
2341
+ // exactly Mustache-friendly but is supported by Underscore).
2342
+ this[prop] = this.formElement[prop].replace(
2343
+ /\{\{values\.([^\}]+)\}\}/g,
2344
+ '{{getValue("$1")}}');
2345
+ }
2346
+ else {
2347
+ // Note applying the array path probably doesn't make any sense,
2348
+ // but some geek might want to have a label "foo[].bar[].baz",
2349
+ // with the [] replaced by the appropriate array path.
2350
+ this[prop] = applyArrayPath(this.formElement[prop], this.arrayPath);
2351
+ }
2352
+ if (this[prop]) {
2353
+ this[prop] = _.template(this[prop], valueTemplateSettings)(formData);
2354
+ }
2355
+ }
2356
+ else {
2357
+ this[prop] = this.formElement[prop];
2358
+ }
2359
+ }, this);
2360
+
2361
+ // Apply templating to options created with "titleMap" as well
2362
+ if (this.formElement.options) {
2363
+ this.options = _.map(this.formElement.options, function (option) {
2364
+ var title = null;
2365
+ if (_.isObject(option) && option.title) {
2366
+ // See a few lines above for more details about templating
2367
+ // preparation here.
2368
+ if (option.title.indexOf('{{values.') !== -1) {
2369
+ title = option.title.replace(
2370
+ /\{\{values\.([^\}]+)\}\}/g,
2371
+ '{{getValue("$1")}}');
2372
+ }
2373
+ else {
2374
+ title = applyArrayPath(option.title, self.arrayPath);
2375
+ }
2376
+ return _.extend({}, option, {
2377
+ value: (isSet(option.value) ? option.value : ''),
2378
+ title: _.template(title, valueTemplateSettings)(formData)
2379
+ });
2380
+ }
2381
+ else {
2382
+ return option;
2383
+ }
2384
+ });
2385
+ }
2386
+ }
2387
+
2388
+ if (this.view && this.view.inputfield && this.schemaElement) {
2389
+ // Case 1: simple input field
2390
+ if (values) {
2391
+ // Form has already been submitted, use former value if defined.
2392
+ // Note we won't set the field to its default value otherwise
2393
+ // (since the user has already rejected it)
2394
+ if (isSet(jsonform.util.getObjKey(values, this.key))) {
2395
+ this.value = jsonform.util.getObjKey(values, this.key);
2396
+ } else if (isSet(this.schemaElement['default'])) {
2397
+ // the value is not provided in the values section but the
2398
+ // default is set in the schemaElement (which we have)
2399
+ this.value = this.schemaElement['default']
2400
+ // We only apply a template if it's a string
2401
+ if (typeof this.value === 'string') {
2402
+ this.value = _.template(this.value, valueTemplateSettings)(formData);
2403
+ }
2404
+
2405
+ }
2406
+ }
2407
+ else if (!ignoreDefaultValues) {
2408
+ // No previously submitted form result, use default value
2409
+ // defined in the schema if it's available and not already
2410
+ // defined in the form element
2411
+ if (!isSet(this.value) && isSet(this.schemaElement['default'])) {
2412
+ this.value = this.schemaElement['default'];
2413
+ if (_.isString(this.value)) {
2414
+ if (this.value.indexOf('{{values.') !== -1) {
2415
+ // This label wants to use the value of another input field.
2416
+ // Convert that construct into {{jsonform.getValue(key)}} for
2417
+ // Underscore to call the appropriate function of formData
2418
+ // when template gets called (note calling a function is not
2419
+ // exactly Mustache-friendly but is supported by Underscore).
2420
+ this.value = this.value.replace(
2421
+ /\{\{values\.([^\}]+)\}\}/g,
2422
+ '{{getValue("$1")}}');
2423
+ }
2424
+ else {
2425
+ // Note applying the array path probably doesn't make any sense,
2426
+ // but some geek might want to have a label "foo[].bar[].baz",
2427
+ // with the [] replaced by the appropriate array path.
2428
+ this.value = applyArrayPath(this.value, this.arrayPath);
2429
+ }
2430
+ if (this.value) {
2431
+ this.value = _.template(this.value, valueTemplateSettings)(formData);
2432
+ }
2433
+ }
2434
+ this.defaultValue = true;
2435
+ }
2436
+ }
2437
+ }
2438
+ else if (this.view && this.view.array) {
2439
+ // Case 2: array-like node
2440
+ nbChildren = 0;
2441
+ if (values) {
2442
+ nbChildren = this.getPreviousNumberOfItems(values, this.arrayPath);
2443
+ }
2444
+ // TODO: use default values at the array level when form has not been
2445
+ // submitted before. Note it's not that easy because each value may
2446
+ // be a complex structure that needs to be pushed down the subtree.
2447
+ // The easiest way is probably to generate a "values" object and
2448
+ // compute initial values from that object
2449
+ /*
2450
+ else if (this.schemaElement['default']) {
2451
+ nbChildren = this.schemaElement['default'].length;
2452
+ }
2453
+ */
2454
+ else if (nbChildren === 0) {
2455
+ // If form has already been submitted with no children, the array
2456
+ // needs to be rendered without children. If there are no previously
2457
+ // submitted values, the array gets rendered with one empty item as
2458
+ // it's more natural from a user experience perspective. That item can
2459
+ // be removed with a click on the "-" button.
2460
+ nbChildren = 1;
2461
+ }
2462
+ for (i = 0; i < nbChildren; i++) {
2463
+ this.appendChild(this.childTemplate.clone());
2464
+ }
2465
+ }
2466
+
2467
+ // Case 3 and in any case: recurse through the list of children
2468
+ _.each(this.children, function (child) {
2469
+ child.computeInitialValues(values, ignoreDefaultValues);
2470
+ });
2471
+
2472
+ // If the node's value is to be used as legend for its "container"
2473
+ // (typically the array the node belongs to), ensure that the container
2474
+ // has a direct link to the node for the corresponding tab.
2475
+ if (this.formElement && this.formElement.valueInLegend) {
2476
+ node = this;
2477
+ while (node) {
2478
+ if (node.parentNode &&
2479
+ node.parentNode.view &&
2480
+ node.parentNode.view.array) {
2481
+ node.legendChild = this;
2482
+ if (node.formElement && node.formElement.legend) {
2483
+ node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);
2484
+ formData.idx = (node.arrayPath.length > 0) ?
2485
+ node.arrayPath[node.arrayPath.length-1] + 1 :
2486
+ node.childPos + 1;
2487
+ formData.value = isSet(this.value) ? this.value : '';
2488
+ node.legend = _.template(node.legend, valueTemplateSettings)(formData);
2489
+ break;
2490
+ }
2491
+ }
2492
+ node = node.parentNode;
2493
+ }
2494
+ }
2495
+ };
2496
+
2497
+
2498
+ /**
2499
+ * Returns the number of items that the array node should have based on
2500
+ * previously submitted values.
2501
+ *
2502
+ * The whole difficulty is that values may be hidden deep in the subtree
2503
+ * of the node and may actually target different arrays in the JSON schema.
2504
+ *
2505
+ * @function
2506
+ * @param {Object} values Previously submitted values
2507
+ * @param {Array(Number)} arrayPath the array path we're interested in
2508
+ * @return {Number} The number of items in the array
2509
+ */
2510
+ formNode.prototype.getPreviousNumberOfItems = function (values, arrayPath) {
2511
+ var key = null;
2512
+ var arrayValue = null;
2513
+ var childNumbers = null;
2514
+ var idx = 0;
2515
+
2516
+ if (!values) {
2517
+ // No previously submitted values, no need to go any further
2518
+ return 0;
2519
+ }
2520
+
2521
+ if (this.view.inputfield && this.schemaElement) {
2522
+ // Case 1: node is a simple input field that links to a key in the schema.
2523
+ // The schema key looks typically like:
2524
+ // foo.bar[].baz.toto[].truc[].bidule
2525
+ // The goal is to apply the array path and truncate the key to the last
2526
+ // array we're interested in, e.g. with an arrayPath [4, 2]:
2527
+ // foo.bar[4].baz.toto[2]
2528
+ key = truncateToArrayDepth(this.formElement.key, arrayPath.length);
2529
+ key = applyArrayPath(key, arrayPath);
2530
+ arrayValue = jsonform.util.getObjKey(values, key);
2531
+ if (!arrayValue) {
2532
+ // No key? That means this field had been left empty
2533
+ // in previous submit
2534
+ return 0;
2535
+ }
2536
+ childNumbers = _.map(this.children, function (child) {
2537
+ return child.getPreviousNumberOfItems(values, arrayPath);
2538
+ });
2539
+ return _.max([_.max(childNumbers) || 0, arrayValue.length]);
2540
+ }
2541
+ else if (this.view.array) {
2542
+ // Case 2: node is an array-like node, look for input fields
2543
+ // in its child template
2544
+ return this.childTemplate.getPreviousNumberOfItems(values, arrayPath);
2545
+ }
2546
+ else {
2547
+ // Case 3: node is a leaf or a container,
2548
+ // recurse through the list of children and return the maximum
2549
+ // number of items found in each subtree
2550
+ childNumbers = _.map(this.children, function (child) {
2551
+ return child.getPreviousNumberOfItems(values, arrayPath);
2552
+ });
2553
+ return _.max(childNumbers) || 0;
2554
+ }
2555
+ };
2556
+
2557
+
2558
+ /**
2559
+ * Returns the structured object that corresponds to the form values entered
2560
+ * by the user for the node's subtree.
2561
+ *
2562
+ * The returned object follows the structure of the JSON schema that gave
2563
+ * birth to the form.
2564
+ *
2565
+ * Obviously, the node must have been rendered before that function may
2566
+ * be called.
2567
+ *
2568
+ * @function
2569
+ * @param {Array(Number)} updateArrayPath Array path to use to pretend that
2570
+ * the entered values were actually entered for another item in an array
2571
+ * (this is used to move values around when an item is inserted/removed/moved
2572
+ * in an array)
2573
+ * @return {Object} The object that follows the data schema and matches the
2574
+ * values entered by the user.
2575
+ */
2576
+ formNode.prototype.getFormValues = function (updateArrayPath) {
2577
+ // The values object that will be returned
2578
+ var values = {};
2579
+
2580
+ if (!this.el) {
2581
+ throw new Error('formNode.getFormValues can only be called on nodes that are associated with a DOM element in the tree');
2582
+ }
2583
+
2584
+ // Form fields values
2585
+ var formArray = $(':input', this.el).serializeArray();
2586
+
2587
+ // Set values to false for unset checkboxes and radio buttons
2588
+ // because serializeArray() ignores them
2589
+ formArray = formArray.concat(
2590
+ $(':input[type=checkbox]:not(:disabled):not(:checked)', this.el).map( function() {
2591
+ return {"name": this.name, "value": this.checked}
2592
+ }).get()
2593
+ );
2594
+
2595
+ if (updateArrayPath) {
2596
+ _.each(formArray, function (param) {
2597
+ param.name = applyArrayPath(param.name, updateArrayPath);
2598
+ });
2599
+ }
2600
+
2601
+ // The underlying data schema
2602
+ var formSchema = this.ownerTree.formDesc.schema;
2603
+
2604
+ for (var i = 0; i < formArray.length; i++) {
2605
+ // Retrieve the key definition from the data schema
2606
+ var name = formArray[i].name;
2607
+ var eltSchema = getSchemaKey(formSchema.properties, name);
2608
+ var arrayMatch = null;
2609
+ var cval = null;
2610
+
2611
+ // Skip the input field if it's not part of the schema
2612
+ if (!eltSchema) continue;
2613
+
2614
+ // Handle multiple checkboxes separately as the idea is to generate
2615
+ // an array that contains the list of enumeration items that the user
2616
+ // selected.
2617
+ if (eltSchema._jsonform_checkboxes_as_array) {
2618
+ arrayMatch = name.match(/\[([0-9]*)\]$/);
2619
+ if (arrayMatch) {
2620
+ name = name.replace(/\[([0-9]*)\]$/, '');
2621
+ cval = jsonform.util.getObjKey(values, name) || [];
2622
+ if (formArray[i].value === '1') {
2623
+ // Value selected, push the corresponding enumeration item
2624
+ // to the data result
2625
+ cval.push(eltSchema['enum'][parseInt(arrayMatch[1],10)]);
2626
+ }
2627
+ jsonform.util.setObjKey(values, name, cval);
2628
+ continue;
2629
+ }
2630
+ }
2631
+
2632
+ // Type casting
2633
+ if (eltSchema.type === 'boolean') {
2634
+ if (formArray[i].value === '0') {
2635
+ formArray[i].value = false;
2636
+ } else {
2637
+ formArray[i].value = !!formArray[i].value;
2638
+ }
2639
+ }
2640
+ if ((eltSchema.type === 'number') ||
2641
+ (eltSchema.type === 'integer')) {
2642
+ if (_.isString(formArray[i].value)) {
2643
+ if (!formArray[i].value.length) {
2644
+ formArray[i].value = null;
2645
+ } else if (!isNaN(Number(formArray[i].value))) {
2646
+ formArray[i].value = Number(formArray[i].value);
2647
+ }
2648
+ }
2649
+ }
2650
+ if ((eltSchema.type === 'string') &&
2651
+ (formArray[i].value === '') &&
2652
+ !eltSchema._jsonform_allowEmpty) {
2653
+ formArray[i].value=null;
2654
+ }
2655
+ if ((eltSchema.type === 'object') &&
2656
+ _.isString(formArray[i].value) &&
2657
+ (formArray[i].value.substring(0,1) === '{')) {
2658
+ try {
2659
+ formArray[i].value = JSON.parse(formArray[i].value);
2660
+ } catch (e) {
2661
+ formArray[i].value = {};
2662
+ }
2663
+ }
2664
+ //TODO: is this due to a serialization bug?
2665
+ if ((eltSchema.type === 'object') &&
2666
+ (formArray[i].value === 'null' || formArray[i].value === '')) {
2667
+ formArray[i].value = null;
2668
+ }
2669
+
2670
+ if (formArray[i].name && (formArray[i].value !== null)) {
2671
+ jsonform.util.setObjKey(values, formArray[i].name, formArray[i].value);
2672
+ }
2673
+ }
2674
+ return values;
2675
+ };
2676
+
2677
+
2678
+
2679
+ /**
2680
+ * Renders the node.
2681
+ *
2682
+ * Rendering is done in three steps: HTML generation, DOM element creation
2683
+ * and insertion, and an enhance step to bind event handlers.
2684
+ *
2685
+ * @function
2686
+ * @param {Node} el The DOM element where the node is to be rendered. The
2687
+ * node is inserted at the right position based on its "childPos" property.
2688
+ */
2689
+ formNode.prototype.render = function (el) {
2690
+ var html = this.generate();
2691
+ this.setContent(html, el);
2692
+ this.enhance();
2693
+ };
2694
+
2695
+
2696
+ /**
2697
+ * Inserts/Updates the HTML content of the node in the DOM.
2698
+ *
2699
+ * If the HTML is an update, the new HTML content replaces the old one.
2700
+ * The new HTML content is not moved around in the DOM in particular.
2701
+ *
2702
+ * The HTML is inserted at the right position in its parent's DOM subtree
2703
+ * otherwise (well, provided there are enough children, but that should always
2704
+ * be the case).
2705
+ *
2706
+ * @function
2707
+ * @param {string} html The HTML content to render
2708
+ * @param {Node} parentEl The DOM element that is to contain the DOM node.
2709
+ * This parameter is optional (the node's parent is used otherwise) and
2710
+ * is ignored if the node to render is already in the DOM tree.
2711
+ */
2712
+ formNode.prototype.setContent = function (html, parentEl) {
2713
+ var node = $(html);
2714
+ var parentNode = parentEl ||
2715
+ (this.parentNode ? this.parentNode.el : this.ownerTree.domRoot);
2716
+ var nextSibling = null;
2717
+
2718
+ if (this.el) {
2719
+ // Replace the contents of the DOM element if the node is already in the tree
2720
+ $(this.el).replaceWith(node);
2721
+ }
2722
+ else {
2723
+ // Insert the node in the DOM if it's not already there
2724
+ nextSibling = $(parentNode).children().get(this.childPos);
2725
+ if (nextSibling) {
2726
+ $(nextSibling).before(node);
2727
+ }
2728
+ else {
2729
+ $(parentNode).append(node);
2730
+ }
2731
+ }
2732
+
2733
+ // Save the link between the form node and the generated HTML
2734
+ this.el = node;
2735
+
2736
+ // Update the node's subtree, extracting DOM elements that match the nodes
2737
+ // from the generated HTML
2738
+ this.updateElement(this.el);
2739
+ };
2740
+
2741
+
2742
+ /**
2743
+ * Updates the DOM element associated with the node.
2744
+ *
2745
+ * Only nodes that have ID are directly associated with a DOM element.
2746
+ *
2747
+ * @function
2748
+ */
2749
+ formNode.prototype.updateElement = function (domNode) {
2750
+ if (this.id) {
2751
+ this.el = $('#' + escapeSelector(this.id), domNode).get(0);
2752
+ if (this.view && this.view.getElement) {
2753
+ this.el = this.view.getElement(this.el);
2754
+ }
2755
+ if ((this.fieldtemplate !== false) &&
2756
+ this.view && this.view.fieldtemplate) {
2757
+ // The field template wraps the element two or three level deep
2758
+ // in the DOM tree, depending on whether there is anything prepended
2759
+ // or appended to the input field
2760
+ this.el = $(this.el).parent().parent();
2761
+ if (this.prepend || this.prepend) {
2762
+ this.el = this.el.parent();
2763
+ }
2764
+ this.el = this.el.get(0);
2765
+ }
2766
+ if (this.parentNode && this.parentNode.view &&
2767
+ this.parentNode.view.childTemplate) {
2768
+ // TODO: the child template may introduce more than one level,
2769
+ // so the number of levels introduced should rather be exposed
2770
+ // somehow in jsonform.fieldtemplate.
2771
+ this.el = $(this.el).parent().get(0);
2772
+ }
2773
+ }
2774
+
2775
+ for (const k in this.children) {
2776
+ if (this.children.hasOwnProperty(k) == false) {
2777
+ continue;
2778
+ }
2779
+ this.children[k].updateElement(this.el || domNode);
2780
+ }
2781
+ };
2782
+
2783
+
2784
+ /**
2785
+ * Generates the view's HTML content for the underlying model.
2786
+ *
2787
+ * @function
2788
+ */
2789
+ formNode.prototype.generate = function () {
2790
+ var data = {
2791
+ id: this.id,
2792
+ keydash: this.keydash,
2793
+ elt: this.formElement,
2794
+ schema: this.schemaElement,
2795
+ node: this,
2796
+ value: isSet(this.value) ? this.value : '',
2797
+ escape: escapeHTML
2798
+ };
2799
+ var template = null;
2800
+ var html = '';
2801
+
2802
+ // Complete the data context if needed
2803
+ if (this.ownerTree.formDesc.onBeforeRender) {
2804
+ this.ownerTree.formDesc.onBeforeRender(data, this);
2805
+ }
2806
+ if (this.view.onBeforeRender) {
2807
+ this.view.onBeforeRender(data, this);
2808
+ }
2809
+
2810
+ // Use the template that 'onBeforeRender' may have set,
2811
+ // falling back to that of the form element otherwise
2812
+ if (this.template) {
2813
+ template = this.template;
2814
+ }
2815
+ else if (this.formElement && this.formElement.template) {
2816
+ template = this.formElement.template;
2817
+ }
2818
+ else {
2819
+ template = this.view.template;
2820
+ }
2821
+
2822
+ // Wrap the view template in the generic field template
2823
+ // (note the strict equality to 'false', needed as we fallback
2824
+ // to the view's setting otherwise)
2825
+ if ((this.fieldtemplate !== false) &&
2826
+ (this.fieldtemplate || this.view.fieldtemplate)) {
2827
+ template = jsonform.fieldTemplate(template);
2828
+ }
2829
+
2830
+ // Wrap the content in the child template of its parent if necessary.
2831
+ if (this.parentNode && this.parentNode.view &&
2832
+ this.parentNode.view.childTemplate) {
2833
+ // only allow drag of children if default or enabled
2834
+ template = this.parentNode.view.childTemplate(template, (!isSet(this.parentNode.formElement.draggable) ? true : this.parentNode.formElement.draggable));
2835
+ }
2836
+
2837
+ // Prepare the HTML of the children
2838
+ var childrenhtml = '';
2839
+ _.each(this.children, function (child) {
2840
+ childrenhtml += child.generate();
2841
+ });
2842
+ data.children = childrenhtml;
2843
+
2844
+ data.fieldHtmlClass = '';
2845
+ if (this.ownerTree &&
2846
+ this.ownerTree.formDesc &&
2847
+ this.ownerTree.formDesc.params &&
2848
+ this.ownerTree.formDesc.params.fieldHtmlClass) {
2849
+ data.fieldHtmlClass = this.ownerTree.formDesc.params.fieldHtmlClass;
2850
+ }
2851
+ if (this.formElement &&
2852
+ (typeof this.formElement.fieldHtmlClass !== 'undefined')) {
2853
+ data.fieldHtmlClass = this.formElement.fieldHtmlClass;
2854
+ }
2855
+
2856
+ // Apply the HTML template
2857
+ html = _.template(template, fieldTemplateSettings)(data);
2858
+ return html;
2859
+ };
2860
+
2861
+
2862
+ /**
2863
+ * Enhances the view with additional logic, binding event handlers
2864
+ * in particular.
2865
+ *
2866
+ * The function also runs the "insert" event handler of the view and
2867
+ * form element if they exist (starting with that of the view)
2868
+ *
2869
+ * @function
2870
+ */
2871
+ formNode.prototype.enhance = function () {
2872
+ var node = this;
2873
+ var handlers = null;
2874
+ var handler = null;
2875
+ var formData = _.clone(this.ownerTree.formDesc.tpldata) || {};
2876
+
2877
+ if (this.formElement) {
2878
+ // Check the view associated with the node as it may define an "onInsert"
2879
+ // event handler to be run right away
2880
+ if (this.view.onInsert) {
2881
+ this.view.onInsert({ target: $(this.el) }, this);
2882
+ }
2883
+
2884
+ handlers = this.handlers || this.formElement.handlers;
2885
+
2886
+ // Trigger the "insert" event handler
2887
+ handler = this.onInsert || this.formElement.onInsert;
2888
+ if (handler) {
2889
+ handler({ target: $(this.el) }, this);
2890
+ }
2891
+ if (handlers) {
2892
+ _.each(handlers, function (handler, onevent) {
2893
+ if (onevent === 'insert') {
2894
+ handler({ target: $(this.el) }, this);
2895
+ }
2896
+ }, this);
2897
+ }
2898
+
2899
+ // No way to register event handlers if the DOM element is unknown
2900
+ // TODO: find some way to register event handlers even when this.el is not set.
2901
+ if (this.el) {
2902
+
2903
+ // Register specific event handlers
2904
+ // TODO: Add support for other event handlers
2905
+ if (this.onChange)
2906
+ $(this.el).bind('change', function(evt) { node.onChange(evt, node); });
2907
+ if (this.view.onChange)
2908
+ $(this.el).bind('change', function(evt) { node.view.onChange(evt, node); });
2909
+ if (this.formElement.onChange)
2910
+ $(this.el).bind('change', function(evt) { node.formElement.onChange(evt, node); });
2911
+
2912
+ if (this.onInput)
2913
+ $(this.el).bind('input', function(evt) { node.onInput(evt, node); });
2914
+ if (this.view.onInput)
2915
+ $(this.el).bind('input', function(evt) { node.view.onInput(evt, node); });
2916
+ if (this.formElement.onInput)
2917
+ $(this.el).bind('input', function(evt) { node.formElement.onInput(evt, node); });
2918
+
2919
+ if (this.onClick)
2920
+ $(this.el).bind('click', function(evt) { node.onClick(evt, node); });
2921
+ if (this.view.onClick)
2922
+ $(this.el).bind('click', function(evt) { node.view.onClick(evt, node); });
2923
+ if (this.formElement.onClick)
2924
+ $(this.el).bind('click', function(evt) { node.formElement.onClick(evt, node); });
2925
+
2926
+ if (this.onKeyUp)
2927
+ $(this.el).bind('keyup', function(evt) { node.onKeyUp(evt, node); });
2928
+ if (this.view.onKeyUp)
2929
+ $(this.el).bind('keyup', function(evt) { node.view.onKeyUp(evt, node); });
2930
+ if (this.formElement.onKeyUp)
2931
+ $(this.el).bind('keyup', function(evt) { node.formElement.onKeyUp(evt, node); });
2932
+
2933
+ if (handlers) {
2934
+ _.each(handlers, function (handler, onevent) {
2935
+ if (onevent !== 'insert') {
2936
+ $(this.el).bind(onevent, function(evt) { handler(evt, node); });
2937
+ }
2938
+ }, this);
2939
+ }
2940
+ }
2941
+
2942
+ // Auto-update legend based on the input field that's associated with it
2943
+ if (this.legendChild && this.legendChild.formElement) {
2944
+ var onChangeHandler = function (evt) {
2945
+ if (node.formElement && node.formElement.legend && node.parentNode) {
2946
+ node.legend = applyArrayPath(node.formElement.legend, node.arrayPath);
2947
+ formData.idx = (node.arrayPath.length > 0) ?
2948
+ node.arrayPath[node.arrayPath.length - 1] + 1 :
2949
+ node.childPos + 1;
2950
+ formData.value = $(evt.target).val();
2951
+ node.legend = _.template(node.legend, valueTemplateSettings)(formData);
2952
+ $(node.parentNode.el).trigger('legendUpdated');
2953
+ }
2954
+ };
2955
+ $(this.legendChild.el).bind('change', onChangeHandler);
2956
+ $(this.legendChild.el).bind('keyup', onChangeHandler);
2957
+ }
2958
+ }
2959
+
2960
+ // Recurse down the tree to enhance children
2961
+ _.each(this.children, function (child) {
2962
+ child.enhance();
2963
+ });
2964
+ };
2965
+
2966
+
2967
+
2968
+ /**
2969
+ * Inserts an item in the array at the requested position and renders the item.
2970
+ *
2971
+ * @function
2972
+ * @param {Number} idx Insertion index
2973
+ */
2974
+ formNode.prototype.insertArrayItem = function (idx, domElement) {
2975
+ var i = 0;
2976
+
2977
+ // Insert element at the end of the array if index is not given
2978
+ if (idx === undefined) {
2979
+ idx = this.children.length;
2980
+ }
2981
+
2982
+ // Create the additional array item at the end of the list,
2983
+ // using the item template created when tree was initialized
2984
+ // (the call to resetValues ensures that 'arrayPath' is correctly set)
2985
+ var child = this.childTemplate.clone();
2986
+ this.appendChild(child);
2987
+ child.resetValues();
2988
+
2989
+ // To create a blank array item at the requested position,
2990
+ // shift values down starting at the requested position
2991
+ // one to insert (note we start with the end of the array on purpose)
2992
+ for (i = this.children.length-2; i >= idx; i--) {
2993
+ this.children[i].moveValuesTo(this.children[i+1]);
2994
+ }
2995
+
2996
+ // Initialize the blank node we've created with default values
2997
+ this.children[idx].resetValues();
2998
+ this.children[idx].computeInitialValues();
2999
+
3000
+ // Re-render all children that have changed
3001
+ for (i = idx; i < this.children.length; i++) {
3002
+ this.children[i].render(domElement);
3003
+ }
3004
+ };
3005
+
3006
+
3007
+ /**
3008
+ * Remove an item from an array
3009
+ *
3010
+ * @function
3011
+ * @param {Number} idx The index number of the item to remove
3012
+ */
3013
+ formNode.prototype.deleteArrayItem = function (idx) {
3014
+ var i = 0;
3015
+ var child = null;
3016
+
3017
+ // Delete last item if no index is given
3018
+ if (idx === undefined) {
3019
+ idx = this.children.length - 1;
3020
+ }
3021
+
3022
+ // Move values up in the array
3023
+ for (i = idx; i < this.children.length-1; i++) {
3024
+ this.children[i+1].moveValuesTo(this.children[i]);
3025
+ this.children[i].render();
3026
+ }
3027
+
3028
+ // Remove the last array item from the DOM tree and from the form tree
3029
+ this.removeChild();
3030
+ };
3031
+
3032
+ /**
3033
+ * Returns the minimum/maximum number of items that an array field
3034
+ * is allowed to have according to the schema definition of the fields
3035
+ * it contains.
3036
+ *
3037
+ * The function parses the schema definitions of the array items that
3038
+ * compose the current "array" node and returns the minimum value of
3039
+ * "maxItems" it encounters as the maximum number of items, and the
3040
+ * maximum value of "minItems" as the minimum number of items.
3041
+ *
3042
+ * The function reports a -1 for either of the boundaries if the schema
3043
+ * does not put any constraint on the number of elements the current
3044
+ * array may have of if the current node is not an array.
3045
+ *
3046
+ * Note that array boundaries should be defined in the JSON Schema using
3047
+ * "minItems" and "maxItems". The code also supports "minLength" and
3048
+ * "maxLength" as a fallback, mostly because it used to by mistake (see #22)
3049
+ * and because other people could make the same mistake.
3050
+ *
3051
+ * @function
3052
+ * @return {Object} An object with properties "minItems" and "maxItems"
3053
+ * that reports the corresponding number of items that the array may
3054
+ * have (value is -1 when there is no constraint for that boundary)
3055
+ */
3056
+ formNode.prototype.getArrayBoundaries = function () {
3057
+ var boundaries = {
3058
+ minItems: -1,
3059
+ maxItems: -1
3060
+ };
3061
+ if (!this.view || !this.view.array) return boundaries;
3062
+
3063
+ var getNodeBoundaries = function (node, initialNode) {
3064
+ var schemaKey = null;
3065
+ var arrayKey = null;
3066
+ var boundaries = {
3067
+ minItems: -1,
3068
+ maxItems: -1
3069
+ };
3070
+ initialNode = initialNode || node;
3071
+
3072
+ if (node.view && node.view.array && (node !== initialNode)) {
3073
+ // New array level not linked to an array in the schema,
3074
+ // so no size constraints
3075
+ return boundaries;
3076
+ }
3077
+
3078
+ if (node.key) {
3079
+ // Note the conversion to target the actual array definition in the
3080
+ // schema where minItems/maxItems may be defined. If we're still looking
3081
+ // at the initial node, the goal is to convert from:
3082
+ // foo[0].bar[3].baz to foo[].bar[].baz
3083
+ // If we're not looking at the initial node, the goal is to look at the
3084
+ // closest array parent:
3085
+ // foo[0].bar[3].baz to foo[].bar
3086
+ arrayKey = node.key.replace(/\[[0-9]+\]/g, '[]');
3087
+ if (node !== initialNode) {
3088
+ arrayKey = arrayKey.replace(/\[\][^\[\]]*$/, '');
3089
+ }
3090
+ schemaKey = getSchemaKey(
3091
+ node.ownerTree.formDesc.schema.properties,
3092
+ arrayKey
3093
+ );
3094
+ if (!schemaKey) return boundaries;
3095
+ return {
3096
+ minItems: schemaKey.minItems || schemaKey.minLength || -1,
3097
+ maxItems: schemaKey.maxItems || schemaKey.maxLength || -1
3098
+ };
3099
+ }
3100
+ else {
3101
+ _.each(node.children, function (child) {
3102
+ var subBoundaries = getNodeBoundaries(child, initialNode);
3103
+ if (subBoundaries.minItems !== -1) {
3104
+ if (boundaries.minItems !== -1) {
3105
+ boundaries.minItems = Math.max(
3106
+ boundaries.minItems,
3107
+ subBoundaries.minItems
3108
+ );
3109
+ }
3110
+ else {
3111
+ boundaries.minItems = subBoundaries.minItems;
3112
+ }
3113
+ }
3114
+ if (subBoundaries.maxItems !== -1) {
3115
+ if (boundaries.maxItems !== -1) {
3116
+ boundaries.maxItems = Math.min(
3117
+ boundaries.maxItems,
3118
+ subBoundaries.maxItems
3119
+ );
3120
+ }
3121
+ else {
3122
+ boundaries.maxItems = subBoundaries.maxItems;
3123
+ }
3124
+ }
3125
+ });
3126
+ }
3127
+ return boundaries;
3128
+ };
3129
+ return getNodeBoundaries(this);
3130
+ };
3131
+
3132
+
3133
+ /**
3134
+ * Form tree class.
3135
+ *
3136
+ * Holds the internal representation of the form.
3137
+ * The tree is always in sync with the rendered form, this allows to parse
3138
+ * it easily.
3139
+ *
3140
+ * @class
3141
+ */
3142
+ var formTree = function () {
3143
+ this.eventhandlers = [];
3144
+ this.root = null;
3145
+ this.formDesc = null;
3146
+ };
3147
+
3148
+ /**
3149
+ * Initializes the form tree structure from the JSONForm object
3150
+ *
3151
+ * This function is the main entry point of the JSONForm library.
3152
+ *
3153
+ * Initialization steps:
3154
+ * 1. the internal tree structure that matches the JSONForm object
3155
+ * gets created (call to buildTree)
3156
+ * 2. initial values are computed from previously submitted values
3157
+ * or from the default values defined in the JSON schema.
3158
+ *
3159
+ * When the function returns, the tree is ready to be rendered through
3160
+ * a call to "render".
3161
+ *
3162
+ * @function
3163
+ */
3164
+ formTree.prototype.initialize = function (formDesc) {
3165
+ formDesc = formDesc || {};
3166
+
3167
+ // Keep a pointer to the initial JSONForm
3168
+ // (note clone returns a shallow copy, only first-level is cloned)
3169
+ this.formDesc = _.clone(formDesc);
3170
+
3171
+ // Compute form prefix if no prefix is given.
3172
+ this.formDesc.prefix = this.formDesc.prefix ||
3173
+ 'jsonform-' + _.uniqueId();
3174
+
3175
+ // JSON schema shorthand
3176
+ if (this.formDesc.schema && !this.formDesc.schema.properties) {
3177
+ this.formDesc.schema = {
3178
+ properties: this.formDesc.schema
3179
+ };
3180
+ }
3181
+
3182
+ // Ensure layout is set
3183
+ this.formDesc.form = this.formDesc.form || [
3184
+ '*',
3185
+ {
3186
+ type: 'actions',
3187
+ items: [
3188
+ {
3189
+ type: 'submit',
3190
+ value: 'Submit'
3191
+ }
3192
+ ]
3193
+ }
3194
+ ];
3195
+ this.formDesc.form = (_.isArray(this.formDesc.form) ?
3196
+ this.formDesc.form :
3197
+ [this.formDesc.form]);
3198
+
3199
+ this.formDesc.params = this.formDesc.params || {};
3200
+
3201
+ // Create the root of the tree
3202
+ this.root = new formNode();
3203
+ this.root.ownerTree = this;
3204
+ this.root.view = jsonform.elementTypes['root'];
3205
+
3206
+ // Generate the tree from the form description
3207
+ this.buildTree();
3208
+
3209
+ // Compute the values associated with each node
3210
+ // (for arrays, the computation actually creates the form nodes)
3211
+ this.computeInitialValues();
3212
+ };
3213
+
3214
+
3215
+ /**
3216
+ * Constructs the tree from the form description.
3217
+ *
3218
+ * The function must be called once when the tree is first created.
3219
+ *
3220
+ * @function
3221
+ */
3222
+ formTree.prototype.buildTree = function () {
3223
+ // Parse and generate the form structure based on the elements encountered:
3224
+ // - '*' means "generate all possible fields using default layout"
3225
+ // - a key reference to target a specific data element
3226
+ // - a more complex object to generate specific form sections
3227
+ _.each(this.formDesc.form, function (formElement) {
3228
+ if (formElement === '*') {
3229
+ _.each(this.formDesc.schema.properties, function (element, key) {
3230
+ this.root.appendChild(this.buildFromLayout({
3231
+ key: key
3232
+ }));
3233
+ }, this);
3234
+ }
3235
+ else {
3236
+ if (_.isString(formElement)) {
3237
+ formElement = {
3238
+ key: formElement
3239
+ };
3240
+ }
3241
+ this.root.appendChild(this.buildFromLayout(formElement));
3242
+ }
3243
+ }, this);
3244
+ };
3245
+
3246
+
3247
+ /**
3248
+ * Builds the internal form tree representation from the requested layout.
3249
+ *
3250
+ * The function is recursive, generating the node children as necessary.
3251
+ * The function extracts the values from the previously submitted values
3252
+ * (this.formDesc.value) or from default values defined in the schema.
3253
+ *
3254
+ * @function
3255
+ * @param {Object} formElement JSONForm element to render
3256
+ * @param {Object} context The parsing context (the array depth in particular)
3257
+ * @return {Object} The node that matches the element.
3258
+ */
3259
+ formTree.prototype.buildFromLayout = function (formElement, context) {
3260
+ var schemaElement = null;
3261
+ var node = new formNode();
3262
+ var view = null;
3263
+ var key = null;
3264
+
3265
+ // The form element parameter directly comes from the initial
3266
+ // JSONForm object. We'll make a shallow copy of it and of its children
3267
+ // not to pollute the original object.
3268
+ // (note JSON.parse(JSON.stringify()) cannot be used since there may be
3269
+ // event handlers in there!)
3270
+ formElement = _.clone(formElement);
3271
+ if (formElement.items) {
3272
+ if (_.isArray(formElement.items)) {
3273
+ formElement.items = _.map(formElement.items, _.clone);
3274
+ }
3275
+ else {
3276
+ formElement.items = [ _.clone(formElement.items) ];
3277
+ }
3278
+ }
3279
+
3280
+ if (formElement.key) {
3281
+ // The form element is directly linked to an element in the JSON
3282
+ // schema. The properties of the form element override those of the
3283
+ // element in the JSON schema. Properties from the JSON schema complete
3284
+ // those of the form element otherwise.
3285
+
3286
+ // Retrieve the element from the JSON schema
3287
+ schemaElement = getSchemaKey(
3288
+ this.formDesc.schema.properties,
3289
+ formElement.key);
3290
+ if (!schemaElement) {
3291
+ // The JSON Form is invalid!
3292
+ throw new Error('The JSONForm object references the schema key "' +
3293
+ formElement.key + '" but that key does not exist in the JSON schema');
3294
+ }
3295
+
3296
+ // Schema element has just been found, let's trigger the
3297
+ // "onElementSchema" event
3298
+ // (tidoust: not sure what the use case for this is, keeping the
3299
+ // code for backward compatibility)
3300
+ if (this.formDesc.onElementSchema) {
3301
+ this.formDesc.onElementSchema(formElement, schemaElement);
3302
+ }
3303
+
3304
+ formElement.name =
3305
+ formElement.name ||
3306
+ formElement.key;
3307
+ formElement.title =
3308
+ formElement.title ||
3309
+ schemaElement.title;
3310
+ formElement.description =
3311
+ formElement.description ||
3312
+ schemaElement.description;
3313
+ formElement.readOnly =
3314
+ formElement.readOnly ||
3315
+ schemaElement.readOnly ||
3316
+ formElement.readonly ||
3317
+ schemaElement.readonly;
3318
+
3319
+ // Compute the ID of the input field
3320
+ if (!formElement.id) {
3321
+ formElement.id = escapeSelector(this.formDesc.prefix) +
3322
+ '-elt-' + slugify(formElement.key);
3323
+ }
3324
+
3325
+ // Should empty strings be included in the final value?
3326
+ // TODO: it's rather unclean to pass it through the schema.
3327
+ if (formElement.allowEmpty) {
3328
+ schemaElement._jsonform_allowEmpty = true;
3329
+ }
3330
+
3331
+ // If the form element does not define its type, use the type of
3332
+ // the schema element.
3333
+ if (!formElement.type) {
3334
+ // If schema type is an array containing only a type and "null",
3335
+ // remove null and make the element non-required
3336
+ if (_.isArray(schemaElement.type)) {
3337
+ if (_.contains(schemaElement.type, "null")) {
3338
+ schemaElement.type = _.without(schemaElement.type, "null");
3339
+ schemaElement.required = false;
3340
+ }
3341
+ if (schemaElement.type.length > 1) {
3342
+ throw new Error("Cannot process schema element with multiple types.");
3343
+ }
3344
+ schemaElement.type = _.first(schemaElement.type);
3345
+ }
3346
+
3347
+ if ((schemaElement.type === 'string') &&
3348
+ (schemaElement.format === 'color')) {
3349
+ formElement.type = 'color';
3350
+ } else if ((schemaElement.type === 'number' ||
3351
+ schemaElement.type === 'integer') &&
3352
+ !schemaElement['enum']) {
3353
+ formElement.type = 'number';
3354
+ if (schemaElement.type === 'number') schemaElement.step = 'any';
3355
+ } else if ((schemaElement.type === 'string' ||
3356
+ schemaElement.type === 'any') &&
3357
+ !schemaElement['enum']) {
3358
+ formElement.type = 'text';
3359
+ } else if (schemaElement.type === 'boolean') {
3360
+ formElement.type = 'checkbox';
3361
+ } else if (schemaElement.type === 'object') {
3362
+ if (schemaElement.properties) {
3363
+ formElement.type = 'fieldset';
3364
+ } else {
3365
+ formElement.type = 'textarea';
3366
+ }
3367
+ } else if (!_.isUndefined(schemaElement['enum'])) {
3368
+ formElement.type = 'select';
3369
+ } else {
3370
+ formElement.type = schemaElement.type;
3371
+ }
3372
+ }
3373
+
3374
+ // Unless overridden in the definition of the form element (or unless
3375
+ // there's a titleMap defined), use the enumeration list defined in
3376
+ // the schema
3377
+ if (!formElement.options && schemaElement['enum']) {
3378
+ if (formElement.titleMap) {
3379
+ formElement.options = _.map(schemaElement['enum'], function (value) {
3380
+ return {
3381
+ value: value,
3382
+ title: hasOwnProperty(formElement.titleMap, value) ? formElement.titleMap[value] : value
3383
+ };
3384
+ });
3385
+ }
3386
+ else {
3387
+ formElement.options = schemaElement['enum'];
3388
+ }
3389
+ }
3390
+
3391
+ // Flag a list of checkboxes with multiple choices
3392
+ if ((formElement.type === 'checkboxes') && schemaElement.items) {
3393
+ var itemsEnum = schemaElement.items['enum'];
3394
+ if (itemsEnum) {
3395
+ schemaElement.items._jsonform_checkboxes_as_array = true;
3396
+ }
3397
+ if (!itemsEnum && schemaElement.items[0]) {
3398
+ itemsEnum = schemaElement.items[0]['enum'];
3399
+ if (itemsEnum) {
3400
+ schemaElement.items[0]._jsonform_checkboxes_as_array = true;
3401
+ }
3402
+ }
3403
+ }
3404
+
3405
+ // If the form element targets an "object" in the JSON schema,
3406
+ // we need to recurse through the list of children to create an
3407
+ // input field per child property of the object in the JSON schema
3408
+ if (schemaElement.type === 'object') {
3409
+ _.each(schemaElement.properties, function (prop, propName) {
3410
+ node.appendChild(this.buildFromLayout({
3411
+ key: formElement.key + '.' + propName
3412
+ }));
3413
+ }, this);
3414
+ }
3415
+ }
3416
+
3417
+ if (!formElement.type) {
3418
+ formElement.type = 'none';
3419
+ }
3420
+ view = jsonform.elementTypes[formElement.type];
3421
+ if (!view) {
3422
+ throw new Error('The JSONForm contains an element whose type is unknown: "' +
3423
+ formElement.type + '"');
3424
+ }
3425
+
3426
+
3427
+ if (schemaElement) {
3428
+ // The form element is linked to an element in the schema.
3429
+ // Let's make sure the types are compatible.
3430
+ // In particular, the element must not be a "container"
3431
+ // (or must be an "object" or "array" container)
3432
+ if (!view.inputfield && !view.array &&
3433
+ (formElement.type !== 'selectfieldset') &&
3434
+ (schemaElement.type !== 'object')) {
3435
+ throw new Error('The JSONForm contains an element that links to an ' +
3436
+ 'element in the JSON schema (key: "' + formElement.key + '") ' +
3437
+ 'and that should not based on its type ("' + formElement.type + '")');
3438
+ }
3439
+ }
3440
+ else {
3441
+ // The form element is not linked to an element in the schema.
3442
+ // This means the form element must be a "container" element,
3443
+ // and must not define an input field.
3444
+ if (view.inputfield && (formElement.type !== 'selectfieldset')) {
3445
+ throw new Error('The JSONForm defines an element of type ' +
3446
+ '"' + formElement.type + '" ' +
3447
+ 'but no "key" property to link the input field to the JSON schema');
3448
+ }
3449
+ }
3450
+
3451
+ // A few characters need to be escaped to use the ID as jQuery selector
3452
+ formElement.iddot = escapeSelector(formElement.id || '');
3453
+
3454
+ // Initialize the form node from the form element and schema element
3455
+ node.formElement = formElement;
3456
+ node.schemaElement = schemaElement;
3457
+ node.view = view;
3458
+ node.ownerTree = this;
3459
+
3460
+ // Set event handlers
3461
+ if (!formElement.handlers) {
3462
+ formElement.handlers = {};
3463
+ }
3464
+
3465
+ // Parse children recursively
3466
+ if (node.view.array) {
3467
+ // The form element is an array. The number of items in an array
3468
+ // is by definition dynamic, up to the form user (through "Add more",
3469
+ // "Delete" commands). The positions of the items in the array may
3470
+ // also change over time (through "Move up", "Move down" commands).
3471
+ //
3472
+ // The form node stores a "template" node that serves as basis for
3473
+ // the creation of an item in the array.
3474
+ //
3475
+ // Array items may be complex forms themselves, allowing for nesting.
3476
+ //
3477
+ // The initial values set the initial number of items in the array.
3478
+ // Note a form element contains at least one item when it is rendered.
3479
+ if (formElement.items) {
3480
+ key = formElement.items[0] || formElement.items;
3481
+ }
3482
+ else {
3483
+ key = formElement.key + '[]';
3484
+ }
3485
+ if (_.isString(key)) {
3486
+ key = { key: key };
3487
+ }
3488
+ node.setChildTemplate(this.buildFromLayout(key));
3489
+ }
3490
+ else if (formElement.items) {
3491
+ // The form element defines children elements
3492
+ _.each(formElement.items, function (item) {
3493
+ if (_.isString(item)) {
3494
+ item = { key: item };
3495
+ }
3496
+ node.appendChild(this.buildFromLayout(item));
3497
+ }, this);
3498
+ }
3499
+
3500
+ return node;
3501
+ };
3502
+
3503
+
3504
+ /**
3505
+ * Computes the values associated with each input field in the tree based
3506
+ * on previously submitted values or default values in the JSON schema.
3507
+ *
3508
+ * For arrays, the function actually creates and inserts additional
3509
+ * nodes in the tree based on previously submitted values (also ensuring
3510
+ * that the array has at least one item).
3511
+ *
3512
+ * The function sets the array path on all nodes.
3513
+ * It should be called once in the lifetime of a form tree right after
3514
+ * the tree structure has been created.
3515
+ *
3516
+ * @function
3517
+ */
3518
+ formTree.prototype.computeInitialValues = function () {
3519
+ this.root.computeInitialValues(this.formDesc.value);
3520
+ };
3521
+
3522
+
3523
+ /**
3524
+ * Renders the form tree
3525
+ *
3526
+ * @function
3527
+ * @param {Node} domRoot The "form" element in the DOM tree that serves as
3528
+ * root for the form
3529
+ */
3530
+ formTree.prototype.render = function (domRoot) {
3531
+ if (!domRoot) return;
3532
+ this.domRoot = domRoot;
3533
+ this.root.render();
3534
+
3535
+ // If the schema defines required fields, flag the form with the
3536
+ // "jsonform-hasrequired" class for styling purpose
3537
+ // (typically so that users may display a legend)
3538
+ if (this.hasRequiredField()) {
3539
+ $(domRoot).addClass('jsonform-hasrequired');
3540
+ }
3541
+ };
3542
+
3543
+ /**
3544
+ * Walks down the element tree with a callback
3545
+ *
3546
+ * @function
3547
+ * @param {Function} callback The callback to call on each element
3548
+ */
3549
+ formTree.prototype.forEachElement = function (callback) {
3550
+
3551
+ var f = function(root) {
3552
+ for (var i=0;i<root.children.length;i++) {
3553
+ callback(root.children[i]);
3554
+ f(root.children[i]);
3555
+ }
3556
+ };
3557
+ f(this.root);
3558
+
3559
+ };
3560
+
3561
+ formTree.prototype.validate = function(noErrorDisplay) {
3562
+
3563
+ var values = jsonform.getFormValue(this.domRoot);
3564
+ var errors = false;
3565
+
3566
+ var options = this.formDesc;
3567
+
3568
+ if (options.validate!==false) {
3569
+ var validator = false;
3570
+ if (typeof options.validate!="object") {
3571
+ if (global.JSONFormValidator) {
3572
+ validator = global.JSONFormValidator.createEnvironment("json-schema-draft-03");
3573
+ }
3574
+ } else {
3575
+ validator = options.validate;
3576
+ }
3577
+ if (validator) {
3578
+ var v = validator.validate(values, this.formDesc.schema);
3579
+ $(this.domRoot).jsonFormErrors(false,options);
3580
+ if (v.errors.length) {
3581
+ if (!errors) errors = [];
3582
+ errors = errors.concat(v.errors);
3583
+ }
3584
+ }
3585
+ }
3586
+
3587
+ if (errors && !noErrorDisplay) {
3588
+ if (options.displayErrors) {
3589
+ options.displayErrors(errors,this.domRoot);
3590
+ } else {
3591
+ $(this.domRoot).jsonFormErrors(errors,options);
3592
+ }
3593
+ }
3594
+
3595
+ return {"errors":errors}
3596
+
3597
+ }
3598
+
3599
+ formTree.prototype.submit = function(evt) {
3600
+
3601
+ var stopEvent = function() {
3602
+ if (evt) {
3603
+ evt.preventDefault();
3604
+ evt.stopPropagation();
3605
+ }
3606
+ return false;
3607
+ };
3608
+ var values = jsonform.getFormValue(this.domRoot);
3609
+ var options = this.formDesc;
3610
+
3611
+ var brk=false;
3612
+ this.forEachElement(function(elt) {
3613
+ if (brk) return;
3614
+ if (elt.view.onSubmit) {
3615
+ brk = !elt.view.onSubmit(evt, elt); //may be called multiple times!!
3616
+ }
3617
+ });
3618
+
3619
+ if (brk) return stopEvent();
3620
+
3621
+ var validated = this.validate();
3622
+
3623
+ if (options.onSubmit && !options.onSubmit(validated.errors,values)) {
3624
+ return stopEvent();
3625
+ }
3626
+
3627
+ if (validated.errors) return stopEvent();
3628
+
3629
+ if (options.onSubmitValid && !options.onSubmitValid(values)) {
3630
+ return stopEvent();
3631
+ }
3632
+
3633
+ return false;
3634
+
3635
+ };
3636
+
3637
+
3638
+ /**
3639
+ * Returns true if the form displays a "required" field.
3640
+ *
3641
+ * To keep things simple, the function parses the form's schema and returns
3642
+ * true as soon as it finds a "required" flag even though, in theory, that
3643
+ * schema key may not appear in the final form.
3644
+ *
3645
+ * Note that a "required" constraint on a boolean type is always enforced,
3646
+ * the code skips such definitions.
3647
+ *
3648
+ * @function
3649
+ * @return {boolean} True when the form has some required field,
3650
+ * false otherwise.
3651
+ */
3652
+ formTree.prototype.hasRequiredField = function () {
3653
+ var parseElement = function (element) {
3654
+ if (!element) return null;
3655
+ if (element.required && (element.type !== 'boolean')) {
3656
+ return element;
3657
+ }
3658
+
3659
+ var prop = _.find(element.properties, function (property) {
3660
+ return parseElement(property);
3661
+ });
3662
+ if (prop) {
3663
+ return prop;
3664
+ }
3665
+
3666
+ if (element.items) {
3667
+ if (_.isArray(element.items)) {
3668
+ prop = _.find(element.items, function (item) {
3669
+ return parseElement(item);
3670
+ });
3671
+ }
3672
+ else {
3673
+ prop = parseElement(element.items);
3674
+ }
3675
+ if (prop) {
3676
+ return prop;
3677
+ }
3678
+ }
3679
+ };
3680
+
3681
+ return parseElement(this.formDesc.schema);
3682
+ };
3683
+
3684
+
3685
+ /**
3686
+ * Returns the structured object that corresponds to the form values entered
3687
+ * by the use for the given form.
3688
+ *
3689
+ * The form must have been previously rendered through a call to jsonform.
3690
+ *
3691
+ * @function
3692
+ * @param {Node} The <form> tag in the DOM
3693
+ * @return {Object} The object that follows the data schema and matches the
3694
+ * values entered by the user.
3695
+ */
3696
+ jsonform.getFormValue = function (formelt) {
3697
+ var form = $(formelt).data('jsonform-tree');
3698
+ if (!form) return null;
3699
+ return form.root.getFormValues();
3700
+ };
3701
+
3702
+
3703
+ /**
3704
+ * Highlights errors reported by the JSON schema validator in the document.
3705
+ *
3706
+ * @function
3707
+ * @param {Object} errors List of errors reported by the JSON schema validator
3708
+ * @param {Object} options The JSON Form object that describes the form
3709
+ * (unused for the time being, could be useful to store example values or
3710
+ * specific error messages)
3711
+ */
3712
+ $.fn.jsonFormErrors = function(errors, options) {
3713
+ $(".error", this).removeClass("error");
3714
+ $(".warning", this).removeClass("warning");
3715
+
3716
+ $(".jsonform-errortext", this).hide();
3717
+ if (!errors) return;
3718
+
3719
+ var errorSelectors = [];
3720
+ for (var i = 0; i < errors.length; i++) {
3721
+ // Compute the address of the input field in the form from the URI
3722
+ // returned by the JSON schema validator.
3723
+ // These URIs typically look like:
3724
+ // urn:uuid:cccc265e-ffdd-4e40-8c97-977f7a512853#/pictures/1/thumbnail
3725
+ // What we need from that is the path in the value object:
3726
+ // pictures[1].thumbnail
3727
+ // ... and the jQuery-friendly class selector of the input field:
3728
+ // .jsonform-error-pictures\[1\]---thumbnail
3729
+ var key = errors[i].uri
3730
+ .replace(/.*#\//, '')
3731
+ .replace(/\//g, '.')
3732
+ .replace(/\.([0-9]+)(?=\.|$)/g, '[$1]');
3733
+ var errormarkerclass = ".jsonform-error-" +
3734
+ escapeSelector(key.replace(/\./g,"---"));
3735
+ errorSelectors.push(errormarkerclass);
3736
+
3737
+ var errorType = errors[i].type || "error";
3738
+ $(errormarkerclass, this).addClass(errorType);
3739
+ $(errormarkerclass + " .jsonform-errortext", this).html(errors[i].message).show();
3740
+ }
3741
+
3742
+ // Look for the first error in the DOM and ensure the element
3743
+ // is visible so that the user understands that something went wrong
3744
+ errorSelectors = errorSelectors.join(',');
3745
+ var firstError = $(errorSelectors).get(0);
3746
+ if (firstError && firstError.scrollIntoView) {
3747
+ firstError.scrollIntoView(true, {
3748
+ behavior: 'smooth'
3749
+ });
3750
+ }
3751
+ };
3752
+
3753
+
3754
+ /**
3755
+ * Generates the HTML form from the given JSON Form object and renders the form.
3756
+ *
3757
+ * Main entry point of the library. Defined as a jQuery function that typically
3758
+ * needs to be applied to a <form> element in the document.
3759
+ *
3760
+ * The function handles the following properties for the JSON Form object it
3761
+ * receives as parameter:
3762
+ * - schema (required): The JSON Schema that describes the form to render
3763
+ * - form: The options form layout description, overrides default layout
3764
+ * - prefix: String to use to prefix computed IDs. Default is an empty string.
3765
+ * Use this option if JSON Form is used multiple times in an application with
3766
+ * schemas that have overlapping parameter names to avoid running into multiple
3767
+ * IDs issues. Default value is "jsonform-[counter]".
3768
+ * - transloadit: Transloadit parameters when transloadit is used
3769
+ * - validate: Validates form against schema upon submission. Uses the value
3770
+ * of the "validate" property as validator if it is an object.
3771
+ * - displayErrors: Function to call with errors upon form submission.
3772
+ * Default is to render the errors next to the input fields.
3773
+ * - submitEvent: Name of the form submission event to bind to.
3774
+ * Default is "submit". Set this option to false to avoid event binding.
3775
+ * - onSubmit: Callback function to call when form is submitted
3776
+ * - onSubmitValid: Callback function to call when form is submitted without
3777
+ * errors.
3778
+ *
3779
+ * @function
3780
+ * @param {Object} options The JSON Form object to use as basis for the form
3781
+ */
3782
+ $.fn.jsonForm = function(options) {
3783
+ var formElt = this;
3784
+
3785
+ options = _.defaults({}, options, {submitEvent: 'submit'});
3786
+
3787
+ var form = new formTree();
3788
+ form.initialize(options);
3789
+ form.render(formElt.get(0));
3790
+
3791
+ // TODO: move that to formTree.render
3792
+ if (options.transloadit) {
3793
+ formElt.append('<input type="hidden" name="params" value=\'' +
3794
+ escapeHTML(JSON.stringify(options.transloadit.params)) +
3795
+ '\'>');
3796
+ }
3797
+
3798
+ // Keep a direct pointer to the JSON schema for form submission purpose
3799
+ formElt.data("jsonform-tree", form);
3800
+
3801
+ if (options.submitEvent) {
3802
+ formElt.unbind((options.submitEvent)+'.jsonform');
3803
+ formElt.bind((options.submitEvent)+'.jsonform', function(evt) {
3804
+ form.submit(evt);
3805
+ });
3806
+ }
3807
+
3808
+ // Initialize tabs sections, if any
3809
+ initializeTabs(formElt);
3810
+
3811
+ // Initialize expandable sections, if any
3812
+ $('.expandable > div, .expandable > fieldset', formElt).hide();
3813
+ formElt.on('click', '.expandable > legend', function () {
3814
+ var parent = $(this).parent();
3815
+ parent.toggleClass('expanded');
3816
+ parent.find('legend').attr("aria-expanded", parent.hasClass("expanded"))
3817
+ $('> div', parent).slideToggle(100);
3818
+ });
3819
+
3820
+ return form;
3821
+ };
3822
+
3823
+
3824
+ /**
3825
+ * Retrieves the structured values object generated from the values
3826
+ * entered by the user and the data schema that gave birth to the form.
3827
+ *
3828
+ * Defined as a jQuery function that typically needs to be applied to
3829
+ * a <form> element whose content has previously been generated by a
3830
+ * call to "jsonForm".
3831
+ *
3832
+ * Unless explicitly disabled, the values are automatically validated
3833
+ * against the constraints expressed in the schema.
3834
+ *
3835
+ * @function
3836
+ * @return {Object} Structured values object that matches the user inputs
3837
+ * and the data schema.
3838
+ */
3839
+ $.fn.jsonFormValue = function() {
3840
+ return jsonform.getFormValue(this);
3841
+ };
3842
+
3843
+ // Expose the getFormValue method to the global object
3844
+ // (other methods exposed as jQuery functions)
3845
+ global.JSONForm = global.JSONForm || {util:{}};
3846
+ global.JSONForm.getFormValue = jsonform.getFormValue;
3847
+ global.JSONForm.fieldTemplate = jsonform.fieldTemplate;
3848
+ global.JSONForm.fieldTypes = jsonform.elementTypes;
3849
+ global.JSONForm.getInitialValue = getInitialValue;
3850
+ global.JSONForm.util.getObjKey = jsonform.util.getObjKey;
3851
+ global.JSONForm.util.setObjKey = jsonform.util.setObjKey;
3852
+
3853
+ })((typeof exports !== 'undefined'),
3854
+ ((typeof exports !== 'undefined') ? exports : window),
3855
+ ((typeof jQuery !== 'undefined') ? jQuery : { fn: {} }),
3856
+ ((typeof _ !== 'undefined') ? _ : null),
3857
+ JSON);