/** * 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,.jsf-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).on("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("").trigger("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(); } } if(!pojo && $(this).hasClass("string")) { pojo = $(this).val(); } // insert only works if there is a pojo if(!pojo) { return; } let beforeInsertCallback = $(this).data().beforeInsert; if(beforeInsertCallback && typeof beforeInsertCallback === "function") { 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).trigger("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).on("click",function(ev){ ev.preventDefault(); $(this).data().inserter.trigger("insert"); return false; }); }); $("input.object", form).each(function(){ $(this).on("update", function(_evt){ const 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; } const blobInput = $(this); // bind on change $(this).on("change", function(evt){ //get file name const fileName = $(this).val().split(/\\/).pop(); blobInput.data("name", fileName); const 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() { const 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).trigger("change"); } // trigger change $(this).trigger("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 const 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,.splitvalue").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 if($(this).hasClass("splitvalue")) { // special "split"-control val = ""; // allow a custom separator const separator = $(this).data().separator || " "; $(this).find("input").each(function(){ if(val.length !== 0) val += separator; val += $(this).val(); }); } 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(typeof $(this).data().processor === "function") { 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 if($(this).hasClass("number") && isNaN(val)) { val = that._getNumber(val.replace(":", "")); } 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).on("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 || !Array.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).on("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) { // allow for an "alt" value if no data is in there cdata = $(this).data().alt || ""; } 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,.splitvalue", $parent).each(function() { let name = $(this).attr("name"); if(!name) { name = $(this).data().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($(this).hasClass('splitvalue')) { const separator = $(this).data().separator || " "; // special case: date-time if(cdata && $(this).hasClass('datetime') && (!cdata.indexOf || cdata.indexOf(separator) == -1)) { cdata = $.jsFormControls.Format.dateTime(cdata); } const split = cdata.split ? cdata.split(separator) : []; $(this).find("input").each(function(idx){ if(split.length > idx) { $(this).val(split[idx]); } else { $(this).val(""); } $(this).trigger("fill"); $(this).trigger("change"); }); } 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).trigger("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).trigger("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).trigger("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).trigger("focus"); if(!ignoreInvalid) { that._debug("Found invalid field: " + $(this).attr("name")); } return false; }); } else { this.find(".invalid").each(function(){ if($(this).is(":hidden")) { that._debug("Found invalid hidden field: " + $(this).attr("name")); } invalid = true; $(this).trigger("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); const 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) { // remove text and then unwrap let span = field.closest("span." + viewClass); let ele = field.show().detach(); span.before(ele); span.remove(); return; } 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); }; /** * 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"); } else if(prevent === $this.data("disabled")) { // already in that state 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,.time,.datetime", this).trigger("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(!Array.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 && typeof prefix === "function") { 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 && 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(typeof prefill === "function") 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).on("click", function(){ let ele = $(this).closest(".POJO"); ele.trigger("delete", [ele]); }); $(".sortUp", line).on("click",function(){ let ele = $(this).closest(".POJO"); ele.trigger("sortUp", [ele]); }); $(".sortDown", line).on("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(/,/g, ""); } else if(num.indexOf(",") !== -1 && num.indexOf(".") === -1) { num = num.replace(/,/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(Array.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(typeof initFunc === "function") { // 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 ); /** * jquery.jsForm.controls * ---------------------- * UI Controls and Field validation * @version 1.3 * @class * @author Niko Berger * @license MIT License GPL */ ;(function( $, window, undefined ){ "use strict"; const JSFORM_INIT_FUNCTIONS = {}; // remember initialization functions const JSFORM_MAP = {}; // remember all forms /** * handlebars extension (+simple date format) */ if(typeof Handlebars !== "undefined") { Handlebars.registerHelper("currency", function(data){ if(!data) return $.jsFormControls.Format.currency(0); return $.jsFormControls.Format.currency(data); }); Handlebars.registerHelper("dec", function(data){ if(!data) return ""; return $.jsFormControls.Format.decimal(data); }); Handlebars.registerHelper("percent", function(data){ if(!data) return "0"; return $.jsFormControls.Format.decimal(data*100); }); Handlebars.registerHelper("date", function(data){ if(!data) return ""; return $.jsFormControls.Format.date(data); }); Handlebars.registerHelper("time", function(data){ if(!data) return ""; return $.jsFormControls.Format.time(data); }); Handlebars.registerHelper("datetime", function(data){ if(!data) return ""; return $.jsFormControls.Format.dateTime(data); }); Handlebars.registerHelper("dateTime", function(data){ if(!data) return ""; return $.jsFormControls.Format.dateTime(data); }); Handlebars.registerHelper("timespan", function(data){ if(!data) return ""; return $.jsFormControls.Format.humanTime(data); }); Handlebars.registerHelper("humanTime", function(data){ if(!data) return ""; return $.jsFormControls.Format.humanTime(data); }); Handlebars.registerHelper("byte", function(data){ if(!data) return ""; return $.jsFormControls.Format.byte(data); }); Handlebars.registerHelper("integer", function(data){ if(!data) return ""; return $.jsFormControls.Format.integer(data); }); } function JsFormControls(element) { this.element = element; // init the dom functionality this._domInit(); } /** * init the dom. This can be called multiple times. * this will also enable "add", "insert" and "delete" for collections * @private */ JsFormControls.prototype._domInit = function() { const location = $(this.element); // validation // check required (this is the first check) location.find("input.mandatory,textarea.mandatory").on("keyup", function(){ if(!$(this).hasClass("mandatory")) { return; } // check for "null" as value as well if($(this).val().length > 0 && $(this).val() !== "null") { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } }); location.find("input.mandatory,textarea.mandatory").on("change", function(){ if(!$(this).hasClass("mandatory")) { return; } if($(this).hasClass("object")) { if($(this).data().pojo) { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } return; } // check for "null" as value as well if($(this).val().length > 0 && $(this).val() !== "null") { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } }).trigger("change"); location.find("select.mandatory").on("change", function(){ if(!$(this).hasClass("mandatory")) { return; } // check for "null" as value as well if($(this).val() !== null && $(this).val() !== "null" && $(this).val().length > 0) { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } }).trigger("change"); if(window.flatpickr) { window.flatpickr.localize('_lib/3rdparty/flatpickr/l10n/de.js'); // Set localization global } // show datepicker for all inputs location.find("input.date").each(function() { let dateformat = null; const format = $(this).attr("data-format"); const $this = $(this); if(window.flatpickr) { window.flatpickr($this[0], { enableTime: false, allowInput: true, time_24hr: false, disableMobile: true, dateFormat: i18n.flatpickrDate, onOpen: [function(dt,value,inst){ if($this.val() === '') inst.jumpToDate(new Date()); else { const curDate = $.jsFormControls.Format.asDate($this.val()); inst.jumpToDate(curDate); inst.setDate(curDate, true); } }] }); } else if($this.datepicker) { // get date format if(typeof format !== "undefined") { dateformat = format; } else if(typeof i18n !== "undefined") { dateformat = i18n.jqdate; } // jquery ui if(dateformat) $(this).datepicker({dateFormat: dateformat}); else $(this).datepicker(); } }); /** * date-time picker */ location.find("input.dateTime,input.datetime").each(function(){ let dateformat = null; let format = $(this).attr("data-format"); const $this = $(this); if(window.flatpickr) { window.flatpickr($(this)[0], { enableTime: true, time_24hr: true, allowInput: true, disableMobile: true, dateFormat: i18n.flatpickrDateTime, minuteIncrement: 15, onOpen: [function(_a,_b,inst){ if($this.val() === '') inst.jumpToDate(new Date()); else { const curDate = $.jsFormControls.Format.asDate($this.val()); inst.jumpToDate(curDate); inst.setDate(curDate, true); } }] }); } else if($this.datetimepicker && $this.hasClass("form-control")) { if(format) { dateformat = format; } else if(typeof i18n !== "undefined" && i18n.date) { dateformat = i18n.date.format; } // convert to group const id = "DTID_" + $(this).attr("name").replace('.', '_'); const group = $('
'); group.attr("id", id); $this.parent().append(group); $this.addClass("datetimepicker-input") .attr("data-target", "#" + id); const addendum = $('
'); group.append($this); group.append(addendum); addendum.attr("data-target", "#" + id); if(!dateformat) { dateformat = "dd.MM.yyyy"; } dateformat = dateformat + " HH:mm"; // convert to php format foo datetime dateformat = FormatParser.toPhpString(dateformat); const dtOptions = { format: dateformat, /* parseDate: (date, format) => { console.log("parsedate", date, format, $.jsFormControls.Format.asDate(date)) return $.jsFormControls.Format.asDate(date); }, formatDate: (date, format) => { console.log("formatdate", date, format) return $.jsFormControls.Format.dateTime(data); }, formatMask: (format)=>{ return format .replace(/Y{4}/g, '9999') .replace(/Y{2}/g, '99') .replace(/M{2}/g, '19') .replace(/D{2}/g, '39') .replace(/H{2}/g, '29') .replace(/m{2}/g, '59') .replace(/s{2}/g, '59'); } */ }; $(this).datetimepicker(dtOptions); } }); /** * show/edit time */ location.find("input.time").each(function(){ // clockpicker requires parent to be clockpicker as well if($(this).clockpicker && $(this).parent().hasClass("clockpicker")) { $(this).attr("type", "text"); $(this).parent().clockpicker({ autoclose: true, placement: 'bottom-adaptive' }); } else if(window.flatpickr) { window.flatpickr($(this)[0], { enableTime: true, noCalendar: true, dateFormat: "H:i", time_24hr: true, disableMobile: true, minuteIncrement: 15, onOpen: [function(a,b,inst){ if($(inst._input).val() !== '') { const [hour, minutes] = $(inst._input).val().split(':'); $('#flatpickr-time').find('.flatpickr-hour').val(hour); $('#flatpickr-time').find('.flatpickr-minute').val(minutes); } }] }); } else if($(this).datetimepicker) { $(this).datetimepicker(); } }); // input validation (number) const numberRegexp = /^[0-9.,-]+$/; location.find("input.number").on("keyup", function(){ let val = $(this).val(); if($(this).hasClass("currency") && val) val = $.jsFormControls.Format._getNumber(val); if(val.length == 0) { return; } if($(this).hasClass("autoclean")) { $(this).val(val.replace(/[^0-9.,-]/g, "")); } else if(numberRegexp.test(val)) { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } }).trigger("keyup"); // currency formatting (add decimal) location.find("input.currency").each(function(){ $(this).on("change blur", function(){ const val = $(this).val(); if(val.length > 0) { $(this).val($.jsFormControls.Format.currency($.jsFormControls.Format._getNumber(val))); } }); $(this).on("focus", function(){ const val = $(this).val(); if(val.length > 0) { $(this).val($.jsFormControls.Format._getNumber(val)); } $(this).select(); }); }); location.find("input.percent").on("change", function(){ const cval = $(this).val(); if(cval.length > 0) { $(this).val($.jsFormControls.Format.decimal($.jsFormControls.Format._getNumber(cval))); } $(this).on("focus", function(){ const val = $(this).val(); if(val.length > 0) { $(this).val($.jsFormControls.Format._getNumber(val)); } $(this).select(); }); }); // decimal formatting (add decimal) location.find("input.decimal").on("change", function(){ const val = $(this).val(); if(val.length > 0) { $(this).val($.jsFormControls.Format.decimal($.jsFormControls.Format._getNumber(val))); } }); // variable unit location.find("input.vunit").on("change", function(){ let val = $(this).val(); if(val.length > 0) { // save the actual data val = $.jsFormControls.Format._getNumber(val); $(this).data().val = val; $(this).val($.jsFormControls.Format.vunit(val, $(this).attr("data-unit"))); } }); const integerRegexp = /\D+$/; location.find("input.integer").on("keyup", function(){ const val = $(this).val(); if(val.length == 0) return; if($(this).hasClass("autoclean")) { $(this).val(val.replace(/\d/g, "")); } else if(integerRegexp.test($(this).val())) { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } }).trigger("keyup"); // regular expression location.find("input.regexp").each(function(){ $(this).on("keyup", function(){ if($(this).hasClass("autoclean")) { $(this).data("regexp", new RegExp($(this).attr("data-regexp"), 'g')); } else { $(this).data("regexp", new RegExp($(this).attr("data-regexp"))); } const val = $(this).val(); if(val.length > 0) { const regexp = $(this).data("regexp"); if($(this).hasClass("autoclean")) { $(this).val(val.replace(regexp, "")); } else if(regexp.test($(this).val())) { $(this).addClass("valid").removeClass("invalid"); } else { $(this).removeClass("valid").addClass("invalid"); } } else if(!$(this).hasClass("mandatory")) { // if not mandatory: nothing is valid $(this).removeClass("invalid").addClass("valid"); } }).trigger("keyup"); $(this).on("change", function(){ $(this).trigger("keyup"); }); }); /* mardown wysiwyg */ location.find("textarea.markdownedit").each(function(){ // create div let $text = $(this); let editContainer = $('
'); editContainer.insertBefore($text); editContainer.css('height', $text.css('height')); // if fullEdit = true -> show all otherwise just simple formatting $text.data().editor = new toastui.Editor({ el: editContainer.get(0), toolbarItems: $text.data().fullEdit ? [ ['heading', 'bold', 'italic', 'quote'], ['ul', 'ol', 'task', 'indent', 'outdent'], ['table'] ] : [ ['bold', 'italic', 'quote'], ['ul', 'ol'] ], height: $text.height() + "px", //height: "450px", initialEditType: 'wysiwyg', initialValue: $('#PsychTextArea').val(), hideModeSwitch:true, events: { change: function() { $text.val($text.data().editor.getMarkdown()); $text.trigger("change"); } } }); // hide textarea $(this).hide(); $text.on("fill", function(){ $text.data().editor.setMarkdown($text.val()); }); }); /* rotatestate stontrol */ location.find("input.rotatestate").each(function(){ let states = $(this).attr("data-state-values"); let defaultClass = $(this).attr("data-state-class"); // no need to continue if there are no states if(!states) { return; } try { states = JSON.parse(states); } catch (ex) { // do not need to continue if we cannot parse the states return; } const stateControl = $(""); if($(this).attr("title")) { stateControl.attr("title", $(this).attr("title")); } if($(this).attr("data-state-style")) { stateControl.attr("style", $(this).attr("data-state-style")); } stateControl.data("states", states); stateControl.data("control", this); stateControl.data("activeState", null); $(this).data("control", stateControl); if(defaultClass) { stateControl.addClass(defaultClass); } // click on the control starts rotating stateControl.click(function(){ const cState = $(this).data().activeState; const cStates = $(this).data().states; const control = $(this).data().control; let newState = null; if(cState !== null) { // go to the 'next' state for(let i = 0; i < cStates.length; i++) { if(cStates[i].value === cState.value) { // last element if(i === cStates.length - 1) { newState = cStates[0]; } else { newState = cStates[i+1]; } break; } } } else { // no state yet - set the first entry as state newState = cStates[0]; } $(control).attr("value", newState.value); // trigger change $(control).trigger("change"); }); // make sure to update state if the value is changed $(this).on("change", function(){ const control = $($(this).data().control); const cState = control.data().activeState; const cStates = control.data().states; if(cState !== null) { // remove "old state" control.removeClass(cState['class']); } // add new State const val = $(this).val(); $.each(cStates, function(){ if(this.value === val) { control.data().activeState = this; if(this.title) { control.attr("title", this.title); } control.addClass(this['class']); return false; } }); }); // trigger initial state $(this).trigger("change"); $(this).after(stateControl); $(this).hide(); }); }; /** * validate a given form * @return true if the form has no invalid fields, false otherwise */ JsFormControls.prototype.validate = function() { // validation $(".required,.regexp,.date,.mandatory,.number,.validate", this.element).trigger("change"); // check for invalid fields return $(".invalid", this.element).length <= 0; }; /** * destroy the jsformcontrols and its resources. * @private */ JsFormControls.prototype.destroy = function( ) { return $(this.element).each(function(){ $(this).removeData('jsFormControls'); }); }; // init and call methods $.fn.jsFormControls = function ( method ) { // Method calling logic if ( typeof method === 'object' || ! method ) { return this.each(function () { if (!$(this).data('jsFormControls')) { $(this).data('jsFormControls', new JsFormControls( this, method )); } }); } else { const args = Array.prototype.slice.call( arguments, 1 ); // only one - return directly if(this.length == 1) { const jsFormControls = $(this).data('jsFormControls'); if (!jsFormControls) return; if(method.indexOf("_") !== 0 && jsFormControls[method]) { return jsFormControls[method](...args); } $.error( 'Method ' + method + ' does not exist on jQuery.jsFormControls' ); return false; } return this.each(function () { const jsFormControls = $.data(this, 'jsFormControls'); if (!jsFormControls) return; if(method.indexOf("_") !== 0 && jsFormControls[method]) { return jsFormControls[method](...args); } else { $.error( 'Method ' + method + ' does not exist on jQuery.jsFormControls' ); return false; } }); } }; /** * global jsForm function for intialization */ $.jsFormControls = function ( name, initFunc ) { let jsForms; // initFunc is a function -> initialize if(typeof initFunc === "function") { // call init if already initialized jsForms = JSFORM_MAP[name]; if(jsForms) { $.each(jsForms, function(){ initFunc(this, $(this.element)); }); } // remember for future initializations JSFORM_INIT_FUNCTIONS[name] = initFunc; } else { // call init if already initialized jsForms = JSFORM_MAP[name]; if(jsForms) { const method = initFunc; const args = Array.prototype.slice.call( arguments, 2 ); $.each(portlets, function(){ this[method](...args); }); } } }; $.jsFormControls.Format = { /** * format a string based on the classes in a dom element. * This will also set a proccessor to "revert" the data */ format: function(ele, cdata) { let $ele = $(ele); if($ele.hasClass("dateTime") || $ele.hasClass("datetime")) { if(isNaN(cdata)) return cdata; return $.jsFormControls.Format.dateTime(cdata); } else if($ele.hasClass("date")) { if(isNaN(cdata)) return cdata; return $.jsFormControls.Format.date(cdata); } else if($ele.hasClass("time")) { if(isNaN(cdata)) return cdata; // as a number: hhmm if($ele.hasClass("number")) { return $.jsFormControls.Format.timeNum(cdata); } else return $.jsFormControls.Format.time(cdata); } else if($ele.hasClass("currency")) { return $.jsFormControls.Format.currency(cdata); } else if($ele.hasClass("select")) { if(!cdata) return ""; if(!$ele.data().options || !$ele.data().options[cdata]) return cdata; // use "options" to convert return $ele.data().options[cdata]; } else if($ele.hasClass("bool")) { if(!cdata) return $ele.data().false || ''; return $ele.data().true || (i18n ? i18n.label.yes : 'X'); } else if($ele.hasClass("byte")) { if(isNaN(cdata)) return cdata; return $.jsFormControls.Format.byte(cdata); } else if($ele.hasClass("decimal")) { $ele.data().processor = $.jsFormControls.Format.getDecimal; return $.jsFormControls.Format.decimal(cdata); } else if($ele.hasClass("vunit")) { // save the actual data $(this).data().val = cdata; $ele.data().processor = $.jsFormControls.Format.getVunit; return $.jsFormControls.Format.vunit(cdata, $ele.attr("data-unit")); } else if($ele.hasClass("percent")) { $ele.data().processor = $.jsFormControls.Format.getPercent; return $.jsFormControls.Format.percent(cdata); } else if($ele.hasClass("percentage")) { $ele.data().processor = $.jsFormControls.Format.getPercent; return $.jsFormControls.Format.percent(cdata) + "%"; } else if($ele.hasClass("humantime")) { $ele.data().processor = $.jsFormControls.Format.getHumanTime; return $.jsFormControls.Format.humanTime(cdata); } else if($ele.hasClass("timespan")) { return $.jsFormControls.Format.timespan(cdata); } else if($ele.hasClass("phone")) { return $.jsFormControls.Format.phone(cdata); } else if($ele.hasClass("timeday")) { if(!cdata) return cdata; return $.jsFormControls.Format.time(Number(cdata*24*3600000)); } else if($ele.hasClass("humantimeday")) { if(!cdata) return cdata; $ele.data().processor = $.jsFormControls.Format.getHumanTime; return $.jsFormControls.Format.humanTime(Number(cdata*24*3600000)); } return cdata; }, /** * internal function that tries to identify where the value is. * This is necessary to support direct call, jqGrid and slickGrid * @param row * @param cell * @param value * @param columnDef * @param dataContext * @private */ _getValue: function(row, cell, value, _columnDef, _dataContext) { // if value is undefined: this is probably a direct call if(typeof cell === "undefined" && typeof value === "undefined") { return row; } // check for slickGrid: row/cell/value if(!isNaN(row) && typeof cell !== "undefined" && typeof value !== "undefined") { return value; } }, /** * format boolean into an ui-icon * @param value true or false * @returns the ui-icon span */ checkBox: function(row, cell, value, columnDef, dataContext) { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); if(value) { return ' '; } return ' '; }, /** * take a string and convert it into a number * @private */ _getNumber: function(num) { if (!num) { return null; } // default number format let numberformat = { groupingSeparator: ",", decimalSeparator: "." }; let pre = null, post = null; if(typeof i18n !== "undefined" && i18n.number) { numberformat = i18n.number; if(i18n.currency) { pre = i18n.currency.prefix; post = i18n.currency.suffix; } } else if(typeof $ !== "undefined" && $(document).data().i18n?.number) { numberformat = $(document).data().i18n.number; if($(document).data().i18n.currency) { pre = $(document).data().i18n.currency.prefix; post = $(document).data().i18n.currency.suffix; } } // make sure num is a string num = "" + num; // check for currency pre/postfix if(pre?.length > 0 && num.indexOf(pre) === 0) { num = num.substring(pre.length); } if(post?.length > 0 && num.indexOf(post) > 0) { num = num.substring(0, num.length - post.length); } // get rid of spaces num = num.trim(); // first check: only grouping and 2 positions afterwards const gs = num.indexOf(numberformat.groupingSeparator); // get rid of the grouping seperator (if any exist) if(gs !== -1) { if(gs >= num.length - 3) { if(numberformat.groupingSeparator !== ".") num = num.replaceAll(numberformat.groupingSeparator, "."); } else { num = num.replaceAll(numberformat.groupingSeparator, ""); } } // now convert the decimal seperator into a "real" decimal if(numberformat.decimalSeparator !== '.' && num.indexOf(numberformat.decimalSeparator) !== -1) { num = num.replaceAll(numberformat.decimalSeparator, "."); } // let javascript to the conversion to a number return Number(num); }, /** * @private */ _pad: function(val) { return ((val < 10) ? "0" : "") + val; }, /** * try parsing a string to date using i18n and libraries such as moment or luxon */ asDate: function(value) { const d = $.jsFormControls.Format.asMoment(value); // null if(!d) return null; // luxon if(d.toJSDate) return d.toJSDate(); // moment.js if(d.toDate) return d.toDate; // already date return d; }, /** * use luxon or moment.js to parse a datestring. * this will use: * - predefined i18n formats (strict mode) * - default momentjs (non-strict) * */ asMoment: function(value) { let m = null; const formats = [i18n.date.format + " " + i18n.date.timeFormat, i18n.date.dateTimeFormat, i18n.date.format, i18n.date.longDateFormat, i18n.date.timeFormat, // add common formats: "d.M.y H:m", "d.M.y", "d/M/y H:m", "d/M/y", "M/dd/yy HH:mm", "M/dd/yyyy HH:mm"]; if(value.toFormat) return value; // luxon parsing if(typeof luxon !== "undefined") { if(luxon.DateTime.isDateTime(value)) return value; //console.log("parsing ", value, formats, i18n.date) if(!isNaN(value)) { if(value.getTime) return luxon.DateTime.fromJSDate(value); return luxon.DateTime.fromMillis(value); } if(value.year) return luxon.DateTime.formObject(value); formats.forEach(function(format){ if(m) return false; try { const cur = luxon.DateTime.fromFormat(value, format); if(cur.isValid) { m = cur; return false; } } catch (ex) { // ignore } }); if(!m) { console.log("unable to parse " + value, formats); m = luxon.DateTime.fromISO(value); } return m; } // moment.js parsing if(typeof moment !== "undefined") { for(let i = 0; i < formats.length; i++) formats[i] = moment().toMomentFormatString(formats[i]); $.each(formats, function(){ if(m) return false; const cur = moment(value, this, true); if(cur.isValid()) { m = cur; return false; } }); if(!m) { m = moment(value); } } // try date if(!m) { return new Date(value); } m.toFormat = m.format; return m; }, asNumber: function(value) { return $.jsFormControls.Format._getNumber(value); }, /** * normalize phone number */ phone: function(phoneNumber) { if (!phoneNumber) { return phoneNumber; } // Remove all non-numeric characters except + phoneNumber = phoneNumber.replace(/[^0-9+]/g, ''); // Convert numbers starting with 00 to + if (phoneNumber.startsWith("00")) { phoneNumber = "+" + phoneNumber.slice(2); } // If the number doesn't start with +, it's likely a local number, return as is if (!phoneNumber.startsWith("+")) { return phoneNumber; } // Extract country code let countryCode, remainingNumber; if (phoneNumber.startsWith("+1")) { countryCode = "1"; remainingNumber = phoneNumber.slice(2); } else { const countryCodeLengths = [2, 3]; // European and other international country codes can be 2 or 3 digits long for (let len of countryCodeLengths) { countryCode = phoneNumber.slice(1, len + 1); // Skip the leading + remainingNumber = phoneNumber.slice(len + 1); if (remainingNumber.length > 0) { break; } } } let formattedNumber; if (countryCode === "1") { // Format North American numbers: +1 XXX-XXX-XXXX formattedNumber = remainingNumber.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3"); } else { // Format other international numbers with spaces every 3 digits (this can be customized) formattedNumber = remainingNumber.replace(/(\d{3})(?=\d)/g, "$1 "); } return `+${countryCode} ${formattedNumber.trim()}`; }, /** * convert a number to a byte */ byte: function(bytes) { if (bytes === "" || !bytes || isNaN(bytes)) { return bytes; } const unit = 1024; if (bytes < unit) return bytes + " B"; const exp = Math.floor(Math.log(bytes) / Math.log(unit)); const pre = "KMGTPE".charAt(exp-1) + "B"; return Math.round(bytes*10 / Math.pow(unit, exp))/10 + pre; }, /** * variable unit. this works by prefixing k(kilo) m(mega) g(giga) t(tera) */ vunit: function(value, unit) { if (value === "" || !value || isNaN(value)) { return value; } let neg = value < 0; if(neg) value *= -1; if(value < 1000) { return (neg?'-':'') + $.jsFormControls.Format.decimal(value) + ' ' + unit; } const un = 1000; const exp = Math.floor(Math.log(value) / Math.log(un)); const pre = "kmgtpe".charAt(exp-1) + unit; return (neg?'-':'') + $.jsFormControls.Format.decimal(Math.round(value*100 / Math.pow(un, exp))/100) + ' ' + pre; }, /** * @private */ decimal: function(num) { if (num === "" || !num || isNaN(num)) { return num; } // get number format let numberformat; if(typeof i18n !== "undefined" && i18n.number) { numberformat = i18n.number; } else if(typeof $ !== "undefined" && $(document).data().i18n?.number) { numberformat = $(document).data().i18n.number; } let n = num; const c = (Math.abs(num - Math.floor(num)) > 0.005) ? 2 : 0; const d = numberformat?.decimalSeparator || '.'; const t = numberformat?.groupingSeparator || ','; const i = parseInt(n = Math.abs( + n || 0).toFixed(c), 10) + ""; const il = i.length; const j = il > 3 ? il % 3 : 0; // convert to a nice number for display return (num<0 ? "-" : "") + (j ? i.substring(0, j) + t : "") + i.substring(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ""); }, /** * @private */ integer: function(num) { if (num === "" || !num || isNaN(num)) { return num; } // default number format let numberformat; if(typeof i18n !== "undefined" && i18n.number) { numberformat = i18n.number; } else if(typeof $ !== "undefined" && $(document).data().i18n?.number) { numberformat = $(document).data().i18n.number; } // convert to a nice number for display let n = num; const t = numberformat?.groupingSeparator || ','; const i = parseInt(n = Math.abs( + n || 0), 10) + ""; const il = i.length; const j = il > 3 ? il % 3 : 0; return (num<0 ? "-" : "") + (j ? i.substring(0, j) + t : "") + i.substring(j).replace(/(\d{3})(?=\d)/g, "$1" + t); }, getDecimal: function(val) { if (val === "") { return 0; } return $.jsFormControls.Format.asNumber(val); }, getVunit: function(val) { if (num === "") { return 0; } return Number(val); }, percent: function(num) { if (num === "" || !num || isNaN(num)) { return num; } return $.jsFormControls.Format.decimal(num*100); }, getPercent: function(val) { if (!val || val === "") { return 0; } if(val.indexOf("%") !== -1) val = val.substring(0, val.length-1); const num = $.jsFormControls.Format.getDecimal(val); return Number(num) / 100; }, /** * @private */ currency: function(row, cell, value, columnDef, dataContext) { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); if(!value) { if(cell) { return " "; } value = 0; } let num = $.jsFormControls.Format.decimal(value); // check for currency let pre = null, post = null; if(typeof i18n !== "undefined") { if(i18n.currency) { pre = i18n.currency.prefix; post = i18n.currency.suffix; } } else if($(document).data().i18n?.number) { if($(document).data().i18n.currency) { pre = $(document).data().i18n.currency.prefix; post = $(document).data().i18n.currency.suffix; } } if(pre) num = pre + num; if(post) num = num + post; return num; }, /** * @private */ dateTime: function(row, cell, value, columnDef, dataContext) { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); if(!value) { if(cell) { return " "; } return ""; } return (this.date(value) + " " + this.time(value)); }, /** * @private */ date: function(row, cell, value, columnDef, dataContext) { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); if(!value || value === "") { if(cell) { return " "; } return ""; } if(isNaN(value)) return value; // get date format let dateformat = null; if(typeof i18n !== "undefined") dateformat = i18n.date; else if($(document).data().i18n?.date) dateformat = $(document).data().i18n.date; if(typeof luxon !== "undefined") { return luxon.DateTime.fromMillis(Number(value)).toFormat(dateformat.shortDateFormat); } const d = new Date(); d.setTime(value); let year = d.getYear(); if(year < 1900) { year += 1900; } if($.format) { return $.format.date(d, dateformat.shortDateFormat); } else { // fallback: german version return this._pad(d.getDate()) + "." + this._pad((d.getMonth()+1)) + "." + this._pad(year); } }, timeNum : (row, cell, value, columnDef, dataContext) => { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); const h = (value < 1000 ? "0" : "") + Math.floor(value / 100) +""; const m = value % 100 +""; return h + ":" + ( m < 10 ? "0" : "") +m; }, /** * @private */ time: function(row, cell, value, columnDef, dataContext) { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); if(!value || value === "") { if(cell) { return " "; } return ""; } if(isNaN(value)) return value; let timeFormat = "HH:mm"; if(typeof i18n !== "undefined") { if(i18n.timeFormat) timeFormat = i18n.timeFormat; else if (i18n.date?.timeFormat) timeFormat = i18n.date.timeFormat; } else if($(document).data().i18n && typeof $(document).data().i18n.timeFormat !== "undefined") timeFormat = $(document).data().i18n.timeFormat; if(typeof luxon !== "undefined") { return luxon.DateTime.fromMillis(value).toFormat(timeFormat); } const d = new Date(); d.setTime(value); if($.format) return $.format.date(d, timeFormat); else return this._pad(d.getHours()) + ":" + this._pad(d.getMinutes()); // + ":" + pad(d.getSeconds()); don't need seconds }, /** * * @param value a string value to format * @param allowms true to allow komma (i.e. 00.00) * @return something in the form of 00:00.00 * @private */ timespan: function(row, cell, value, columnDef, dataContext) { value = $.jsFormControls.Format._getValue(row, cell, value, columnDef); if(!value) value = "0"; const tokens = value.split(":"); let allowkomma = false; // check each token for(let i=0; i 0) { out += h + "h "; // ignore seconds and milliseconds if we have hours s = 0; value = 0; } if (m > 0) { out += m + "m "; // ignore milliseconds value = 0; } if (s > 0) { out += s + "s "; value = 0; } if (value > 0) { out += value + "ms"; } // trim output return out.trim(); }, /** * convert a string with a time in human format back to a long. * This works for any combination of * Xh Xm xs xms * * @param val the value to convert */ getHumanTime: function(val) { if(!val || val === "") return 0; // go through val let result = 0; let num = ""; let tu = ""; let mult = val.charAt(0) === '-' ? -1 : 1; const convert = function(){ if(num === "") { return; } const curNum = Number(num); switch(tu) { case "ms": case "mill": result += curNum; break; case "s": case "secs": result += curNum * 1000; break; case "": case "m": case "min": case "minute": result += curNum * 60000; break; case "h": case "hour": result += curNum * 3600000; break; case "d": case "day": case "days": result += curNum * 24 * 3600000; break; } // reset tu = ""; num = ""; }; for(let i = 0; i < val.length; i++) { const c = val.charAt(i); switch(c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if(tu !== "") { // convert the old number convert(); } num += c; break; case 'm': case 'i': case 'n': case 's': case 'h': case 'o': case 'u': case 'r': case 'a': case 'e': case 'c': case 'y': case 'd': tu += c; break; default: // ignore } } // one more convert - just in case we missed something convert(); return result*mult; } }; })( jQuery, window ); /** * @returns the trimmed string */ String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); }; /* check start of a string */ String.prototype.startsWith = function(str) { if((this === null) || (this.length <= 0)) return false; if((str === null) || (str == "null") || (str.length <= 0)) return false; if(this.substr(0, str.length) == str) return true; return false; }; /* check start of a string */ String.prototype.startsWithIgnoreCase = function(str) { if((this === null) || (this.length <= 0)) return false; if((str === null) || (str == "null") || (str.length <= 0)) return false; if(this.substr(0, str.length).toLowerCase() == str.toLowerCase()) return true; return false; }; /* check end of a string */ String.prototype.endsWith = function(str) { if((this === null) || (this.length <= 0)) return false; if((str === null) || (str == "null") || (str.length <= 0) || (str.length > this.length)) return false; if(this.substr(this.length - str.length) == str) return true; return false; };