/** * jquery.jsForm * ------------- * JsForm control for handling html UI with json objects * @version 1.0 * @class * @author Niko Berger * @license MIT License GPL */ ;(function( $, window, undefined ){ "use strict"; let JSFORM_INIT_FUNCTIONS = {}, // remember initialization functions JSFORM_MAP = {}; // remember all forms /** * @param element {Node} the container node that should be converted to a jsForm * @param options {object} the configuraton object * @constructor */ function JsForm (element, options) { let $this = $(element); // create the options this.options = $.extend({}, { /** * enable form control rendering (if jsForm.controls is available) and validation */ controls: true, /** * the object used to fill/collect data */ data: null, /** * the prefix used to annotate the input fields */ prefix: "data", /** * set to null to discourage the tracking of "changed" fields. * Disabling this will increase performance, but disabled the "changed" functionality. * This will add the given css class to changed fields. */ trackChanges: "changed", /** * set to false to only validate visible fields. * This is discouraged especially when you have tabs or similar elements in your form. */ validateHidden: true, /** * skip empty values when getting an object */ skipEmpty: false, /** * an object with callback functions that act as renderer for data fields (class=object). * ie. { infoRender: function(data){return data.id + ": " + data.name} } */ renderer: null, /** * an object with callback functions that act as pre-processors for data fields (class=object). * ie. { idFilter: function(data){return data.id} } */ processors: null, /** * dataHandler will be called for each field filled. */ dataHandler: null, /*{ serialize: function(val, field, obj) { if(field.hasClass("reverse")) return val.reverse(); }, deserialize: function(val, field, obj) { if(field.hasClass("reverse")) return val.reverse(); } }*/ /** * optional array of elements that should be connected with the form. This * allows the splitting of the form into different parts of the dom. */ connect: null, /** * The class used when calling preventEditing. This will replace all * inputs with a span with the given field */ viewClass: "jsfValue" }, options); // read prefix from dom if($this.attr("data-prefix") && (this.options.prefix === "data" || this.options.prefix === "")) { if($this.attr("data-prefix") !== "") { this.options.prefix = $this.attr("data-prefix"); } } this.element = element; this._init(); } /** * init the portlet - load the config * @private */ JsForm.prototype._init = function() { // init the basic dom functionality this._domInit(); // enable form controls if(this.options.controls) { if($.jsFormControls) { // handle multiple form parts $.each(this._getForm(), function(){ $(this).jsFormControls(); }); } else { try { if(typeof console !== "undefined") { this._debug("jquery.JsForm.controls not available!"); } } catch(ex) { // ignore } } } // fill/init with the first data this._fill(); }; /** * Connect a dom element with an already existing form. * @param ele the new part of the form */ JsForm.prototype.connect = function(ele) { // collection lists with buttons this._initCollection(ele, this.options.prefix); // init conditionals this._initConditional(ele, this.options.prefix, this.options); // enable form controls if(this.options.controls) { if($.jsFormControls) { // handle multiple form parts $(ele).jsFormControls(); } } this._fillDom(ele); if(!this.options.connect) this.options.connect = []; this.options.connect.push(ele); }; /** * @return all nodes for this jsform (main + connected) */ JsForm.prototype.getNodes = function() { return this._getForm(); }; /** * init the dom. This can be called multiple times. * this will also enable "add", "insert" and "delete" for collections * @private */ JsForm.prototype._domInit = function() { const that = this; // handle multiple form parts $.each(this._getForm(), function(){ // collection lists with buttons that._initCollection(this, that.options.prefix); // init conditionals that._initConditional(this, that.options.prefix, that.options); }); }; /** * simple debug helper * @param msg the message to print * @private */ JsForm.prototype._debug = function(msg, param) { try { const cons = console || (window?window.console:null); if (!cons || !cons.log) return; let p = null; if($.isPlainObject(param)) { p = JSON.stringify(param, null, " "); } else { p = param; } if(!p) { p = ""; } cons.log(msg + p); } catch(ex) { // ignore } }; /** * initialize conditionals. * basic rule is: * any dom element that has a conditional and either * a data-show or data-hide attribute or a data-eval attribute * * @param form the base dom element * @param prefix the prefix to check for * @private */ JsForm.prototype._initConditional = function(form, prefix, options) { const that = this; let showEvaluator = function(ele, data, fields) { // if any field has a value -> show let show = false; $.each(fields, function(){ let value = that._getValueWithArrays(data, this); if($(that).data().condition && value !== $(that).data().condition) return; else if(!value || value === "" || value === 0 || value === -1) return; show = true; // skip processing return false; }); if(show) ele.show(); else ele.hide(); }, hideEvaluator = function(ele, data, fields) { // if any field has a value -> hide let show = false; $.each(fields, function(){ let value = that._getValueWithArrays(data, this); if($(that).data().condition && value !== $(that).data().condition) return; else if(!value || value === "" || value === 0 || value === -1) return; show = true; // skip processing return false; }); if(show) ele.hide(); else ele.show(); }; // remember the conditionals for faster dom access this.conditionals = $(form).find(".conditional"); this.conditionals.each(function(){ $(this).data().conditionalEval = []; let fields = $(this).attr("data-show"); if(fields && fields.length > 0) { $(this).data().conditionalEval.push({ func: showEvaluator, field: fields.split(" ") }); } fields = $(this).attr("data-hide"); if(fields && fields.length > 0) { $(this).data().conditionalEval.push({ func: hideEvaluator, field: fields.split(" ") }); } fields = $(this).attr("data-eval"); if(fields && fields.length > 0) { // custom evaluator if(options.conditionals[fields]) $(this).data().conditionalEval.push({ func: options.conditionals[fields] }); } }); }; /** * evaluate conditionals on the form * @param form the form to search for conditionals * @param data the data */ JsForm.prototype._evaluateConditionals = function(form, data) { this.conditionals.each(function(){ const ele = $(this); // go throguh all evaluation functions $.each(ele.data().conditionalEval, function() { this.func(ele, data, this.field); }); }); }; /** * initialize collections * @param form the base dom element * @param prefix the prefix to check for * @private */ JsForm.prototype._initCollection = function(form, prefix) { // precent double init if($(form).data().collections) return; // all collections const collectionMap = {}; const that = this; $(form).data().collections = collectionMap; $(".collection", form).each(function() { const colName = $(this).attr("data-field"); // skip collections without a data-field mapping if (!colName || colName.indexOf(prefix + ".") !== 0) { return; } const container = $(this); // remember the collection const cols = collectionMap[colName]; if(cols) { cols.push(container); } else { collectionMap[colName] = [container]; } // take the container out that._initList(container); // after adding: check if we want reorder control if(!container.hasClass("ui-sortable") && container.hasClass("sortable") && container.sortable) { // get the config object let config = container.attr("data-sortable"); if(!config) { config = {}; } else { config = JSON.parse(config); } container.sortable(config); container.on("sortstop", function() { that._reorder(container); }); } $(this).on("add", function(ev, pojo, fn){ if(ev.target !== this) return; const fieldName = $(this).attr("data-field"); const subPrefix = fieldName.substring(fieldName.lastIndexOf('.')+1); // skip if fieldName doest match if(fn && (fieldName !== fn && subPrefix !== fn) ) return; const tmpl = $(this).data("template"); if(!pojo) { pojo = {}; } // and has a template if(!tmpl) { return; } const idx = $(this).children(".POJO").length; const line = tmpl.clone(true); $(line).addClass("POJO"); that._fillLine($(this), pojo, line, subPrefix, idx); $(this).append(line); // trigger a callback after the data has been rendered) $(this).trigger("postAddCollection", [line, $(line).data().pojo, fieldName]); }); }); $(".add", form).each(function(){ const fieldName = $(this).attr("data-field"); if (!fieldName || fieldName.indexOf(prefix + ".") !== 0) { return; } // add the collection $(this).data().collections = collectionMap[fieldName]; // only init once if($(this).data().hasJsForm) { return; } $(this).data().hasJsForm = true; $(this).click(function(ev){ ev.preventDefault(); // get prefill data let prefill = $(this).data().prefill; if(prefill) { if(typeof prefill === "function") { prefill = prefill(); } else if(prefill.length > 2) prefill = JSON.parse(prefill); } else { prefill = null; } // search for a collection with that name $.each($(this).data("collections"), function() { $(this).trigger("add", [prefill, fieldName]); }); }); }); $(".clear", form).each(function(){ let fieldname = $(this).attr("data-field"); if (!fieldname) { return; } $(this).on("click", function(){ $(this).closest(".POJO").find("input[name='"+fieldname+"']").data().pojo = null; $(this).closest(".POJO").find("input[name='"+fieldname+"']").val("").change(); }); }); // insert: similar to add - but works with events $(".insert", form).each(function(){ const fieldName = $(this).data().field; if (!fieldName || fieldName.indexOf(prefix + ".") !== 0) { console.log("INSERT: unable to find " + fieldName, this); return; } const subPrefix = fieldName.substring(fieldName.lastIndexOf('.')+1); // only init once if($(this).data().isCollection) { return; } $(this).data().isCollection = true; // remember the collections $(this).data().collections = collectionMap[fieldName]; $(this).on("insert", function(_ev, pojo){ if(!pojo) { pojo = $(this).data().pojo; } if(!pojo && $(this).is("select")) { const sel = $(this).find(":selected"); if(sel.data().pojo) pojo = sel.data().pojo; else if(sel.val() !== "" && sel.val() !== "null") { pojo = sel.val(); } } // insert only works if there is a pojo if(!pojo) { return; } let beforeInsertCallback = $(this).data().beforeInsert; if(beforeInsertCallback && $.isFunction(beforeInsertCallback)) { pojo = beforeInsertCallback(pojo); // insert only works if there is a pojo if(!pojo) { return; } } // search for a collection with that name $.each($(this).data("collections"), function() { $(this).trigger("add", [pojo, subPrefix]); }); // empty field $(this).val(""); $(this).data().pojo = null; $(this).focus(); }); }); // insert: helper button (triggers insert) $(".insertAction", form).each(function(){ const fieldName = $(this).data().field; if(!fieldName) { console.log("Field name not specified", this); return; } // only init once if($(this).data().inserter) { return; } // find the insert element for this data-field let inserter = $(this).parent().find(".insert"); if(inserter.length === 0) { // go one more level inserter = $(this).parent().parent().find(".insert"); } if(inserter.length === 0) { console.log("Unable to find inserter for field: " + fieldName); return; } // remember the inserter $(this).data().inserter = inserter; $(this).click(function(ev){ ev.preventDefault(); $(this).data().inserter.trigger("insert"); return false; }); }); $("input.object", form).each(function(){ $(this).on("update", function(evt){ let pojo = $(this).data().pojo; if($(this).attr("data-display") || $(this).attr("data-render")) { $(this).val(that._renderObject(pojo, $(this).attr("data-display"), $(this).attr("data-render"))); } }); }); // fileupload $("input.blob", form).each(function(){ // only available on input type file if($(this).attr("type") !== "file") { return; } let blobInput = $(this); // bind on change $(this).on("change", function(evt){ //get file name let fileName = $(this).val().split(/\\/).pop(); blobInput.data("name", fileName); let files = evt.target.files; // FileList object // Loop through the FileList (and render image files as thumbnails.(skip for ie < 9) if(files && files.length) { $.each(files, function() { let reader = new FileReader(); // closure to capture the file information reader.onload = function(e) { // get the result blobInput.data("blob", e.target.result); }; // Read in the image file as a data URL. reader.readAsDataURL(this); $(this).trigger("fileChange"); }); } }); }); }; /** * init a container that has a tempalate child (first child). * @param container the contianer element * @private */ JsForm.prototype._initList = function(container) { // avoid double initialisation if(container.data("template")) { return; } // get all children const tmpl = container.children().detach(); // remove an id if there is one tmpl.removeAttr("id"); container.data("template", tmpl); }; /** * generate the array with all DOM elements that are connected with * the form. * @private */ JsForm.prototype._getForm = function() { const form = [$(this.element)]; if(this.options.connect) $.each(this.options.connect, function(){ form.push($(this)); }); return form; }; /** * clear/reset a form. The prefix is normally predefined by init * @param form the form * @param prefix the optional prefix used to identify fields for this form */ JsForm.prototype._clear = function(form, prefix) { // get the prefix from the form if not given if(!prefix) { prefix = this.options.prefix; } $(form).removeData("pojo"); $("input,select,textarea", form).each(function(){ let name = $(this).attr("name"); // empty name - ignore if (!name || name.indexOf(prefix + ".") !== 0) { return; } // cut away the prefix name = name.substring((prefix+".").length); // skip empty if(name.length < 1) { return; } // remove all pojos delete $(this).data().pojo; if($(this).attr("type") === "checkbox") { $(this).prop("checked", false); } else if($(this).attr("type") === "radio") { $(this).prop("checked", false); } else if($(this).data().valclass && $(this)[$(this).data().valclass].val){ $(this)[$(this).data().valclass](val, ""); } else { $(this).val(""); } if($(this).hasClass("blob")) { $(this).removeData("blob"); } // special type select box: select the FIRST child if($(this).is("select")) { $('option[selected="selected"]', this).prop('selected', false); $('option:first', this).prop('selected', true); $(this).val($("option:first", this).val()); $(this).change(); } // trigger change $(this).change(); }); $(".collection", form).each(function() { let fieldname = $(this).attr("data-field"); // only collections with the correct prefix if(!fieldname || fieldname.indexOf(prefix+".") !== 0) { return; } // get rid of all $(this).empty(); }); }; /** * Handle arrays when creating pojos * @param ele the element * @param pojo the base object * @param name the name of the field * @param val the value to check agains * @private */ JsForm.prototype._handleArrayInPojo = function(ele, pojo, name, val) { // create an array out of this if(!pojo[name]) { pojo[name] = []; } if(ele.attr("type") === "checkbox" || ele.attr("type") === "CHECKBOX") { // do we want the value of not let use = ele.is(":checked"); let pushVal = true; $.each(pojo[name], function(data, index){ if(this == val) { // dont need to push pushVal = false; // we dont use it - remove it if(!use) pojo[name].splice(index, 1); return false; } }); if(pushVal && use) pojo[name].push(val); } else { let num = ele.attr("data-array"); if(!num || isNaN(num)) { num = null; } else num = Number(num); // no num -> add the array if(num === null) pojo[name].push(val); else pojo[name][num] = val; } }; /** * set a value in a pojo * @param pojo the data pojo * @param name the name of the field to set (allows . syntax) * @param val the value to set * @param $this the object the val comes from for array check */ JsForm.prototype._setPojoVal = function(pojo, name, val, $this) { const that = this; // check if we have a . - if so split if (name.indexOf(".") === -1) { // handle arrays if($this && $this.hasClass("array")) { that._handleArrayInPojo($this, pojo, name, val); } else pojo[name] = val; } else { let parts = name.split("."); let prev; let current = pojo[parts[0]]; if (!current || !$.isPlainObject(current)) { pojo[parts[0]] = {}; current = pojo[parts[0]]; } for(let i = 1; i < parts.length - 1; i++) { prev = current; current = prev[parts[i]]; if(current === undefined || current === null) { current = {}; prev[parts[i]] = current; } } // set prev as the name prev = parts[parts.length - 1]; // handle arrays if($this && $this.hasClass("array")) { that._handleArrayInPojo($(this), current, prev, val); } else { current[prev] = val; } } }; /** * ceate a pojo from a form. Takes special data definition classes into account: * * @param start the element to start from (ie. the form or tr) * @param pojo the pojo to write everything to * @param prefix a prefix: only fields with the given prefix will be included in the pojo * @private */ JsForm.prototype._createPojoFromInput = function (start, prefix, pojo) { // check if we have an "original" pojo let startObj = null; const that = this; // normally we edit the pojo on ourselves - so result is null let result = null; // get it from the starting dom element if($(start).data().pojo) { startObj = $(start).data().pojo; } // if we have an object, use this as base and fill the pojo if(startObj) { if(typeof startObj === "object") $.extend(true, pojo, startObj); else // primitive: simply return return startObj; } $(start).find("input,select,textarea,button,.jsobject").each(function(){ let name = $(this).attr("data-name"); if(!name) { name = $(this).attr("name"); } // empty name - ignore if (!name) { return; } // skip grayed (=calculated) or transient fields if($(this).hasClass("transient") || $(this).hasClass("grayed")) { return; } // must start with prefix if(name.indexOf(prefix + ".") !== 0) { return; } $(this).trigger("validate", true); // cut away the prefix name = name.substring((prefix+".").length); let val = $(this).val(); if($(this).data().valclass && $(this)[$(this).data().valclass]){ val = $(this)[$(this).data().valclass]("val"); } // jsobject use the pojo data directly - ignore the rest if($(this).hasClass("jsobject")) { val = $(this).data().pojo; } else { // ignore empty values when skipEmpty is set if(that.options.skipEmpty && (!val || val === "" || val.trim() === "")) { return; } if($(this).hasClass("emptynull") && (!val || val === "" || val === "null" || val.trim() === "")) { // nullable fields do not send empty string val = null; } else if($(this).hasClass("object") || $(this).hasClass("POJO")) { if($("option:selected", this).data() && $("option:selected", this).data().pojo) { if($("option:selected", this).data().pojo) val = $("option:selected", this).data().pojo; else if($("option:selected", this).attr("data-obj")) val = JSON.parse($("option:selected", this).attr("data-obj")); } else { val = $(this).data().pojo; } // limit to only one field if(val && $(this).data().onylfield) { let objlimit = val[$(this).data().onylfield]; val = { }; val[$(this).data().onylfield] = objlimit; } // object can also have a processor if($.isFunction($(this).data().processor)) { val = $(this).data().processor(val); } else { let processor = $(this).attr("data-processor"); if(processor && that.options.processors[processor]) { val = that.options.processors[processor](val); } } } else if($(this).hasClass("blob")) { // file upload blob val = $(this).data("blob"); } else // set empty numbers or dates to null if(val === "" && ($(this).hasClass("number") || $(this).hasClass("percent") || $(this).hasClass("integer") || $(this).hasClass("dateFilter")|| $(this).hasClass("dateTimeFilter"))) { val = null; } // we might have a value processor on this: this is added by the jsForm.controls if($(this).data().processor) { val = $(this).data().processor(val); } else if($(this).attr("type") === "checkbox" || $(this).attr("type") === "CHECKBOX") { // a checkbox as an array if($(this).hasClass("array")) { // the special case: array+checkbox is handled on the actual setting val = $(this).val(); if($(this).attr("data-obj")) { val = JSON.parse($(this).attr("data-obj")); } } else { val = $(this).is(':checked'); } } else if($(this).attr("type") === "radio" || $(this).attr("type") === "RADIO") { if(!$(this).is(':checked')) { return; } if($(this).hasClass("bool") || $(this).hasClass("boolean")) { val = $(this).val() === "true"; } else if($(this).hasClass("number")) { val = Number($(this).val()); } } else if ($(this).hasClass("number") || $(this).hasClass("integer")) { if($(this).hasClass("date") && isNaN(val)) { if($.format) { let d = $.format.date(val); d.setHours(0); d.setMinutes(0); d.setSeconds(0); d.setMilliseconds(0); val = d.getTime(); } else val = new Date(val).getTime(); } else val = that._getNumber(val); if(isNaN(val)) { val = 0; } } else if($(this).hasClass("bool")) { val = ($(this).val() === "true"); } else if($(this).hasClass("boolean")) { switch($(this).val()) { case "true": val = true; break; case "false": val = false; break; default: val = null; } } } // we got the value - send it to the processor if(that.options.dataHandler) { val = that.options.dataHandler.serialize(val, $(this), pojo); } // handle simple collection if(name.length < 1) { // handle simple collection: we want the val as result result = val; return false; } that._setPojoVal(pojo, name, val, $(this)); }); // for "selection" collection $(start).find(".selectcollection").each(function(){ let name = $(this).attr("data-field"); // empty name - ignore if (!name) { return; } // skip grayed (=calculated) or transient fields if($(this).hasClass("transient")) { return; } // must start with prefix if(name.indexOf(prefix + ".") !== 0) { return; } $(this).trigger("validate", true); // cut away the prefix name = name.substring((prefix+".").length); // always an array (reset current data) let arrVal = []; // see if we go by checkbox or by css class (if both -> class wins) let selectedClass = $(this).attr("data-selected"); let id = $(this).attr("data-id"); $(this).children().each(function(){ // check selection if(selectedClass) { if(!$(this).hasClass(selectedClass)) return; } else { if(!$("input[name='"+name+"']", this).prop('checked')) return; } // get the "id"/object let cobj = null; // no id given - check the value of the checkbox if(!id && ! $("input[name='"+name+"']", this).hasClass("obj")) { cobj = $("input[name='"+name+"']", this).val(); } else { // get the object cobj = $(this).data("obj"); if(!cobj && $(this).attr("data-obj")) { cobj = JSON.parse($(this).attr("data-obj")); } } // no object/data found if(!cobj) return; arrVal.push(cobj); }); that._setPojoVal(pojo, name, arrVal); }); return result; }; /** * helper function to enable tracking on fields * @param ele the element to track */ JsForm.prototype._enableTracking = function(ele) { if(!ele || ele.length === 0) { return; } const that = this; if(that.options.trackChanges && !$(ele).data().track) { $(ele).data().track = true; $(ele).change(function(){ if($(this).val() !== $(this).data().orig) { $(this).addClass(that.options.trackChanges); }else { $(this).removeClass(that.options.trackChanges); } }); } }; /** * search for collections to fill * @param parent the parentnode * @param data the data * @param prefix the prefix used to find fields * @param idx the index - this is only used for collections * @private */ JsForm.prototype._fillSelectCollection = function (parent, data, prefix, idx) { const that = this; const $parent = $(parent); // locate all "select collections" $parent.find(".selectcollection").each(function() { const selectedClass = $(this).attr("data-selected"); const id = $(this).attr("data-id"); const fieldname = $(this).attr("data-field"); // only collections with the correct prefix if(!fieldname || fieldname.indexOf(prefix+".") !== 0) { return; } // data for the collection filling let colData = null; let cname = fieldname; // remove the prefix if (prefix) { cname = cname.substring(prefix.length + 1); } colData = that._get(data, cname); if(!colData || !$.isArray(colData)) { colData = []; } // reset selection if(selectedClass) { $(this).children("." + selectedClass).removeClass(selectedClass); $(this).children().each(function(){ if($(this).hasClass("jsfselect")) return; // identify that we already bound $(this).addClass("jsfselect"); $(this).click(function(){ $(this).toggleClass(selectedClass); $(this).trigger("selected"); }); // trigger "deselected" $(this).trigger("selected"); }); } // remove ALL checkboxes $("input[name='"+cname+"']", this).prop('checked', false); // now go through each child and apply selection if appropriate $(this).children().each(function(){ // get the "id"/object let cid = ""; // no id given - check the value of the checkbox if(!id) { cid = $("input[name='"+cname+"']", this).val(); } else { // get the object let obj = $(this).data("obj"); if(!obj && $(this).attr("data-obj")) { obj = JSON.parse($(this).attr("data-obj")); } // avoid exception if(!obj) return; // take the id field of the object cid = obj[id]; } if(!cid) return; for(let i = 0; i < colData.length; i++) { let did = colData[i]; if(id && did) did = did[id]; // found it if(cid == did){ if(selectedClass) { $(this).addClass(selectedClass).trigger("selected"); } $("input[name='"+cname+"']", this).prop('checked', true); return; } } }); }); }; /** * fill all non-editable values with data. * * @param parent the root of the subtree * @param data the data * @param prefix the prefix used to find fields * @param idx the index - this is only used for collections * @private */ JsForm.prototype._fillFieldData = function (parent, data, prefix, idx) { const that = this; const $parent = $(parent); if(prefix.indexOf(".") > 0) { prefix = prefix.substring(prefix.indexOf(".")+1); } // locate all "mustache templates" $parent.find(".templatefield").each(function() { const attr = $(this).data().attr; let mustache = $(this).data().mustache; if(!mustache) { if(typeof Hogan !== "undefined") { mustache = Hogan.compile($(this).data().template.replace(/\[\[/g, "{{").replace(/]]/g, "}}")); } else if(typeof Handlebars !== "undefined") { mustache = { render: Handlebars.compile($(this).data().template.replace(/\[\[/g, "{{").replace(/]]/g, "}}")) }; } else { console.error("No mustache renderer found. templating not available (include Handlebars.js or Hogan.js)"); } // save for next $(this).data().mustache = mustache; } const params = { data: that.options.data, cur: data }; $(this).attr(attr, mustache.render(params)); }); // locate all "fields" $parent.find(".field").each(function() { let name = $(this).data().name; if(!name) { name = $(this).data().field; } // add optional prefix let dataprefix = $(this).attr("data-prefix"); if(!dataprefix) { dataprefix = ""; } // and postfix let datapostfix = $(this).attr("data-postfix"); if(!datapostfix) { datapostfix = ""; } if(!name) { if(this.nodeName.toUpperCase() === 'A') { name = $(this).attr("href"); $(this).attr("href", "#"); }else if(this.nodeName.toUpperCase() === 'IMG') { name = $(this).attr("src"); if(name.indexOf("#") === 0) { name = name.substring(1); } $(this).attr("src", "#"); }else { name = $(this).text(); } $(this).data("name", name); $(this).show(); } if(!prefix || name.indexOf(prefix + ".") >= 0) { let cname = name; if (prefix) { cname = cname.substring(prefix.length + 1); } let cdata = that._get(data, cname, false, idx); if(!cdata && cdata !== 0 && cdata !== false) { cdata = ""; } else if(cdata !== "") { if(dataprefix !== "") { cdata = dataprefix + cdata; } if(datapostfix !== "") { cdata = cdata + datapostfix; } } // check for currency if($(this).hasClass("currency")) { if (!cdata) cdata = 0; } // keep the original in the title if($(this).hasClass("titleval")) { $(this).attr("title", cdata); } if($(this).hasClass("setObj")) { // keep the data object $(this).data().pojo = cdata; $(this).addClass("POJO"); } else { // we got the value - send it to the processor if(that.options.dataHandler) { cdata = that.options.dataHandler.deserialize(cdata, $(this), cname, data); } if($(this).hasClass("formatter") && $(this).data().formatter) { cdata = Formatter[$(this).data().formatter](null, null, cdata); } // format the string if($.jsFormControls) { cdata = $.jsFormControls.Format.format(this, cdata); } if(this.nodeName.toUpperCase() === 'A') { $(this).attr("href", cdata); } else if(this.nodeName.toUpperCase() === 'IMG') { $(this).attr("src", cdata); } else if(this.nodeName.toUpperCase() === "DIV" || $(this).hasClass("noescape")){ $(this).html(cdata); } else { $(this).text(cdata); } } } }); }; /** * fill a dom subtree with data. * * @param parent the root of the subtree * @param data the data * @param prefix the prefix used to find fields * @param idx the index - this is only used for collections * @private */ JsForm.prototype._fillData = function (parent, data, prefix, idx) { const that = this; const $parent = $(parent); if(prefix.indexOf(".") > 0) { prefix = prefix.substring(prefix.indexOf(".")+1); } // allow repainting of this subtree if(!$parent.data().refresh) { $parent.data().refresh = true; $parent.on("refresh", function(){ let curData = $(this).data().pojo; if(!curData) return; that._fillData($(this), curData, prefix, idx); }); } $("input,textarea,button", $parent).each(function() { let name = $(this).attr("name"); if(!name) { return; } that._enableTracking(this); if(!prefix || name.indexOf(prefix + ".") >= 0) { let cname = name; if (prefix) { cname = cname.substring(prefix.length + 1); } let cdata = that._get(data, cname, false, idx); // we got the value - send it to the processor if(that.options.dataHandler) { cdata = that.options.dataHandler.deserialize(cdata, $(this), cname, data); } // ignore file inputs - they have no value if($(this).attr("type") == "file") { $(this).data().pojo = cdata; $(this).addClass("POJO"); return; } if ($(this).hasClass("object")) { $(this).data().pojo = cdata; $(this).addClass("POJO"); // set the cdata cdata = that._renderObject(cdata, $(this).attr("data-display"), $(this).attr("data-render")); } else if ($(this).hasClass("jsobject")) { $(this).data().pojo = cdata; $(this).addClass("POJO"); } else if($.isPlainObject(cdata)) { // for simple arrays - make sure cdata is not an object but an empty string, otherwise add wont work if(cname === '') { cdata = ''; } else { $(this).data().pojo = cdata; $(this).addClass("POJO"); cdata = that._renderObject(cdata, $(this).attr("data-display"), $(this).attr("data-render")); } } if($(this).attr("type") === "checkbox") { // array in checkbox if($(this).hasClass("array")) { // checkbox: set if any part of the array matches let cbVal = $(this).val(); let cbId = null; if($(this).attr("data-obj")) { cbVal = JSON.parse($(this).attr("data-obj")); } // get the id if($(this).attr("data-id")) { cbId = $(this).attr("data-id"); cbVal = cbVal[cbId]; } let found = false; if(cdata) { $.each(cdata, function(){ let cid = this; if(cbId) cid = cid[cbId]; if(cid == cbVal) { found = true; return false; } }); } // select $(this).prop("checked", found); } else { $(this).prop("checked", (cdata === true || cdata === "true")); } } else if($(this).attr("type") === "radio") { if($(this).hasClass("bool")) { if(cdata && $(this).val() === "true") $(this).prop("checked", true); else if(!cdata && $(this).val() === "false") $(this).prop("checked", true); else $(this).prop("checked", false); } else if($(this).hasClass("number")) { $(this).prop("checked", cdata == $(this).val()); } else { $(this).prop("checked", cdata == $(this).val()); } } else { if(!cdata && cdata !== 0 && cdata !== false) { cdata = ""; } // format the string if($.jsFormControls) { cdata = $.jsFormControls.Format.format(this, cdata); } // array handling if($(this).hasClass("array")) { // fixed numbers let num = $(this).attr("data-array"); if(!num || isNaN(num)) { num = null; } else num = Number(num); if(num !== null && cdata && cdata.length > num) { $(this).val(cdata[num]); } else { $(this).val(""); } } else if($(this).data().valclass && $(this)[$(this).data().valclass]){ if(cdata.toDate) $(this)[$(this).data().valclass]("val", cdata.toDate()); else $(this)[$(this).data().valclass]("val", cdata); }else $(this).val(cdata); } if(that.options.trackChanges) { $(this).data().orig = $(this).val(); } // make sure fill comes before change to allow setting of values $(this).trigger("fill"); $(this).change(); } }); $("select", $parent).each(function() { let name = $(this).attr("name"); if(!name) { return; } if(!prefix || name.indexOf(prefix + ".") >= 0) { let cname = name; if (prefix) { cname = cname.substring(prefix.length + 1); } that._enableTracking(this); // remove "old" selected options $(this).children("option:selected").prop("selected", false); let pk = $(this).attr("data-key"); if(!pk) { pk = "id"; } let value = that._get(data, cname, false, idx); // we got the value - send it to the processor if(that.options.dataHandler) { value = that.options.dataHandler.deserialize(value, $(this), cname, data); } // try selecting based on the id if (value[pk] || !isNaN(value[pk])) { // find out which one to select $(this).find("option").each(function(){ let obj = $(this).data().pojo; if(!obj) { obj = $(this).data().obj; } if(obj) { if(value[pk] === obj[pk]) { $(this).prop("selected", true); return false; } } else { // make sure to avoid string issues: use == if($(this).val() == value[pk]) { $(this).attr("selected", true); return false; } } }); // trigger the change (but dont mark it) $(this).change().removeClass("changed"); return; } else if($(this).hasClass("bool")) { value = value ? "true" : "false"; } else if($(this).hasClass("boolean")) { if(value === false) value = "false"; else if(value) value = "true"; else value = ""; } $(this).find("option[value='"+value+"']").prop("selected", true); $(this).val(value); if(that.options.trackChanges) $(this).data().orig = $(this).val(); // trigger the change (but dont mark it) $(this).change().trigger("fill").removeClass("changed"); } }); }; /** * ceate a pojo from a form. Takes special data definition classes into account: * * @param ignoreInvalid return a pojo, even if fields do not pass client side validation * @return {Object} a new pojo */ JsForm.prototype.get = function(ignoreInvalid) { const that = this; const originalPojo = this.options.data; const prefix = this.options.prefix; // get the pojo let pojo = {}; if(originalPojo && $.isPlainObject(originalPojo)) { pojo = $.extend({}, originalPojo); } // check for invalid fields let invalid = false; // go through all form parts $.each(this._getForm(), function(){ // fill the base that._createPojoFromInput(this, prefix, pojo); if(!that.options.validateHidden) { this.find(".invalid").filter(":visible").each(function(){ invalid = true; $(this).focus(); if(!ignoreInvalid) { that._debug("Found invalid field: " + $(this).attr("name")); } return false; }); } else { this.find(".invalid").each(function(){ invalid = true; $(this).focus(); return false; }); } // get the collection if(that._getCollection(this, prefix, pojo, ignoreInvalid)) { invalid = true; } }); if(!ignoreInvalid && invalid) { return null; } return pojo; }; /** * retrieve the pojo from a collection element * @param line one element of the collection * @return the data object representing the current line */ JsForm.prototype.getCollection = function(line) { if(!line) { console.debug("Collection Line not given."); return; } const that = this; const originalPojo = line[0].data().pojo; let prefix = line[0].parent().data().field; prefix = prefix.substring(prefix.lastIndexOf(".")+1); let pojo = {}; if(originalPojo && $.isPlainObject(originalPojo)) { pojo = $.extend({}, originalPojo); } // update that._createPojoFromInput(line[0], prefix, pojo); return pojo; }; /** * fill a pojo based on collections * @param form {DOMElement} the base element to start looking for collections * @param prefix {string} the prefix used * @param pojo {object} the object to fill * @param ignoreInvalid {boolean} if true the function will return as soon as an invalid field is found * @return true if the colelction encountered an invalid field */ JsForm.prototype._getCollection = function(form, prefix, pojo, ignoreInvalid) { const that = this; // check for invalid fields let invalid = false; form.find(".collection").each(function() { if((!ignoreInvalid && invalid) || $(this).hasClass("transient")) { return; } let fieldname = $(this).attr("data-field"); // only collections with the correct prefix if(!fieldname || fieldname.indexOf(prefix+".") !== 0) { return; } fieldname = fieldname.substring((prefix+".").length); let colParent = that._getParent(pojo, fieldname, true); // get only the last part if(fieldname.indexOf('.') !== -1) { fieldname = fieldname.substring(fieldname.lastIndexOf('.') + 1); } // clear the collection colParent[fieldname] = []; // go through all direct childs - each one is an element $(this).children().each(function(){ let ele = {}, result; result = that._createPojoFromInput($(this), fieldname, ele); if(!result) { //that._debug("no string result - get subcollection"); // also collect sub-collections that._getCollection($(this), fieldname, ele, ignoreInvalid); } // check if the pojo is empty if(!that._isEmpty(ele) || result) { if($(".invalid", this).length > 0) { invalid = true; if(!ignoreInvalid) return false; } if(!result) { colParent[fieldname].push(ele); } else colParent[fieldname].push(result); } else { $(".invalid", this).removeClass("invalid"); } }); }); return invalid; }; /** * Get the data object used as a base for get(). * Note that modifying this directly might result into unwanted results * when working with some functions that rely on this object. * * @returns the original data object */ JsForm.prototype.getData = function() { // make srue we do have an object to work with if(!this.options.data) { this.options.data = {}; } return this.options.data; }; /** * allow setting a field to read-only and back * @param field the field to set the mode (editing/view) * @param mode true to */ JsForm.prototype.fieldMode = function(field, mode) { if(!field) return; // check if this is already a jquery object, otherwise ocnvert if(!field.data) { field = $("input[name='"+field + "']", this.element); } const viewClass = this.options.viewClass; if(mode) { if (field.closest("span." + viewClass)[0]) return; let val = field.val(); if (val === "null" || val === null || field.attr("type") === "submit") val = ""; if(field.hasClass("trueFalse") || field.hasClass("bool") || field.hasClass("boolean")) { if(field.is(':checked')) val = 'X'; else val = ' '; } // convert \n to brs - escape all other html val = val.replace(//g, ">").replace(/\n/g, "
"); let thespan = $(''+val+''); if(field.parent().hasClass("ui-wrapper")) field.parent().hide().wrap(thespan); else field.hide().wrap(thespan); } else { // remove text and then unwrap let span = field.closest("span." + viewClass); let ele = field.show().detach(); span.before(ele); span.remove(); } }; /** * uses form element and replaces them with "spans" that contain the actual content. * the original "inputs" are hidden * @param form the form * @param enable true: switch inputs with spans, false: switch spans back, undefined: toggle */ JsForm.prototype.preventEditing = function(prevent) { const $this = $(this.element); const viewClass = this.options.viewClass; if(typeof prevent === "undefined") { // get the disable from the form itself prevent = $this.data("disabled")?false:true; } else { // already in that state if(prevent === $this.data("disabled")) { return; } } if (prevent) { $this.find("input, textarea").each(function() { if ($(this).closest("span." + viewClass)[0]) return; if($(this).attr("type") == "hidden") return; let val = $(this).val(); if (val === "null" || val === null || $(this).attr("type") === "submit") val = ""; if($(this).hasClass("trueFalse") || $(this).hasClass("bool") || $(this).hasClass("boolean")) { if($(this).is(':checked')) val = 'X'; else val = ' '; } let thespan; if($(this).hasClass("noescape")) { thespan = $('
'+val+'
'); thespan.html(val); } else { // convert \n to brs - escape all other html val = val.replace(//g, ">").replace(/\n/g, "
"); thespan = $(''+val+''); } if($(this).parent().hasClass("ui-wrapper")) { $(this).parent().hide().before(thespan); } else { $(this).hide().before(thespan); } }); // selects are handled slightly different $this.find("select").each(function() { if ($(this).closest("span."+viewClass)[0]) return; let val = $(this).children(":selected").html(); if (val === "null" || val === null) val = ""; let thespan = $(''+val+''); // toggle switches work a little different if($(this).hasClass("ui-toggle-switch")) { $(this).prev().hide().before(thespan); } else { $(this).hide().before(thespan); } }); } else { $this.find("span." + viewClass +",div." + viewClass).each(function() { // remove text and then unwrap let ele = $(this).next("input,select,textarea,.ui-wrapper,.ui-toggle-switch").show(); $(this).remove(); }); } $this.data("disabled", prevent); }; /** * validate a given form * @return true if the form has no invalid fields, false otherwise */ JsForm.prototype.validate = function() { // get the prefix from the form if not given let valid = true; $.each(this._getForm(), function(){ // validation $(".required,.regexp,.date,.mandatory,.number,.validate,.integer", this).change(); // check for invalid fields if($(".invalid", this).length > 0) { valid = false; } }); return valid; }; /** * fill a form based on a pojo. * @param noInput set true to not set any inputs * @private */ JsForm.prototype._fill = function(noInput) { const that = this; $(this.element).addClass("POJO"); $(this.element).data("pojo", this.options.data); // handle multiple form parts $.each(this._getForm(), function(){ try { that._fillDom(this, noInput); } catch (ex) { console.log("Exception while filling form", ex, new Error().stack); } }); }; /** * This is the actual worker function that fills the dom subtree * with data. * @param ele the element to fill * @param noInput skip input fields * @private */ JsForm.prototype._fillDom = function(ele, noInput) { const that = this; // dont clear if we only fill the inputs if(!noInput) { that._clear(ele, that.options.prefix); } // fill read-only fields that._fillFieldData(ele, that.options.data, that.options.prefix); if(!noInput) { // fill base that._fillData(ele, that.options.data, that.options.prefix); // fill select-collections that._fillSelectCollection(ele, that.options.data, that.options.prefix); } // fill normal collection forms that._fillCollection(ele, that.options.data, that.options.prefix, noInput); // (re-)evaluate all conditionals that._evaluateConditionals(ele, that.options.data); }; /** * @param container the container element * @param data an array containing the the data * @param prefix a prefix for each line of data * @param noInput skip input fields * @private */ JsForm.prototype._fillCollection = function(container, data, prefix, noInput) { const that = this; // fill collections $(".collection", container).each(function() { const container = $(this); const fieldname = $(this).attr("data-field"); // only collections with the correct prefix if(!data || !fieldname || fieldname.indexOf(prefix+".") !== 0) { return; } // data for the collection filling let cname = fieldname; // remove the prefix if (prefix) { cname = cname.substring(prefix.length + 1); } const colData = that._get(data, cname); if(!colData) { return; } // cut away any prefixes - only the fieldname is used if(cname.indexOf('.') !== -1) { cname = cname.substring(cname.lastIndexOf('.')+1); } // fill the collection if(noInput) { for(let i = 0; i < colData.length; i++) { const line = $(container.children().get(i)); const cur = colData[i]; // only fill read only fields that._fillFieldData(line, cur, cname, i+1); // fill with data that._fillCollection(line, cur, cname, noInput); } } else { that._fillList(container, colData, cname); } }); }; /** * @param container the container element * @param data an array containing the the data * @param prefix a prefix for each line of data * @param lineFunc function(line,cur) - can return false to skip the line * @private */ JsForm.prototype._fillList = function(container, data, prefix, lineFunc) { const tmpl = container.data("template"); const that = this; if(!tmpl) { return; } // clean out previous list container.empty(); // not an array if(!$.isArray(data)) { return; } // cut away any prefixes - only the fieldname is used if(prefix.indexOf('.') !== -1) { prefix = prefix.substring(prefix.lastIndexOf('.')+1); } // check if we need to sort the array if(container.hasClass("sort")) { let sortField = container.attr("data-sort"); if(sortField) { switch(container.attr("data-sorttype")) { case 'alpha': data.sort(); break; case 'alphainsensitiv': data.sort(function(a,b){ a = a[sortField]; b = b[sortField]; if(a) a = a.toLowerCase(); if(b) b = b.toLowerCase(); if(ab) return 1; return 0; }); break; default: data.sort(function(a,b){ return a[sortField] - b[sortField]; }); } // descending: reverse if(container.attr("data-sortdesc")) { data.reverse(); } } } if(!lineFunc) { if($.isFunction(prefix)) { lineFunc = prefix; prefix = null; } } for(let i = 0; i < data.length; i++) { const cur = data[i]; const line = tmpl.clone(true); // save current line line.data().pojo = cur; line.addClass("POJO"); if(lineFunc) { if(lineFunc(line, cur) === false) { continue; } } that._fillLine(container, cur, line, prefix, i); container.append(line); // trigger a callback container.trigger("postAddCollection", [line, $(line).data().pojo, prefix]); } }; /** * add controls into a collection entry(i.e. delete) * @param line the new collection * @private */ JsForm.prototype._fillLine = function(container, cur, line, prefix, i) { const that = this; const $line = $(line); // default take passed $line.data().pojo = cur; let prefill = $line.data("prefill"); if(!prefill) prefill = $line.val("data-prefill"); // allow prefill if(prefill){ if($.isFunction(prefill)) prefill($line.data().pojo, $(line)); else if(prefill.substring) $line.data().pojo = JSON.parse(prefill); else if($.isPlainObject(prefill)) $line.data().pojo = prefill; } // init controls that._enableTracking($("input,textarea,select", line)); // new line always has changes if(that.options.trackChanges) $("input,textarea,select", line).addClass(that.options.trackChanges); that._addCollectionControls(line); if(prefix && ! $(line).data().fillLine) { // trigger a callback container.trigger("addCollection", [line, $(line).data().pojo]); $(line).data().fillLine = true; $(line).on("refresh", function(_ev){ // fill read only fields that._fillFieldData($line, $line.data().pojo, prefix, i+1); // "fill data" that._fillData($line, $line.data().pojo, prefix, i+1); // "finished" $line.trigger("refreshed", [$line, $line.data().pojo]); return false; }).trigger("refresh"); // enable collection controls that._initCollection(line, prefix); // fill with data that._fillCollection(line, cur, prefix); } }; /** * add controls into a collection entry(i.e. delete) * @param line the new collection * @private */ JsForm.prototype._addCollectionControls = function(line) { const that = this; const container = $(line).closest(".collection"); // enable controls on the line if($.jsFormControls) { $(line).jsFormControls(); } // delete the current line line.on("delete", function(ev, target){ // avoid acting on events not meant for me if(target && target[0] !== this) { return; } const ele = $(this); const pojo = $(ele).data().pojo; const base = $(this).closest(".collection"); ele.detach(); // trigger a callback $(base).trigger("deleteCollection", [ele, pojo]); }); line.on("sortUp", function(ev, target){ // avoid acting on events not meant for me if(target && target[0] !== this) { return; } // check if there is an up const ele = $(this); const prev = ele.prev(".POJO"); if(prev.size() === 0) { // no previous element - return return; } ele.detach(); prev.before(ele); // reorder (if possible) that._reorder(ele); }); line.on("sortDown", function(ev, target){ // avoid acting on events not meant for me if(target && target[0] !== this) { return; } // check if there is a down let ele = $(this); let next = ele.next(".POJO"); if(next.size() === 0) { // no next element - return return; } ele.detach(); next.after(ele); // reorder (if possible) that._reorder(ele); }); $(".delete", line).click(function(){ let ele = $(this).closest(".POJO"); ele.trigger("delete", [ele]); }); $(".sortUp", line).click(function(){ let ele = $(this).closest(".POJO"); ele.trigger("sortUp", [ele]); }); $(".sortDown", line).click(function(){ let ele = $(this).closest(".POJO"); ele.trigger("sortDown", [ele]); }); // if collection is sortable: refresh it if(container.hasClass("sortable")&& $(container).sortable) { container.sortable("refresh"); } }; /** * Reorder a collection (actually its fields) * @param ele one element of the collection or the collection itself * @private */ JsForm.prototype._reorder = function(ele) { if(!ele.attr("data-sort")) { ele = ele.closest(".collection"); } // get the field to use for sorting let sortField = $(ele).attr("data-sort"); if(!sortField || ($(ele).attr("data-sorttype") && $(ele).attr("data-sorttype") !== "number") || ($(ele).attr("data-sortdesc") && $(ele).attr("data-sortdesc") !== "false")) { return; } // go through each child and get the pojo let prio = 0; $.each($(ele).children(), function(){ let data = $(this).data("pojo"); // no data yet - add one if(!data) { data = {}; $(this).data("pojo", data); } data[sortField] = prio++; }); }; /** * render an object based on a string. * Note: comma is a special char and cannot be used! * @param obj the object * @param skin the string to render with (i.e. id, ":", test) * @private */ JsForm.prototype._renderObject = function(obj, skin, renderer) { if(!obj || (!skin && !renderer)) return ""; if(renderer) { if(this.options.renderer && this.options.renderer[renderer]) return this.options.renderer[renderer](obj); this._debug("Unable to find renderer: " + renderer); return ""; } const that = this; let ret = ""; $.each(skin.split(","), function(){ let val = this.trim(); if(val.indexOf("'") === 0 || val.indexOf('"') === 0) { ret += val.substring(1, val.length - 1); } else { ret += that._get(obj, val); } }); return ret; }; /** * Retrieve a value from a given object by using dot-notation * @param obj the object to start with * @param expr the child to get (dot notation) * @param create set to true and non-existant levels will be created (always returns non-null) * @param idx only filles when filling collection - can be access using $idx * @private */ JsForm.prototype._get = function(obj, expr, create, idx) { let ret, p, prm = "", i; if(typeof expr === "function") { return expr(obj); } if (!obj) { return ""; } // reference the object itself if(expr === "") return obj; // reference to the index if(expr === "$idx") return idx; ret = obj[expr]; if(!ret) { try { if(typeof expr === "string") { prm = expr.split('.'); } i = prm.length; if(i) { ret = obj; while(ret && i--) { p = prm.shift(); // create the levels if(create && !ret[p]) { ret[p] = {}; } ret = ret[p]; } } } catch(e) { /* ignore */ } } if(ret === null || ret === undefined) { ret = ""; } // trim the return if(ret.trim) { return ret.trim(); } return ret; }; /** * Parse a dot notation that includes arrays * http://stackoverflow.com/questions/13355278/javascript-how-to-convert-json-dot-string-into-object-reference * @param obj * @param path a dot notation path to search for. Use format parent[1].child */ JsForm.prototype._getValueWithArrays = function(obj, path) { if(obj === null) { return null; } path = path.split('.'); let arrayPattern = /(.*)\[(\d+)\]/; for (let i = 1; i < path.length; i++) { let match = arrayPattern.exec(path[i]); try { if (match) { obj = obj[match[1]][parseInt(match[2], 10)]; } else { obj = obj[path[i]]; } } catch(e) { this._debug(path + " " + e); } } return obj; }; /** * get the "parent" object of a given dot-notation. this will not return the actual * element given in the dot notation but itws parent (i.e.: when using a.b.c -> it will return b) * @param obj the object to start with * @param the child to get (dot notation) * @param create set to true and non-existant levels will be created (always returns non-null) * @private */ JsForm.prototype._getParent = function(obj, expr, create) { if(expr.indexOf('.') === -1) return obj; expr = expr.substring(0, expr.lastIndexOf('.')); return this._get(obj, expr, create); }; /** * helper function to get the number of a value * @param num the string * @returns a number or null * @private */ JsForm.prototype._getNumber = function(num) { if (!num) { return null; } // check if we have jsForm controls (internationalization) if($.jsFormControls) return $.jsFormControls.Format._getNumber(num); // remove thousand seperator... if(num.indexOf(",") !== -1 && num.indexOf(".") !== -1) { num = num.replace(new RegExp(",", 'g'), ""); } else if(num.indexOf(",") !== -1 && num.indexOf(".") === -1) { num = num.replace(new RegExp(",", 'g'), "."); } return Number(num); }; /** * checks if a letiable is empty. This will check array, and whole objects. If a json object * only contains empty "elements" then it is considered as empty. * Empty for a number is 0/-1 * Empty for a boolena is false * * @param pojo the pojo to check * @returns {Boolean} true if it is empty * @private */ JsForm.prototype._isEmpty = function(pojo) { // boolean false, null, undefined if(!pojo) { return true; } // array if($.isArray(pojo)) { // zero length if(pojo.length === 0) { return true; } // check each element for(const element of pojo) { if(!this._isEmpty(element)) { return false; } } return true; } // an object if($.isPlainObject(pojo)) { if($.isEmptyObject(pojo)) { return true; } for(let f in pojo){ if(!this._isEmpty(pojo[f])) { return false; } } return true; } // a number if(!isNaN(pojo)) { return Number(pojo) === 0 || Number(pojo) === -1; } // a string return (pojo === "" || pojo === " "); }; /** * compares two objects. note: empty string or null is the same as not existant * @param a the object to compare * @param b the object to compare with * @param idField if set then used for sub-objects instead of complete compare * @return true if they contain the same content, false otherwise */ JsForm.prototype._equals = function(a, b, idField) { // empty arrays if(!a && b && b.length && b.length === 0) { return true; } if(!b && a && a.length && a.length === 0) { return true; } if(!a && !b) { return true; } let p = null; for(p in a) { if(typeof(b[p]) === 'undefined' && a[p] !== null && a[p] !== "" && a[p].length !== 0) { // 0 == undefined if((a[p] === "0" || a[p] === 0) && !b[p]) continue; return false; } if (a[p]) { switch(typeof(a[p])) { case 'object': if(idField && a[p][idField]) { if (a[p][idField] === b[p][idField]) continue; } // go deep if (!this._equals(a[p], b[p])) { return false; } break; case 'function': // skip functions break; default: // both are "false" if(!a[p] && !b[p]) { break; } if((a === true || a === false) && a !== b) { return false; } if(!isNaN(a[p]) || !isNaN(b[p])) { if(Math.abs(Number(a[p]) - Number(b[p])) < 0.0000001) { break; } return false; } if(("" + a[p]).length !== ("" +b[p]).length) { return false; } if (a[p] !== b[p] && Number(a[p]) !== Number(b[p])) { return false; } } } else { if (b[p]) { return false; } } } for(p in b) { if((!a || typeof(a[p]) === 'undefined') && b[p] !== null && b[p] !== "") { return false; } } return true; }; /** * Compares a pojo with the current generated object * @param pojo the pojo to compare with * @return true if any change between formfields and the pojo is found */ JsForm.prototype.equals = function(pojo, idField) { const obj = this.get(false); return this._equals(obj, pojo, idField); }; /** * Compares the current form with the last time the form was filled. * * @returns {Boolean} true if the form has changed since the last fill */ JsForm.prototype.changed = function() { if(!this.options.trackChanges) return false; let changed = false; const that = this; $.each(this._getForm(), function(){ if($("." + that.options.trackChanges, this).size() > 0) { changed = true; return false; } }); return changed; }; /** * Clears all change information to avoid triggering change events */ JsForm.prototype.clearChanged = function() { const that = this; // reset changes $.each(this._getForm(), function(){ this.find("." + that.options.trackChanges).removeClass(that.options.trackChanges); }); }; /** * Resets any changes and updates the data based on user input (revert) */ JsForm.prototype.resetChanged = function() { if(!this.options.trackChanges) return false; let changed = false; const that = this; $.each(this._getForm(), function(){ $("." + that.options.trackChanges, this).each(function(){ $(this).removeClass(that.options.trackChanges); $(this).data().orig = $(this).val(); }); }); return changed; }; JsForm.prototype._equalsCollection = function(form, prefix, pojo) { const that = this; let differs = false; $(".collection", form).each(function() { if(differs) { return; } let fieldname = $(this).attr("data-field"); // only collections with the correct prefix if(!fieldname || fieldname.indexOf(prefix+".") !== 0) { return; } fieldname = fieldname.substring((prefix+".").length); if(fieldname.length < 1) { return; } let childCounter = 0; // go through all direct childs - each one is an element $(this).children().each(function(){ if(differs) { return; } // check if we have more elements if(childCounter >= pojo[fieldname].length) { differs = true; return; } const ele = pojo[fieldname][childCounter++]; if(that._pojoDifferFromInput($(this), fieldname, ele)) { differs = true; } if(!that._equalsCollection($(this), fieldname, ele)) differs = true; }); if(pojo[fieldname] && childCounter < pojo[fieldname].length) { differs = true; } }); // we want to know if its equals -> return not return !differs; }; /** * fill the form with data. * * @param data {object} the data */ JsForm.prototype.fill = function(pojo) { // set the new data this.options.data = $.extend({}, pojo); // fill everything this._fill(); $(this.element).trigger("filled", this, pojo); }; /** * fill the fields with data. Not inputs or selects * * @param data {object} the data */ JsForm.prototype.fillFields = function(pojo) { // set the new data this.options.data = $.extend({}, pojo); // fill only fields - no inputs this._fill(true); }; /** * re-evaluate the conditionals in the form based on the given data. * if no data is given, the form is serialized * @param data {object} the data */ JsForm.prototype.applyConditions = function(pojo) { // set the new data if(!pojo) pojo = this.get(true); // evaluate everything this._evaluateConditionals(this.element, pojo); }; /** * reset a form with the last data, overwriting any changes. */ JsForm.prototype.reset = function() { // fill with empty object this.fill({}); }; /** * Clear all fields in a form */ JsForm.prototype.clear = function() { const that = this; $.each(this._getForm(), function(){ that._clear(this, that.options.prefix); }); }; /** * destroy the jsform and its resources. * @private */ JsForm.prototype.destroy = function( ) { return $(this.element).each(function(){ $(this).removeData('jsForm'); if($.jsFormControls) { // handle multiple form parts $(this).jsFormControls("destroy"); } }); }; // init and call methods $.fn.jsForm = function ( method ) { // Method calling logic if ( typeof method === 'object' || ! method ) { return this.each(function () { if (!$(this).data('jsForm')) { $(this).data('jsForm', new JsForm( this, method )); } }); } else { let args = Array.prototype.slice.call( arguments, 1 ), jsForm; // none found if(this.length === 0) { return null; } // only one - return directly if(this.length === 1) { jsForm = $(this).data('jsForm'); if (jsForm) { if(method.indexOf("_") !== 0 && jsForm[method]) { let ret = jsForm[method].apply(jsForm, args); return ret; } $.error( 'Method ' + method + ' does not exist on jQuery.jsForm' ); return false; } } return this.each(function () { jsForm = $.data(this, 'jsForm'); if (jsForm) { if(method.indexOf("_") !== 0 && jsForm[method]) { return jsForm[method].apply(jsForm, args); } else { $.error( 'Method ' + method + ' does not exist on jQuery.jsForm' ); return false; } } }); } }; /** * global jsForm function for intialisation */ $.jsForm = function ( name, initFunc ) { let jsForms = JSFORM_MAP[name]; // initFunc is a function -> initialize if($.isFunction(initFunc)) { // call init if already initialized if(jsForms) { $.each(jsForms, function(){ initFunc(this, $(this.element)); }); } // remember for future initializations JSFORM_INIT_FUNCTIONS[name] = initFunc; } else { // call init if already initialized if(jsForms) { let method = initFunc; let args = Array.prototype.slice.call( arguments, 2 ); $.each(portlets, function(){ this[method].apply(this, args); }); } } }; })( jQuery, window );