You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

jsonform.js 128KB

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