Source: view/easyui/jeasy-html.js


/** easyui html handler
 * <p>Easyui is html based api. All jclient integration is via html intervention.</p>
 * <p>This module is a helper for handling html tag attribute string parsing.</p>
 * @module jeasy/html */

if (J === undefined)
	console.error("You need initialize J - use <script>jeasy-api.js</script> first.");

/**This port instance should arleady understand application's prots
 * - initialized in project's frame */
const Port = jvue.Protocol.Port;

/** attribute names in html tag defining attres for handling by jeasy-html frame */
const ir = {
	/** target name, pager use ir-grid, grid use ir-t*/
	t: "ir-t",
	/** e.g. ds.sql-key in dataset.xml, shouldn't used in html in jeasy v1.0
	 * TODO Planning to support client defined query. */
	sk: 'ir-sk',

	root: 'ir-root',
	/** grid item on select handler name (according easyUI document, there is not 'onChange')*/
	onselect: "ir-onselect",
	onchange: "ir-onchange",

	oncheck: "ir-oncheck",
	onload: "ir-onload",
	oncheckAll: "ir-oncheck-all",

	/**EasyUI grid can be configured with sk.
	 * a.k.a. ir-grid, used by pager to specify grid id. */
	grid: 'ir-grid',

	/**EasyUI treegrid can be configured with sk.
	 * a.k.a. TODO ir-treegrid, used by pager to specify grid id. */
	treegrid: 'ir-treegrid',

	/* easyui "data-options", alias is defined here, e.g. alais = "field: personName" */
	ezDataopts: 'data-options',

	expr: 'ir-expr',
	/** Field name in form, which is to be loaded as record's alias, like "field" of easyui datagrid data-options*/
	field: 'ir-field',

	orderby: 'ir-orderby',

	groupby: 'ir-groupby',

	/** intial page size, later will using easyui pagination's size (by EzGrid.page()) */
	pagesize: 'ir-size',

	/** ? ? */
	checkexpr: 'ir-checkexpr',

	/** combobox */
	combobox: 'ir-cbb',

	/**EasyUI tree can be configured with sk. */
	tree: 'ir-tree',

	/**EasyUI combotree can be configured with sk. */
	cbbtree: 'ir-cbbtree',

	/** query from */
	query: 'ir-query',

	all: 'ir-all',

	/**The row in a grid/tree/cbb to be selected:
	 * ir-select = "field-name: variable" */
	select: 'ir-select',

	/** Modal dialog form tag, value = callback-name: ir-modal='onModal' */
	modal: 'ir-modal',

	deflt: {
		gridId: 'irlist',
		treeId: 'irtree',
		cbbtreeId: 'cbbirtree',
		pagerId: 'irpager',
		queryId: 'irquery',
		modalId: 'irmodal',
		cbbId: 'ircbb',
		_All_: '-- ALL --',
	},
};

/* test helper:
 * http://www.regular-expressions.info/javascriptexample.html
 */
const regex = {
	/**reqex for ir-t maintable
	 * [0] aUser:b
	 * [1] aUser
	 * [2] :b
	 * [3] b */
	maintbl: /\s*(\w+)\s*(:\s*(\w+))?/i,

	// deprecated join: /\s*([jJrRlL])\s*:\s*(\w+)(\s*:\s*(\w+)){0,1}\s+(.+)\s*/i,
	/**regex for matching join definition is html.<br>
	 * deprecated :<br>
	 * [0] j:b_cates:t1   person=b_cates.persId 'v' % t1.col <br>
	 * [1] j <br>
	 * [2] b_cates <br>
	 * [3]  : t1 <br>
	 * [4] t1 <br>
	 * [5] person=b_cates.persId 'v' % t1.col <br>

	 * [0] j:{@tb}:a a.x=b.y <br>
	 * [1] j <br>
	 * [2] {@ <br>
	 * [3] tb <br>
	 * [4] } <br>
	 * [5] :a <br>
	 * [6] a <br>
	 * [7] a.x=b.y <br>

	 * [0] j:tb :a a.x=b.y and a.z=c.w <br>
	 * [1] j <br>
	 * [2] undefined <br>
	 * [3] tb <br>
	 * [4] undefined  <br>
	 * [5] :a <br>
	 * [6] a <br>
	 * [7] a.x=b.y and a.z=c.w <br>
	 */
	join: /\s*([jJrRlL])\s*:\s*(\{\@)?\s*(\w+)\s*(\})?(\s*:\s*(\w+)){0,1}\s+(.+)\s*/i,

	/**Regex for replacing variable in joining's ON condition.<br>
	 * [0] abc = {@ x.y} devi=ccd
	 * [1] abc =
	 * [2] x.y
	 * [3] devi=ccd
	 * FIXME V0.2 we can parsing multiple variable expression. Here is an example:
	 * [0] abc = {@ x.y} devi=ccd abc = {@ x.y} devi=ccd
	 * [1] abc = {@ x.y} devi=ccd abc =
	 * [2] x.y
	 * [3] devi=ccd
	 * */
	onCondParm: /\s*(.*)\{\@\s*(.+)\s*\}(.*)/i,

	/* e.g. b_articles.pubDate desc
	 * TEST1: [0]a.FullPath desc [1]a. [2]a [3]FullPath [4] desc [5]desc
	 * TEST2: [0]FullPath desc [1]undefined [2]undefined [3]FullPath [4] desc [5]desc
	 * TEST3: [0]FullPath [1]undefined [2]undefined [3]FullPath [4] undefined [5]undefined
	 */
	order: /\s*((\w+)\.){0,1}(\w+)(\s+(asc|desc|ASC|DESC){0,1}){0,1}\s*/i,

	/**regex for matching expr like "field:sqlAlias"*/
	alais: /field\s*:\s*\'(\w+)\'/i,

	/**
	 * [0] x.y.z.c
	 * [1] x
	 * [2] y.z.c
	 */
	vn: /\s*(\w+)\.(.*)/i,

	/** Parse string like "ds.sql-key arg1, {@obj.var1}, arg2, ..."*/
	cbbArg: /\{\@\s*(.+)\s*\}/i,

	/**Add # at start if none
	 * @param {string} str
	 * @param {string} defltStr default if  str is undefined or null
	 * @return {string} "#str" */
	sharp_: function (str, defltStr) {
		if (str === undefined || str === null )
			str = defltStr;
		if (typeof str === "string" && str.substring(0, 1) !== '#')
			return '#' + str;
		return str;
	},

	/**Add # at start if none
	 * @param {string} str string with or without a starting '#'
	 * @return {string} "str" without starting '#' */
	desharp_: function (str) {
		if (typeof str === "string" && str.substring(0, 1) === '#')
			return str.substring(1);
		return str;
	},

	/**split target with <i>separator</i> then the the ith element
	 * @param {string} target
	 * @param {string} separator
	 * @param {int} ith optinal.<br>If undefined, return all splitted array
	 * @return {Array|string} the ith element or all the array.
	 */
	split: function (target, separator, ith) {
		if (target === undefined) { return; }

		if (separator === undefined) {
			console.error('can not separate', separator, target);
			return;
		}

		target = target.trim();

		if (separator != ',') {
			console.error('Your separator not supported yet...', separator);
		}

		var ss = target.split(/\s*,\s*/);

		if (ith !== undefined) { return ss[ith]; }
		else { return ss; }
	},

	isblank: jvue.Jregex.isblank,
};

/**[Internal API] html tag's attribute parser.
 * Parsing ir-expr="max(t.col)", etc.
 */
function Tag (debug) {
	this.debug = debug;

	/**Try supplement jsvar with html tag's attributes
	 * @param {any} jsvar
	 * @param {string} targId html tag id.
	 * @param {string} attr html attribute name */
	this.merge = function(jsvar, tagId, attr) {
		if (jsvar !== undefined && jsvar !== null)
			return jsvar;
		if (typeof tagId === 'string' && typeof attr === 'string') {
			tagId = regex.sharp_(tagId);
			return $(tagId).attr(attr);
		}
	};

	/**<p>Try merge arg references in sk string into opts.sqlArgs, rethrn the splited sk.</p>
	 * Output: opts.sqlArgs<br>
	 * Input: opts.args, sk string<br>
	 * That means this function also change data in opts.
	 * @param {Object} opts
	 * @param {string} sk string like that from ir-tree, ir-cbb, etc.
	 * @return {Array} sk (first splited string in parameter sk) */
	this.mergargs = function(opts, sk) {
		// if (sk === undefined || sk === null)
		// 	console.error(ir.cbbtree + " attr in " + treeId + " is undefined. In jeasy v1.0, only configurd combobox is suppored (must have ir-sk).");
		// else {
		// 	var parsed = tag.parseSk(sk, opts.args);
		// 	sk = parsed.shift();
		// 	if (sqlArgs === undefined)
		// 		sqlArgs = [];
		// 	sqlArgs = sqlArgs.concat(parsed);
		// }

		// if (sk === undefined || sk === null)
		// 	console.error("sk attr in is undefined. In jeasy v1.0, only configurd dataset is suppored - must have sk in ir-tree, ir-combbox etc.",
		// 			opts, sk);
		// else {
			var parsed = tag.parseSk(sk, opts.args);
			sk = parsed.shift();
			if (opts.sqlArgs === undefined)
				opts.sqlArgs = [];
			else if (typeof opts.sqlArgs === 'string')
				opts.sqlArgs = [opts.sqlArgs];
			opts.sqlArgs = opts.sqlArgs.concat(parsed);
			return sk;
		// }
	};

	/**Format table-joins request array: [{tabl, t, on, as}], used for QueryReq.join(...).
	 * @param {string} t "b_articles, j:b_cate, l:b_author:a authorId=authorId and a.name like 'tom'"
	 * @return {Array} [{tabl, t, on, as}], where t = main-table | j | r | l
	 */
	this.joins = function (t, joins) {
		var tss = t.split(','); // [t1, j:b_cates[:alais] cateId=cateId, ...]
		var tabls = new Array();

		for(var i = 0; i < tss.length; ++i) {
			var m = regex.join.exec(tss[i]);
			if(m) {
				// [0] j:{@tb}:a a.x=b.y[1] j
				// [2] {@  (undefined)	[3] tb		[4] } (undefined)
				// [5] :a				[6] a		[7] a.x=b.y

				// try match variable in ON condition
				var oncond = m[7];
				// mOnVar = x.y
				var mOnVar = regex.onCondParm.exec(m[7]);
				if (mOnVar) {
					// if there is variable in on condtion clause, replace with value
					// No need to parse all logic condition to array
					var v = this.findVar(mOnVar[2]);
					if (typeof v !== "undefined") {
						// if (typeof v.length === "number")
						if (Array.isArray(v))
							oncond = mOnVar[1] + this.concatArray(v);
						else
							oncond = mOnVar[1] + "'" + v + "'";
					}
					else {
						if (this.debug) console.log('WARN - found parameter condition '
								+ m[7] + ' in table joining, but no variable can be used to replace the variable.' )
						oncond = mOnVar[1] + "'" + mOnVar[2] + "'";
					}
					if (mOnVar.length > 2)
						oncond = oncond + mOnVar[3];
				}

				//tabls.push({"t": m[1], "tabl": m[2], "as": m[4], "on": m[5]});
				if (m[2] === '{@' && m[4] === '}') {
					// sub select joining
					if (joins === undefined || typeof joins[m[3]] !== 'object') {
						console.error('You sepcified using sub query as joining table, but can not found the definition',
										m, joins);
					}
					else {
						var subselect = joins[m[3]];
						tabls.push({"t": m[1], "tabl": subselect, "as": m[6], "on": oncond});
					}
				}
				else
					tabls.push({"t": m[1], "tabl": m[3], "as": m[6], "on": oncond});
			}
			else {
				var mt = regex.maintbl.exec(tss[i]);
				// " aUser:b" = aUser:b, aUser, :b, b
				if (mt[1] === undefined)
					console.error("Can't parse main table: " + tss[i]);
				else
					tabls.push({"t": "main-table", "tabl": mt[1], "as": mt[3]});
			}
		}
		return tabls;
	};

	/** Convert js array into stirng quoted with "'"
	 * @param {array} arr
	 * @return {string} ('el1', ...)
	 */
	this.concatArray = function (arr) {
		var buf = "(";
		for (var ix = 0; ix < arr.length; ix++) {
			if (buf !== "(")
				buf += ", ";
			buf += "'" + arr[ix] + "'";
		}
		return buf + ")";
	};

	/**Find j-orderby tag and compose order-by request array:<br>
	 * [{"tabl": tabl, "field": column, "asc": "asc/desc"}, ...]
	 * @param {string} ordstr string in j-order="t.col asc/desc"
	 * @param {string} maintabl
	 */
	this.orders = function(ordstr, maintabl) {
		if (typeof ordstr !== 'string')
			return;

		var orders = new Array();
		var ordss = ordstr.split(',');
		for(var i = 0; i < ordss.length; ++i) {
			var match = regex.order.exec(ordss[i]);
			if (match) {
				var asc = "asc";
				if(typeof match[5] != "undefined" && match[5] == "desc")
					asc = "desc";
				var tabl = maintabl;
				if(match[2] !== undefined)
					tabl = match[2];
				if(match[3] === undefined) {
					// col can't be null
					alert("Someting wrong in html: " + ordstr);
					return orders;
				}
				// orders.push({"tabl": tabl, "col": match[3], "asc": asc });
				orders.push( [tabl + "." + match[3], asc] );
			}
		}
		return orders;
	};

	this.groups = function(grpstr) {
		if (typeof grpstr !== 'string')
			return;

		var grps = new Array();
		var grpss = grpstr.split(',');
		return grpss;
	};

	/**Match expr in "target" with regexAlais.
	 * @param {string} target: string to be matched
	 * @return {string} alais name of sql expr
	 * */
	this.findAlais = function (target) {
		var match = regex.alais.exec(target);
		if(match)
			return match[1];
		else {
			console.log("ERROR - can't parsing field expression: " + target);
			return target;
		}
	};

	/**find var from string like "x.y.z"
	 * If not found, the vn will be returned, no error reported.
	 * This is for auto form bounding, where this is common for many widgets.
	 * @param {string} vn var name
	 * @param {Object} argPool args buffer for looking varialbkles, if not found, try in window globaly.
	 * @return {Object} value represented by vn, e.g. "x.y.z" */
	this.findVar = function (vn, argPool) {
		// vn is a global variable
		if (window[vn] !== undefined)
			return window[vn];
		// vn is a variable in argPool
		if (argPool !== undefined && argPool[vn] !== undefined)
			return argPool[vn];

		// now vn must has at least one "."
		var v = window;
		var field;

		var vnss = regex.vn.exec(vn);
		// found a null value
		if (vnss === null)
			// return null;
			return vn;

		// does arg pool has the variable?
		if (argPool !== undefined && argPool[vnss[1]] !== undefined) {
			v = argPool[vnss[1]];
			field = vnss[2];
			vnss = regex.vn.exec(vnss[2]);
		}

		while (vnss) {
			if (v[vnss[1]]) {
				v = v[vnss[1]];
				field = vnss[2];
				vnss = regex.vn.exec(vnss[2]);
			}
			else break;
		}

		if (v === window) {
			if (window[vn])
				return window[vn];
			else {
				// console.error("Can't find variable for " + vn);
				return vn;
			}
		}
		else if (typeof v === "object") {
			if (typeof v[field] === "function")
				return v[field]();
			else return v[field];
		}
		else return v;
	};

	/** Parse String like "ds.sql-key arg1, {@obj.var1}, arg2, ..."
	 * @param {string} irsk
	 * @param {Object} argBuff
	 * @return {Array} [0] ds, [1] sql-key, [2] argBuff + [arg1, var1, arg2, ...]
	 */
	this.parseSk = function (irsk, argBuff) {
		if ( irsk === null || typeof irsk === "undefined" )
			return [];

		var args = [];
		var skss = irsk.trim().split(",");

		var sk = skss[0];
		if (sk.substring(0, 4) === "cbb.") {
			// fault tolerance to old version
			console.warn("sk's table name (cbb) ignored. In jeasy v1.0, there is only 1 xml table configued in dataset.xml.")
			sk = sk.substring(4);
		}
		args.push( sk );

		// replace variables
		// var regexCbbArg = /{\@\s*(.+)\s*\}/g;
		for ( var ix = 1; ix < skss.length; ix++ ) {
			var mOnVar = regex.cbbArg.exec( skss[ix] );
			if ( mOnVar ) {
				var v = tag.findVar( mOnVar[1], argBuff );
				args.push (v);
			}
			else
				args.push (skss[ix]);
		}
		return args;
	};

	/** Parse String like "ds.sql-key arg1, {@obj.var1}, arg2, ..."
	 * @param {string} irselect e.g. "roleId: {@roleId}, ...", or "roleId: {@role.roleId}"
	 * @param {Object} argBuff e.g. {roleId: 'aaa'}
	 * @return {Array} e.g. {roleId: roleId's value('aaa'), ...}
	this.parseSelect = function (irselect, argBuff) {
		if ( irselect === null || irselect === undefined )
			return {};

		var args = {};
		var nvss = irselect.trim().split(",");

		// replace variables
		// var regex.cbbArg = /\{\@\s*(.+)\s*\}/g;
		for ( var ix = 1; ix < nvss.length; ix++ ) {
			var nvs = nvss[ix].split(":");
			var mOnVar = regex.cbbArg.exec( nvs[1] );
			var nv = {}	// found selecting n-v
			if ( mOnVar ) {
				var v = tag.findVar( mOnVar[1], argBuff );
				nv[nvs[0].trim()] = v;
			}
			else
				nv[nvs[0].trim()] = nvss[ix];
			Object.assign(args, nv);
		}
		return args;
	}
	 */
};
const tag = new Tag(jeasy.log);

/**[Internal API] Common handlers for ir attributes, like ir-t, ir-list etc.*/
function EzHtml (J) {
	/**Get attr value from tag-id */
	this.ir = function (tagId, atr) {
		// if (tagId.substring(0, 1) != "#")
		// 	tagId = "#" + tagId;
		tagId = regex.sharp_(tagId);
		var a = $(tagId).attr(atr);
		return a;
	};

	this.has = function (tagId, atr) {
		var a = this.ir(tagId, atr);
		return a !== undefined;
	};

	/**@deprecated: as all options using opts() merging attributes, this function should deprecated.
	 * Parse table and joinning definition in html
	 * @param {string} pagerId pager id. default "irpager"
	 * @return {Array} [{t: "main-table/j/r/l", tabl: "table-1", as: "alais", on: cond-string}]<br>
	 * where cond-string = "col1=col2"
	 */
	this.tabls = function (gridId, extJoins) {
		var t = this.ir(gridId, ir.t);
		if (t === undefined)
			console.error("In jeasy api v1.0, pager's t is moved to list/grid/details-form's tag - must presented there.");
		else
			return tag.joins(t, extJoins);
	}

	/**Find th-expr definitons in easyui list/grid head.
	 * @param {string} gridId list id to be bind
	 * @param {string} defltabl main table name, used as html ignored table name
	 * @return {Array} [{expr: col, gfunc, tabl, as: c1}, ...]
	 */
	this.thExprs = function (gridId, defltabl) {
		if (defltabl) defltabl = defltabl.trim();

		var ths = $(gridId + " th");
		var exprs = new Array();

		var al_k = {}; // for checking duplicated alais

		for(var i = 0; i < ths.length; ++i) {
			var expr = {};
			var th = ths[i];
			var al = tag.findAlais($(th).attr(ir.ezDataopts));
			expr.alais = al;

			if(al_k[al]) {
				console.log("WARN - found duplicating alais: " + al + ". Ignoring...");
				continue;
			} else al_k[al] = true;

			// can handle raw expr (user specified expression)
			var exp = $(th).attr(ir.expr);
			if (typeof exp === "string")
				exprs.push({exp: exp, as: al});
			else
				exprs.push({exp: al, as: al});
		}
		return exprs;
	};

	/**Find field-expr definitons in easyui list/grid head.
	 * @param {string} gridId list id to be bind
	 * @param {string} defltabl main table name, used as html ignored table name
	 * @return {Array} [{expr: col, gfunc, tabl, as: c1}, ...]
	 */
	this.formExprs = function (formId, defltTabl) {
		var exprs = [];
		$(formId + " ["+ ir.field + "]").each( function(key, domval) {
			var as = this.attributes[ir.field] === undefined ?
						undefined : this.attributes[ir.field].value;
			var expr = this.attributes[ir.expr] === undefined ?
						undefined : this.attributes[ir.expr].value;
			if (expr === undefined)
				expr = as;
			if (defltTabl)
				// FIXME why thExprs() don't need a table alias?
				exprs.push({exp: defltTabl + "." + expr, as: as});
			else
				exprs.push({exp: expr, as: as});
		} );
		return exprs;
	};

	/**Collect all cheched items from easyui treee.
	 * @param {string} treeId
	 * @param {Object} opts options:<br>
	 * cols: {p1: v1, p2: v2, ...}<br>
	 * 		p: the DB field name, v: the easy tree item's propterty where the value will be got.<br>
	 * append: {p1: v1, ...} appending values, e.g. {role: '0101'} is used to get all functions of role 0101.<br>
	 * check: checked column name, OPTIONAL<br>
	 * @return {Array} columns to be insert / update, etc.<br>
	 *	 [ [ [ "funcId", "1A"   ],		- value-row 0, col 0 (funcId)<br>
	 *	     [ "roleId", "0101" ],		- value-row 0, col 1 (this.roleId == '0101')<br>
	 *	   ],<br>
	 *	   [ [ "funcId", "0101" ],		- value-row 1, col 0 (funcId)<br>
	 *	     [ "roleId", "0101" ],		- value-row 1, col 1 (this.roleId == '0101')<br>
	 *	 ]<br>
	 *	 this means two records will be inserted like<br>
	 *	 insert a_role_funcs (funId, roleId) values ('1A', '0101'), ('0101', '0101')<br>
	 * TODO may be we can support ir-checkTreeItem ="field: value, ..."?, but user must know easyUI item's properties?
	 */
	this.checkedTreeItem = function (treeId, opts) {
		var nodes;
		if (opts !== undefined && typeof opts.eztype === 'string')
		 	nodes = $(regex.sharp_(treeId, ir.deflt.treeId))[opts.eztype]('getChecked');
		else
			// use tree as default
		 	nodes = $(regex.sharp_(treeId, ir.deflt.treeId)).tree('getChecked');

		if (nodes === undefined) {
		 	if (jconsts.verbose < 3) {
				console.error('can not collect checked items. ', treeId, opts);
			}
			return [];
		}

		var eaches = new Array();
		var colnames = new Array();
		var first = true;
		for (var i = 0; i < nodes.length; i++) {
			var r = [];

			if (opts.cols) {
				Object.keys(opts.cols).forEach(function (k, ix) {
					// opts.cols.k = ez-name, so r.k <= node[i].name's value
					r.push([k, nodes[i][opts.cols[k]]]);
					if (first) {
						colnames.push(k);
					}
				});
			}

			if (opts.append) {
				Object.keys(opts.append).forEach(function (k, ix) {
					r.push([k, opts.append[k]]);
					if (first) {
						colnames.push(k);
					}
				});
			}
			first = false;
			// Object.assign(r, opts.append);

			// TODO if supporting ir-checkTreeItem, we need handling variables
		    eaches.push(r);
		}
		return [colnames, eaches];
	};

	/**Merget js arg (opts) with html tag(#tagId)'s attributes,
	 * with the js args ovrriding html attributes
	 * - except arrays like sqlArgs, they are concated.
	 * @param {string} tagId tag id that alredy sharped
	 * @param {Object} opts options to be merged (overriding tag attributes)<br>
	 * Options of sqlArgs, args and select are not handled.<br>
	 * opts.conn: connection id. if not defined, using jconsts.conn from jeasy-api.<br>
	 * opts.t: ir-t value, a function branch tag. in jeasy v1.0, only used by stree.serv<br>
	 * opts.maintabl: target db table. If undefined, try split from first of ir-t<br>
	 * opts.sk: semantic key string with parameters, like roels.ez, {@obj1.var1}<br>
	 * opts.all: add an "-- ALL --" item<br>
	 * opts.query: query form id, ir-query<br>
	 * opts.root: ree root ID<br>
	 * opts.cbb: combobox sk<br>
	 * opts.cbbtree: combotree sk<br>
	 * opts.tree: tree sk<br>
	 * opts.grid: TODO<br>
	 * opts.treegrid: treegrid sk<br>
	 * opts.pagesize: page size, if parameter is -1 or undefined, will be overriden by tag attribute<br>
	 * opts.select: selected id <br>
	 * opts.onclick: on click function or function name<br>
	 * opts.onselect: on item select function or function name<br>
	 * opts.onload: on load function or function name<br>
	 * opts.oncheck: on check function or function name<br>
	 * opts.oncheckAll: on check all function or function name<br>
	 * opts.onerror: on error call back, merged with EasyMsger.error(m.fail)<br>
	 * opts.onok: on ok call back, merged with EasyMsger.info(m.ok)<br>
	 * opts.joins: UserReq | QueryReq, user defined sub-query used as joining table.
	 * <b>note</b>: pk is not handled here
	 * @return {Object} merged options
	 */
	this.opts = function (tagId, opts) {
		if (typeof opts !== 'object')
			opts = {};
		if (opts) {
			opts.conn = tag.merge(opts.conn, tagId, ir.conn);
			if (opts.conn === undefined || opts.conn === null) {
				opts.conn = jconsts.conn;
			}

			opts.t = tag.merge(opts.t, tagId, ir.t);
			// try find maintable
			if (typeof opts.maintabl !== 'string' && typeof opts.t === 'string') {
				var tbls = tag.joins(opts.t, opts.joins);
				opts.maintabl = tbls[0].tabl;
			}

			opts.sk = tag.merge(opts.sk, tagId, ir.sk);
			opts.all = opts.all || EasyHtml.has(tagId, ir.all);
			opts.query = tag.merge(opts.query, tagId, ir.query);
			opts.root = tag.merge(opts.root, tagId, ir.root);
			opts.orderby = tag.merge(opts.orderby, tagId, ir.orderby);
			opts.groupby = tag.merge(opts.groupby, tagId, ir.groupby);

			opts.select = tag.merge(opts.select, tagId, ir.select);
			if (typeof opts.select === 'string') {
				opts.select = tag.findVar(opts.select, opts.args);
			}

			opts.cbb = tag.merge(opts.cbb, tagId, ir.combobox);
			opts.cbb = tag.mergargs(opts, opts.cbb);

			opts.grid = tag.merge(opts.grid, tagId, ir.grid);
			opts.grid = tag.mergargs(opts, opts.grid);

			opts.treegrid = tag.merge(opts.treegrid, tagId, ir.treegrid);
			opts.treegrid = tag.mergargs(opts, opts.treegrid);

			opts.cbbtree = tag.merge(opts.cbbtree, tagId, ir.cbbtree);
			opts.cbbtree = tag.mergargs(opts, opts.cbbtree);

			opts.tree = tag.merge(opts.tree, tagId, ir.tree);
			opts.tree = tag.mergargs(opts, opts.tree);

			if (opts.pagesize < 0) {
				opts.pagesize = undefined;
			}
			opts.pagesize = tag.merge(opts.pagesize, tagId, ir.pagesize);
			if (opts.pagesize === undefined) opts.pagesize = -1;

			opts.onclick = tag.merge(opts.onclick, tagId, ir.onclick);
			if (typeof opts.onclick === 'string') {
				opts.onclick = eval(opts.onclick);
			}

			opts.onselect = tag.merge(opts.onselect, tagId, ir.onselect);
			if (typeof opts.onselect === 'string')
				opts.onselect = eval(opts.onselect);

			opts.onchange = tag.merge(opts.onchange, tagId, ir.onchange);
			if (typeof opts.onchange === 'string')
				opts.onchange = eval(opts.onchange);

			opts.onload = tag.merge(opts.onload, tagId, ir.onload);
			if (typeof opts.onload === 'string')
				opts.onload = eval(opts.onload);

			opts.oncheck = tag.merge(opts.oncheck, tagId, ir.oncheck);
			if (typeof opts.oncheck === 'string')
				opts.oncheck = eval(opts.oncheck);

			opts.oncheckAll = tag.merge(opts.oncheckAll, tagId, ir.oncheckAll);
			if (typeof opts.oncheckAll === 'string')
				opts.oncheckAll = eval(opts.oncheckAll);

			// opts.onerror = tag.merge(opts.onerror, tagId, ir.onerror);
			if (typeof opts.onerror === 'string') {
				opts.onerror = eval(opts.onerror);
			}
			else if (typeof opts.onerror === undefined) {
				opts.onerror = EasyMsger.error;
			}

			if (typeof opts.onok === 'string') {
				opts.onok = eval(opts.onok);
			}
			else if (typeof opts.onok === undefined) {
				opts.onok = EasyMsger.ok;
			}
		}

		Object.keys(opts).forEach(function (k, ix) {
			if(k === undefined || opts[k] === undefined || opts[k] === null)
				delete opts[k];
		});

		return opts;
	};

	/** bind easy ui objects, replacing EzGrid.bindPage(), EzTree.bind().
	 * @param {string} eztype, 'treegrid' | 'datagrid' | 'tree' | 'combotree'
	 * @param {string} tagId
	 * @param {object} opts. See EzHtml#opts(), in addition of
	 * opts.style: addint style to easy object, e.g. height: 81px.
	 * @return {any} any of the selected easyUI function returned, e.g. return of 'datagird()'
	 */
	this.ez = function (eztype, tagId, json, opts) {
		var ezOpts = { data: json, }
		jeasy.mainRow(tagId, undefined);
		if (typeof opts === 'object')
			// ezOpts = Object.assign(ezOpts, opts);
			ezOpts = Object.assign(ezOpts, {
				onSelect: function(ix, nn) {
					if (typeof ix === 'number')
						node = nn;
					else if (typeof ix === 'object')
						node = ix;
					else console.error('TODO ............');

					jeasy.mainRow(tagId, node);
					if(typeof opts.onselect === "function")
						opts.onselect(ix, node)
			},
			onCheck: function(ix, nn){
				if (typeof ix === 'number')
						node = nn;
					else if (typeof ix === 'object')
						node = ix;
					else console.error('TODO ............');

					jeasy.mainRow(tagId, node);
				if(typeof opts.oncheck === "function")
						opts.oncheck(ix, node)
			},

			onClick:function(ix, nn) {
				if (typeof ix === 'number')
						node = nn;
					else if (typeof ix === 'object')
						node = ix;
					else console.error('TODO ............');

				jeasy.mainRow(tagId, node);

				if(typeof opts.onclick === "function")
						opts.onclick(ix, node)
			},
			onLoadSuccess: function(node, data) {
				//jeasy.mainRow(tagId, node);
				if(typeof opts.onload === "function")
						opts.onload(node, data)
			},
			style: "height: 81px"
			});

		return $(regex.sharp_(tagId))[eztype](ezOpts);
	};
};
const EasyHtml = new EzHtml(J);

function EzCbb (J) {
	/**bind combobox
	 * @param {string} cbbId combobox id
	 * @param {Object} opts<br>
	 * sk: semantic key string with parameters, like roels.ez, {@obj1.var1}<br>
	 * args: arguments buffer where to find variables needed by sk<br>
	 * sqlArgs: arguments directly provided and send back to server - needed by sk<br>
	 * select: selected id<br>
	 * all: add an "-- ALL --" item<br>
	 * onselect: onselect function or function name<br>
	 */
	this.combobox = function(cbbId, opts) {
		// this.combobox = function(cbbId, sk, argbuff, selectId, _All_, onChangef) {
		cbbId = regex.sharp_(cbbId, ir.deflt.cbbId);
		opts = EasyHtml.opts(cbbId, opts);
		var cbb = $(cbbId);

		var sk = opts.cbb;

		// request JMessage body
		var req = new jvue.DatasetCfg(	// s-tree.serv (SemanticTree) uses DatasetReq as JMessage body
					jconsts.conn,		// connection id in connexts.xml
					sk);				// sk in dataset.xml
		req.args(opts.sqlArgs);

		// all request are created as user reqs except query, update, insert, delete and ext like dataset.
		// DatasetReq is used as message body for semantic tree.
		// Port.stree is port of SemanticTree.
		// t=load/reforest/retree
		// user act is ignored for reading
		var jmsg = ssClient.userReq(jconsts.conn, Port.dataset, req);

		// get data, then bind easyui combotree
		ssClient.commit(jmsg, function(resp) {
			if (jconsts.verbose > 5) console.log(resp);
			var cbb = $(cbbId);
			// var rows = resp.data;
			var rows = jeasy.rows(resp);
			if (opts.all)
				rows.unshift({text: ir.deflt._All_, value: ir.deflt._All_});
			cbb.combobox({
				data: rows,
				multiple: opts.multi !== undefined && opts.multi !== null && opts.multi === true,
				onSelect: typeof opts.onselect === "function" ? opts.onselect : function(e) {
					if (jeasy.log) console.log(e);
				},
				onChange: typeof opts.onchange === "function" ? opts.onchange : function(e) {
					if (jeasy.log) console.log(e);
				}
			});
			if(typeof(opts.select) === "string")
				cbb.combobox('setValue', opts.select);
			else if(typeof(opts.select) === "number")
				cbb.combobox('setValue', opts.select);
			//select is true,default first row
			else if(opts.select === true)
				cbb.combobox('setValue', rows[0].value);
		});
	};

	this.getValue = function(cbbId) {
		var cbb = $(regex.sharp_(cbbId));
		if (cbb)
			return cbb.combobox('getValue');
	};
};
const EasyCbb = new EzCbb(J);

function EzTree(J) {
	this.J = J;

	this.log = true,
	this.alertOnErr = true,

	/**Bind configured dataset to easyui combotree.
	 * @param {string} treeId
	 * @param {Object} opts<br>
	 * sk: semantic key string with parameters, like roels.ez, {@obj1.var1}<br>
	 * args: arguments buffer where to find variables needed by sk<br>
	 * sqlArgs: arguments directly provided and send back to server - needed by sk<br>
	 * select: selected id<br>
	 * all: add an "-- ALL --" item<br>
	 * rootId: root id <br>
	 * multi: is multiple selected<br>
	 * onselect: onselect function or function name<br>
	 */
	this.combotree = function(treeId, opts) {
		treeId = regex.sharp_(treeId, ir.deflt.cbbtreeId);
		var tree = $(treeId);
		opts = EasyHtml.opts(treeId, opts);
		var sk = opts.cbbtree;
		var sqlArgs = opts.sqlArgs;

		if (sk === undefined || sk === null)
			console.error(ir.cbbtree + " attr in " + treeId + " is undefined. In jeasy v1.0, only configurd combobox is suppored (must have an sk from dataset.xml).");

		// request JMessage body
		var req = new jvue.DatasetCfg(	// s-tree.serv (SemanticTree) uses DatasetReq as JMessage body
					jconsts.conn,		// connection id in connexts.xml
					sk,					// sk in dataset.xml
					'sqltree');			// ask for configured dataset as tree
		req.rootId = opts.rootId;
		req.args(opts.sqlArgs);

		// all request are created as user reqs except query, update, insert, delete and ext like dataset.
		// DatasetReq is used as message body for semantic tree.
		// Port.stree is port of SemanticTree.
		// t=load/reforest/retree
		// user act is ignored for reading
		var jmsg = ssClient.userReq(jconsts.conn, Port.stree, req);

		// get data, then bind easyui combotree
		ssClient.commit(jmsg, function(resp) {
			console.log(resp);
			var tree = $(treeId);
			if (opts.all)
				resp.data.unshift({text: ir.deflt._All_, id: ir.deflt._All_, value: ir.deflt._All_});
			tree.combotree({
				data: resp.data,
				multiple: opts.multi !== undefined && opts.multi !== null && opts.multi === true,
//				onSelect: typeof onChangef === "function" ? onChangef : function(e) {
//					if (jeasy.log) console.log(e);
//				}
				onSelect: typeof opts.onselect === "function" ? opts.onselect : function(e) {
					if (jeasy.log) console.log(e);
				},
				onChange: typeof opts.onchange === "function" ? opts.onchange : function(e) {
					if (jeasy.log) console.log(e);
				}
			});
			if(typeof(opts.select) != "undefined" && opts.select != null)
				tree.combotree('setValue', opts.select);
		});
	};

	/**Bind easyUI tree, with click/select function.<br>
	 * Data is gotten from s-tree.serv, with sk = 'sk'.
	 * - easyui treegrid must recursive looped to get all selected items.<br>
	 * @param {string} idTree easy tree id:
	 * @param {Object} opts<br>
	 * sk: semantic key string with parameters, like roels.ez, {@obj1.var1}<br>
	 * args: arguments buffer where to find variables needed by sk<br>
	 * sqlArgs: arguments directly provided and send back to server - needed by sk<br>
	 * select: selected id<br>
	 * all: add an "-- ALL --" item<br>
	 * rootId: root id <br>
	 * multi: is multiple selected<br>
	 * onselect: onselect function or function name<br>
	 * onload: on load callback<br>
	 * onClick: on item click event handler<br>
	 */
	this.stree = function ( treeId, opts ) {
		treeId = regex.sharp_(treeId, ir.deflt.treeId);
		var tree = $(treeId);

		opts = EasyHtml.opts(treeId, opts);
		var sk = opts.tree;

		if (sk === undefined || sk === null || sk === '')
			console.error(ir.tree + " attr in " + treeId + " is undefined. In jeasy v1.0, only configurd semantic tree is suppored (must have an sk from dataset.xml).");

		// request JMessage body
		var req = new jvue.DatasetCfg(	// s-tree.serv (SemanticTree) uses DatasetReq as JMessage body
					jconsts.conn,		// connection id in connexts.xml
					sk,					// sk in datast.xml
					'sqltree')	// TODO sk != undefined, delete and test
					.args(opts.sqlArgs);

		// all request are created as user reqs except query, update, insert, delete and 'ext' class like dataset.
		// DatasetReq is used as message body for semantic tree.
		// Port.stree is port of SemanticTree.
		// t=load/reforest/retree
		// user act is ignored for reading
		var jmsg = ssClient.userReq(jconsts.conn, jvue.Protocol.Port.stree, req);

		// get data, then bind easyui tree
		// ssClient is created after logged in.
		ssClient.commit(jmsg, function(resp) {
			// resp.data.forEach(function (v, i) {
			// 	v.checked = false;
			// });
			console.log(resp);
			EasyTree.bind(treeId,	// id
					resp.data,		// forest,
					'tree',			// easyui tree()
					opts.onclick,
					opts.onselect,
					opts.oncheck,
					opts.onload);
		});
	};

	/**[Internal API] Bind easyui tree with data from serv.
	 * @param {string} treeId html tag id
	 * @param {array} json rows
	 * @param {string} treeType tree | treegrid
	 * @param {function} onClick on click callback
	 * @param {function} onSelect on selection changed callback
	 * @param {function} onCheck on selection changed callback
	 * @param {function} onLoad on binding success callback
	 */
	this.bind = function (treeId, json, treeType, onClick, onSelect, onCheck, onLoad) {

		var ezOpts = {
			onclick:onClick,
			onselect:onSelect,
			oncheck:onCheck,
			onload:onLoad
		}
		EasyHtml.ez(treeType, treeId, json, ezOpts);
		EasyMsger.close();
	};

	/**Ask server (SemanticTree) travel throw sub-tree from rootId, re-organize fullpath.
	 * This is a helper in case client bugs, competetions, etc. that makes a tree's fullpath incorrect.
	 * @param {string} tabl,
	 * @param {string} sk semantic sk in dataset/xml/s-trre
	 * @param {string} rootId
	 * @param {string} onSuccessf success callback
	 */
	this.retree = function (tabl, sk, root, onSuccess) {
		this.semanticRetree (tabl, sk, 'retree', {root: root}, null, null, onSucess);
	};

	this.reforest = function (tabl, sk, onSuccess) {
		this.semanticRetree (tabl, sk, 'reforest', null, null, null, onSucess);
	};

	this.semanticRetree = function (tabl, sk, t, args, act, onSucess) {
		if (treeId.substring(0, 1) != "#")
			treeId = "#" + treeId;

		if (typeof sk === "undefined" || sk === null)
			sk = tree.attr(_aSemantik);

		var req = new jvue.DatasetCfg(	// s-tree.serv (SemanticTree) uses DatasetReq as JMessage body
					jconsts.conn,		// connection id in connexts.xml
					sk);				// sk in datast.xml
		var jmsg = ssClient.userReq(jconsts.conn, 'retree', jvue.Protocol.Port.stree, req, act);

		// ssClient is created after logged in.
		ssClient.commit(jmsg, onSuccess);
	};
};
const EasyTree = new EzTree (J);

function EzGrid (J) {
	this.pageInfo = {};

	/**This method is used to bind CRUD main list.
	 * Data (rows) are paged at server sied.
	 * @param {string} pagerId the easyui pager's id
	 * @param {Object} opts<br>
	 * opts.args: arguments buffer where to find variables needed by sk sql <br>
	 * opts.sqlArgs: arguments directly provided and send back to server - needed by sk <br>
	 * See also EzHtml.opts().
	 *
	 */
	this.pager = function (pagerId, opts) {
		// this.pager = function (pagerId, qformId, onLoad, onSelect, onCheck, onCheckAll) {
		if(pagerId === null || pagerId === undefined || typeof pagerId !== 'string') {
			console.error("pager id is not valid");
			return;
		}
		else pagerId = regex.sharp_(pagerId, ir.deflt.pagerId);

		var gridId = $(pagerId).attr(ir.grid);
		if (gridId === undefined || gridId === null || gridId.trim() === '') {
			console.error("gird/list id defined in pager is not valid. A " + ir.grid + " in pager tag must defined.");
			return;
		}
		gridId = regex.sharp_(gridId, ir.deflt.gridId);

		opts = EasyHtml.opts(gridId, opts);
		opts = EasyHtml.opts(pagerId, opts);

		// semantics key (config.xml/semantics)
		var semantik = opts.sk;

		// Remember some variabl for later calling onPage()
		if (this.pageInfo[pagerId] === undefined) {
			this.pageInfo[pagerId] = {
				queryId: opts.query,
				total: 0,
				page: 0,
				size: opts.pagesize,
			};
			$(pagerId).pagination({
				pageSize: opts.pagesize,
				onSelectPage: opts.onchangepage==undefined?this.onPage:opts.onchangepage,
			});
		}

		var req;
		if (semantik !== undefined)
			// dataset way
			req = new jvue.DatasetCfg(	// SysMenu.java (menu.sample) uses DatasetReq as JMessage body
						jconsts.conn,	// connection id in connexts.xml
						semantik);		// sk in datast.xml
		else {
			// try query.serv way
			// TODO change to tag.joins(opts.t)
			var tbls = EasyHtml.tabls(gridId, opts.joins);
			if (tbls !== undefined) {
				// create a query request
				var maint = tbls[0].tabl;
				var mainAlias = tbls[0].as;
				req = ssClient.query(null,	// let the server find connection
							maint,			// main table
							mainAlias,		// main alias
							this.pageInfo[pagerId]); // this.pageInfo, saving page ix for consequent querying
				var q = req.body[0];

				// handle query defined in grid attrs
				// [{exp: t.col as: c}, ...]
				var exprs = EasyHtml.thExprs(gridId, mainAlias);
				q.exprss(exprs);

				// joins ( already parsed )
				q.joinss(tbls.splice(1, tbls.length - 1));

				// where clause
				var wheres = EasyQueryForm.conds(opts.query, mainAlias);
				// q.wheres("=", "u.userId", "'" + uid + "'");
				q.whereCond(wheres);

				// order by
				q.orderbys(tag.orders(opts.orderby));

				// group by
				q.groupbys(tag.groups(opts.groupby));


			}
			else console.error('Grid can support both ir-grid or ir-t, but none of them can be found.', opts);
		}
		// post request, handle response
		EasyMsger.progress();
		ssClient.commit(req, function(resp) {
			var rows = jeasy.rows(resp);
			var total = jeasy.total(resp, 0);
			EasyGrid.bindPage (gridId, rows, total, opts);

			var pgInf = EasyGrid.pageInfo[pagerId];
			pgInf.total = total;
			EasyGrid.bindPager(pagerId, total, pgInf.page, pgInf.size, opts.onpagechange);
		}, EasyMsger.error);
	};

	/**Load grid without a pager
	 * @param {string} gridId
	 * @param {Object} opts
	 * t: ir-t string (override html ir-t)
	 * queryId: query form Id<br>
	 * rowpk: row's pk<br>
	 * select: select an item when load<br>
	 */
	this.grid = function (gridId, opts) {
		gridId = regex.sharp_(gridId, ir.deflt.gridId);

		opts = EasyHtml.opts(gridId, opts);

		// var semantik = $(gridId).attr(ir.sk);
		var semantik = opts.sk;
		// var pgSize = opts.pagesize;

		// Remember some variabl for later calling onPage()
		if (this.pageInfo[gridId] === undefined) {
			this.pageInfo[gridId] = {
				queryId: opts.queryId,
				total: 0,
				page: opts.page,
				size: opts.pagesize,
			};
		}

		var req;
		if (semantik !== undefined)
			// dataset way
			req = new jvue.DatasetCfg(	// SysMenu.java (menu.sample) uses DatasetReq as JMessage body
						jconsts.conn,	// connection id in connexts.xml
						semantik);		// sk in datast.xml
		else {
			// try query.serv way
			// var tbls = opts.t;
			var tbls = tag.joins(opts.t, opts.joins);
			// if (tbls === undefined || (typeof tbls === 'string' && tbls.trim().length < 2))
			// 	tbls = EasyHtml.tabls(gridId);

			if (tbls !== undefined) {
				// create a query request
				var maint = tbls[0].tabl;
				var mainAlias = tbls[0].as;
				req = ssClient.query(null,	// let the server find connection
							maint,			// main table
							mainAlias,		// main alias
							this.pageInfo[gridId]); // this.pageInfo, saving page ix for consequent querying
				var q = req.body[0];

				// handle query defined in grid attrs
				// [{exp: t.col as: c}, ...]
				var exprs = EasyHtml.thExprs(gridId, mainAlias);
				q.exprss(exprs);

				// joins ( already parsed )
				q.joinss(tbls.splice(1, tbls.length - 1));

				// where clause
				var wheres = EasyQueryForm.conds(opts.query, mainAlias);
				// q.wheres("=", "u.userId", "'" + uid + "'");
				q.whereCond(wheres);

				// order by
				q.orderbys(tag.orders(opts.orderby));

				// group by
				q.groupbys(tag.groups(opts.groupby));
			}
			else console.error('Grid can support both ir-grid or ir-t, but none of them can be found.', opts);
		}
		// post request, handle response
		EasyMsger.progress();
		ssClient.commit(req, function(resp) {
			var rows = jeasy.rows(resp);
			var total = jeasy.total(resp, 0);
			// EasyGrid.bindPage (gridId, rows, total, opts.onSelect, opts.onCheck, opts.onCheckAll, opts.onLoad);
			EasyGrid.bindPage (gridId, rows, total, opts);
		}, EasyMsger.error);
	};

	this.treegrid = function (gridId, opts) {
		gridId = regex.sharp_(gridId, ir.deflt.gridId);
		opts = EasyHtml.opts(gridId, opts);

		// Remember some variabl for later calling onPage()
		if (this.pageInfo[gridId] === undefined) {
			this.pageInfo[gridId] = {
				queryId: opts.queryId,
				total: 0,
				page: opts.page,
				size: opts.pagesize,
			};
		}

		var req;
		if (opts.t == undefined && opts.treegrid !== undefined) {
			// dataset way

			// req = new jvue.DatasetCfg(	// SysMenu.java (menu.sample) uses DatasetReq as JMessage body
			// 			jconsts.conn,	// connection id in connexts.xml
			// 			opts.sk);		// sk in datast.xml
			req = new jvue.DatasetCfg(	// s-tree.serv (SemanticTree) uses DatasetReq as JMessage body
						jconsts.conn,	// connection id in connexts.xml
						opts.treegrid,	// sk in datast.xml
						'sqltree')
						.args(opts.sqlArgs);
			req = ssClient.userReq(jconsts.conn, jvue.Protocol.Port.stree, req);
		}
		else {
			// try query.serv way
			// var tbls = opts.t;
			var tbls = tag.joins(opts.t, opts.joins);

			if (tbls !== undefined) {
				// create a query request
				var maint = tbls[0].tabl;
				var mainAlias = tbls[0].as;

				var q = new jvue.DatasetCfg(	// s-tree.serv (SemanticTree) uses DatasetReq as JMessage body
							jconsts.conn,		// connection id in connexts.xml
							opts.treegrid,      // sk in datast.xml
							'',					// a()
							opts.sqlArgs,
							maint,              // main table - used when sk is null
							mainAlias)          // main alias - used when sk is null

				// all request are created as user reqs except query, update, insert, delete and 'ext' class like dataset.
				// DatasetReq is used as message body for semantic tree.
				// Port.stree is port of SemanticTree.
				// t=load/reforest/retree
				// user act is ignored for reading
				req = ssClient.userReq(jconsts.conn, jvue.Protocol.Port.stree, q);

				// handle query defined in grid attrs
				// [{exp: t.col as: c}, ...]
				var exprs = EasyHtml.thExprs(gridId, mainAlias);
				q.exprss(exprs);

				// joins ( already parsed )
				q.joinss(tbls.splice(1, tbls.length - 1));

				// where clause
				var wheres = EasyQueryForm.conds(opts.query, mainAlias);
				// q.wheres("=", "u.userId", "'" + uid + "'");
				q.whereCond(wheres);

				// order by
				q.orderbys(tag.orders(opts.orderby));
			}
			else console.error('Treegrid can support both ir-grid or ir-t, but none of them can be found.', opts);
		}
		// post request, handle response
		EasyMsger.progress();
		ssClient.commit(req, function(resp) {
			console.log(resp);
			EasyMsger.close();
			EasyTree.bind(gridId,	// id
					resp.data,		// forest,
					'treegrid',
					opts.onclick,
					opts.onselect,
					opts.oncheck,
					opts.onload);
		}, EasyMsger.error);
	}

	/**call easyui $(pagerId).pagination("refresh", ...
	 * @param {string} pagerId
	 * @param {int} total
	 * @param {int} page
	 * @param {int} size
	 */
	this.bindPager = function (pagerId, total, page, size, onpagechange) {
		// Design Notes: How to implement jeasy based on easyUI with keeping component design in mind?
		if (jconsts.verbose >= 2) console.warn('Debug Notes (verbose 2):\n',
			'If you see this error message from easyUI:\n',
			'TypeError: _c0 is undefined[Learn More]\n',
			'You probably run into a jeasy api bug.\n',
			'You must be careful because those following js code may not executed normally.\n',
			'Cause:\n',
			'EzGrid#bindPage() can not been called more than once because the pageInfo have a ' +
			'record of a pager which prevent a new pager been created in the same main page. See #pager() pageInfo section.\n',
			'This is an OOP issue - all modal component accessing a shared global data failed.\n',
			'We will handle this later.\n',
			'To temporarily detour this problem, you must set EasyGrid.pageInfo.' + pagerId + ' = undefined.',
			'See example jsample/user.js/Role#initRole().');
		$(pagerId).pagination("refresh", {
			total: total,
			pageNumber: page + 1,
			pageSize: size,
			'pagechange': onpagechange
		});
	};

	/** Helper function to load page on pager's event. */
	this.onPage = function (pageNumb, size) {
		// onPage is pagination's onSelectPage handler, so this.id = pager-id
		var pgInf = EasyGrid.pageInfo['#' + this.id];
		pgInf.page = pageNumb - 1;
		pgInf.size = size;
		EasyGrid.pager(this.id, {query: pgInf.queryId});
	};

	/**Bind json data to easyUI datagrid or treegrid.<br>
	 * @param {string} gridId
	 * @param {Array} json rows / forest to be bound
	 * @param {number} total total rows
	 * @param {object} opts options: select, oncheck, onselect, oncheckAll
	 * @param {boolean} ezTreegrid is treegrid?
	 */
	this.bindPage = function (gridId, json, total, opts, ezTreegrid) {

		EasyHtml.ez('datagrid', gridId, json, opts);
		EasyMsger.close();
		return;

		// FIXME this not correct!
		// FIXME this not correct!
		// FIXME this not correct!

		// var ezfunc = g[ezTreegrid];	// ezTreegrid = 'treegrid', 'datagrid', 'tree', ...
		// ezfunc("loadData", json);	// now ezfunc is a function

		// FIXME this not correct!
		// FIXME this not correct!
		// FIXME this not correct!
		// FIXME this not correct!
		if (ezTreegrid) {
			g.treegrid(opts);
			if (opts != undefined && opts.onCheckAll) {
				g.treegrid({ onCheckAll: opts.oncheckAll,
					onUncheckAll: opts.oncheckAll});
			}
			g.treegrid("loadData", json);
		}
		else {
			g.datagrid(opts);
			if (opts !== undefined && opts.onCheckAll) {
				g.datagrid({ onCheckAll: opts.oncheckAll,
					onUncheckAll: opts.oncheckAll});
			}
			g.datagrid("loadData", json);
		}
		if (opts.onCheckAll)
			g.datagrid({ onCheckAll: opts.onCheckAll,
				onUncheckAll: opts.onCheckAll});
		if(opts.onload)
			g.datagrid({ onLoadSuccess: opts.onload});
		g.datagrid("loadData", json);

		if (typeof opts.select === 'object') {
			var ix = jeasy.findRowIdx(json, opts.select)
			if (ezTreegrid) {
				g.treegrid("selectRow", ix);
			}
			else {
				g.datagrid("selectRow", ix);
			}
		}
		// if(typeof isSelectFirst === "undefined" || isSelectFirst != false) {
		// 	// select row 1
		// 	g.datagrid("selectRow", 0);
		// }

		EasyMsger.close();
//		if (typeof opts.onload === "function")
//			opts.onload ( json, total );
	};

	/** delete selected row.
	 * If no row is selected, call opts.onalert, or EzMsger.alert(m.none_selected);
	 * @param {string} gridId easy grid in whiche the selected row to be deleted
	 * @param {Object} opts
	 * opts.maintable: target table.<br>
	 * opts.pk: pk condition for sql where clause.<br>
	 * opts.select the selected record id overriding auto retrieved selected row's id.<br>
	 * opts.onload: the success callback, default: EasyMsger.ok<br>
	 * opts.onerror: the error handle, default: EasyMsger.error.<br>
	 * @param {UpdateReq} the delete request (a = 'D')
	 */
	this.delrow = function(gridId, opts) {
		gridId = regex.sharp_(gridId, ir.deflt.gridId);
		opts = EasyHtml.opts(gridId, opts);

		var rw = jeasy.getMainRow(gridId);
		if (rw === undefined || rw === null) {
			if (typeof opts.onerror === 'function')
				opts.onerror(EasyMsger.m.none_selected);
			else
				EasyMsger.alert(EasyMsger.m.none_selected);
			return;
		}

		var pkv = rw[opts.pk];
		var rq = ssClient.delete(opts.conn, opts.maintabl,
			{pk: opts.pk, v: pkv}, opts.posts);
		ssClient.commit(rq, opts.onok, opts.onerror);
	};
};
const EasyGrid = new EzGrid(J);

////////////////////////  Easy API for Basic CRUD   ////////////////////////////
//
function EzQueryForm(J) {
	this.load = function(formId) {
		// 1. load ir-combobox
		$(formId + " ["+ ir.combobox + "]").each(function(key, domval) {
			EasyCbb.combobox(domval.id);
		});

		// 2. combotree
		$(formId + " ["+ ir.cbbtree + "]").each(function(key, domval) {
			EasyTree.combotree(domval.id);
		});
	};

	/**Find condition array from query form, in fields with name attribute
	 * - can be serialized by jquery serializeArray().
	 * @param {} queryid
	 * @param {} defltTabl
	 * @return {array} [{field, v, logic}]
	 */
	this.conds = function (queryid, deftTabl) {
		// if(queryid !== null && queryid !== undefined && queryid.substring(0, 1) != "#")
		// 	queryid = "#" + queryid;
		// else if (typeof queryid === "undefined")
		// 	queryid = _query;
		queryid = regex.sharp_(queryid, ir.deflt.queryId);

		var conds;
		var fields = $( queryid ).serializeArray();
		if (!fields || fields.length <= 0) {
			console.log("No query condition form found in " + queryid);
			return ;
		}
		return this.formatConds(deftTabl, fields);
	}

	/**Format query condition according to "name" attr in tag of form (form-id=queryId).
	 * name="t.col", where t can be alais (handled by serv)
	 * @param {string} defltTabl main table
	 * @param {fields} fields condition fields like that selected by $(".name")
	 * @return {array} [{op: logic, l: "tabl"."col", r: val} ...]
	 */
	this.formatConds = function (defltTabl, fields) {
		// [{field: "recId", v: "rec.001", logic: "=", tabl: "b_articles"}, ...]
		var conds = new Array();
		$.each( fields, function( i, field ) {
			// Ignore items like "-- ALL --" in combobox
			if (field.value === ir.deflt._All_) return;
			if (field.value === undefined || field.value === '' || field.value === ir.deflt._All_)
				return;

			// $( ).append( field.value + " " );
			var n = field.name.replace(/\s\s+/g, ' ');
			var namess = n.split(' '); // table.column >/=

			// is constant of expression? default: false. value will be quoted (as constants)
	 		var asExpr = false;

			if (namess.length < 2) {
				console.log("WARN - name attr in form (id=irquery) must indicating logic operand: " + n);
				return;
			}
			else if (namess.length === 3 && namess[2].trim().toLowerCase() === 'expr') {
				asExpr = true;
			}

			if (asExpr) {
				conds.push([namess[1].trim(), namess[0],
					field.value]);
			}
			else  {
				conds.push([namess[1].trim(), namess[0],
					"'" + field.value + "'"]);	// FIXME: what about \' ?
			}
		});
		return conds;
	}
};
const EasyQueryForm = new EzQueryForm(J);

function EzModal() {
	/**add details */
	this.addDetails = function (src, title, h, w, init, isUeditor, modalId, gridId) {
		ssClient.usrCmd("insert");

		// try to find the row
		var row;
		if (gridId)
		 	row = jeasy.mainRow(gridId);
		else row = jeasy.getMainRow(ir.deflt.gridId);

		this.showDialog(jeasy.c, src, title, h, w, init, isUeditor, modalId, row);
	};

	/**popping a dialog for edit details
	 * @param src
	 * @param title
	 * @param h
	 * @param w
	 * @param init
	 * @param isUeditor
	 * @param modalId
	 * @param listId
	 * */
	this.editDetails = function (src, title, h, w, init, isUeditor, modalId, listId) {
		var row = null;

		listId = regex.sharp_(listId, ir.deflt.gridId);

		var row = jeasy.getMainRow(listId);
		if(row === undefined) {
			EasyMsger.info(EasyMsger.m.none_selected);
			return;
		}

		ssClient.usrCmd("edit");

		this.showDialog(jeasy.u, src, title, h, w, init, isUeditor, modalId, row);
	};

	/**
	 * @param {string} src html
	 * @param {string} title
	 * @param {number} h
	 * @param {number} w
	 * @param {string} init handler
	 * @param {bool} isUeditor is u editer
	 * @param {string} modalId div id for modal
	 * @param {Object} row data
	 * @param {Object} pkvals pk
	 */
	this.showDialog = function (crud, src, title, h, w, init, isUeditor, modalId, row) {
		if (modalId === undefined)
			modalId = ir.deflt.modalId;
		modalId = regex.sharp_(modalId);

		console.log(src);

		var win_options = {
			resizable: false,
			modal: true,
			width: w,
			height: h,
			collapsible: false,
			minimizable: false,
			maximizable: false,
			title: title
		};

		var _modalId = regex.desharp_(modalId);
		var newWin = $('<div>').attr('id', _modalId);
		$(modalId).append(newWin);
		// console.log($(modalId, $('iframe').get(0).contentWindow.document));

		if(!('top' in win_options)) {
			win_options.top = 5;
		}
		newWin.window(win_options);
		newWin.window({
			href: src,
			id: _modalId,
			onMove: function(left, top) {
				var position = {
					left: left,
					top: top
				};
				if(top <= 0) {
					position.top = 5;
					$(this).window('move', position);
				}
				if(left + win_options.width <= 20) {
					position.left = left + 50;
					$(this).window('move', position);
				}
				if(top >= $(window).height()) {
					position.top = $(window).height() - 30;
					$(this).window('move', position);
				}
				if(left >= $(window).width() - 20) {
					position.left = $(window).width() - 30;
					$(this).window('move', position);
				}
			},
			onClose: function() {
				// dispose Ueditor, to be removed
				if(typeof(isUeditor) != "undefined" && isUeditor == true) {
					if(typeof(UE.getEditor('container')) != 'undefined') {
						UE.getEditor('container').destroy();
					}
				}
				newWin.remove();
			},
			onLoad: function() {
				// EasyModal.callInit(init, row, pkvals)
				// load details form, call user's onload handler (in ir-modal)
				EasyModal.callInit(crud, modalId, init, row);
			}
		});

		if(win_options.height < $(window).height()) {
			newWin.window('center');
		}
	};

	this.callInit = function (crud, formId, fn, row) {
		var f;
		if (typeof fn === 'function')
			f = fn;
		else if (typeof fn === 'string')
			f = eval(fn);
		if (f) {
			f(crud, formId, ssClient, row);
		}
		else
			console.error("can't find function " + fn);
	};

	/**Details form loading, a helper called by user onModal() to load a CRUD details form.<br>
	 * @param {string} modalId: optional form id, default "irform".
	 * @param {Object} opts options:
	 * t: serv target (at least main table),<br>
	 * 		can be configured from $(formId)[ir-t] (set the parameter as null)
	 * pk: Value(s) will be appended to query's where clause.
	 * 		1. Object: {pk, v} pk value for quering record. (only pk for main table, on table name)<br>
	 * 		2. Array: [{condt}] conditions returned by EasyQeuryForm.conds();
	 * onload: event handler<br>
	 * 		Functions that for special tasks, e.g. loading svg should done here.
	 */
	this.load = function (modalId, opts) {
		// this.load = function (formId, irt, pk, callback) {
		modalId = regex.sharp_(modalId, ir.deflt.modalId);
		opts = EasyHtml.opts(modalId, opts);
		// find sql "from" clause
		var joins = tag.joins(opts.t, opts.joins);

		var mainAlias = joins[0].as;
		var exprs = EasyHtml.formExprs(modalId, mainAlias);
		var wheres = EasyQueryForm.conds(modalId, mainAlias);

		var req = ssClient.query(null,	// let the server find connection
					joins[0].tabl,		// main table
					joins[0].as);		// main alias

		var q = req.body[0];
		var pk = opts.pk;
		q.exprss(exprs)
			.joinss(joins.splice(1, joins.length - 1))
			.whereCond(wheres);
		if (pk !== undefined && Array.isArray(pk))
			q.whereCond(pk);
		else if (typeof pk === "object" && pk.pk !== undefined) {
			// some times user's code use 'this' in callback, makes arguments wrong
			if (pk.v === undefined)
				console.warn('pk may not correct', pk);
			q.whereCond("=", pk.pk, "'" + pk.v + "'");
		}

		// post request, handle response
		EasyMsger.progress();
		ssClient.commit(req, function(resp) {
			var rows = jeasy.rows(resp);
			EasyModal.bindWidgets (modalId, rows[0], opts.onload);
		}, EasyMsger.error);
	};

	this.bindWidgets = function(formId, rec, callback) {
		$(formId + " ["+ ir.field + "]").each( function(key, domval) {
			// value is a DOM, see http://api.jquery.com/each/
			var f = this.attributes[ir.field].value;
			var v = rec ? rec[f] : undefined;

			var opts = EasyHtml.opts(domval.id, {args: rec, select: v});

			// ir-field presented, this widget  needing been auto bound
			if ( this.attributes[ir.field].name !== undefined ) {
				// set value like a text input
				// case 1: bind ir-combobox
				if (this.attributes[ir.combobox]) {
					if (this.attributes[ir.combobox].name !== undefined) {
						// an ir-combobox from configured dataset
						// this.value = v;
						// EasyCbb.combobox(domval.id, null, {row: rec}, v);
						// EasyCbb.combobox(domval.id, {args: rec, select: v, onselect: onChange});
						EasyCbb.combobox(domval.id, opts);
					}
					else console.log("EasyModal.bindWidgets(): ignoring combobox " + domval.id + " " + domval.name);
				}
				// case 2: bind ir-cbbtree
				else if (this.attributes[ir.cbbtree]) {
					// EasyTree.combotree( domval.id, {args: rec, select: v, onselect: onChange});
					EasyTree.combotree( domval.id, opts);
				}
				// case 1.1: bind easyui-combobox no ir-cbb
				else if (this.classList && (this.classList.contains('easyui-combobox') )) {
					if ( v !== undefined && v!== null && v.trim().length > 0) {
						try {
							$('#' + domval.id).combobox('setValue', v);
						} catch ( ex ) {
							console.log("loadSimpleForm(): Value " + v + " can't been set to combobox " + domval.id);
						}
					}
				}
				// case 3: bind easyui-datebox/datetimebox
				else if (this.classList && (this.classList.contains('easyui-datetimebox')
										|| this.classList.contains('easyui-datebox')   ) ) {
					//$("#installDate").datebox("setValue", row.installDate);
					if ( v !== undefined && v!== null && v.trim().length > 0) {
						try {
							var dt = new Date(v);
							$('#' + domval.id).datebox('setValue', v);
						} catch ( ex ) {
							console.log("loadSimpleForm(): Value " + v + " can't been set to datebox " + domval.id);
						}
					}
				}
				// case 4: bind easyui-numberbox
				else if (this.classList && (this.classList.contains('easyui-numberbox') )) {
					//$("#installDate").datebox("setValue", row.installDate);
					if ( v !== undefined && v!== null && v.trim().length > 0) {
						try {
							$('#' + domval.id).numberbox('setValue', v);
						} catch ( ex ) {
							console.log("loadSimpleForm(): Value " + v + " can't been set to numberbox " + domval.id);
						}
					}
				}
				// case 5: bind easyui-numberspinner
				else if (this.classList && (this.classList.contains('easyui-numberspinner')))
					try {
						$('#' + domval.id).numberspinner('setValue', v);
					} catch ( ex ) {
						console.log("loadSimpleForm(): Value " + v +
							" can't been set to easyui numberspinner " + domval.id);
					}
				// case 6.1: datagrid pager
				else if (this.attributes[ir.grid]) {
					// merge grid's attributes, with pager's attributes overriding gird's
					var gridId = EasyHtml.ir(this.id, ir.grid);
					opts = EasyHtml.opts(gridId, opts);

					EasyGrid.pager(this.id, opts);
				}
				// case 6.2: datagrid
				else if  (this.classList && (this.classList.contains('easyui-datagrid'))) {
					if (jeasy.log)
						console.log('Trying bind datagrid automatically, ir-field: ', f, v);
					// EasyGird.datagrid(this.id, {select: v, onselect: onChange});
					EasyGird.datagrid(this.id, opts);
				}
				// case 7: bind text input - should this moved to the first?
				else if  (this.nodeName!="TEXTAREA"&&this.classList && (this.classList.contains('easyui-textbox') || this.classList.contains('textbox')))
					$(regex.sharp_(this.id)).textbox({value: v});
				else{
				     if(v !== undefined && v!== null && v.trim().length > 0)
				       this.value = v;
				}
			}
		});

		EasyMsger.close();
		if (typeof callback === "function") {
			if (jeasy.log)
				console.log("EzModal.bindWidgets() doesn't calling callback on all task finished, "
					+ "but called when all autobinding fields are iterated while waiting response from serv.")
			callback();
		}
	}

	/** close dialog
	 * @param {string} dlgId dialog id
	 */
	this.close = function (dlgId) {
		dlgId = regex.sharp_(dlgId, ir.deflt.modalId);
		$(dlgId).window('close');

		if (typeof this.onclose === 'function')
			this.onclose();
	};

	/**Get a save-form request.
	 * Any form element with 'name' attribute will be saved into tabl.
	 * @param {stirng} crud jeasy.c | u | d
	 * @param {string} dlgid formId to be packaged
	 * @param {string} tabl target table
	 * @param {string} pk {pk, v} for update condition, ignored when crud = jeasy.c
	 * @param {string} regexExclude exclude expressiong<br>
	 * <p>This function uses jquery.serializeArray() to serialize all tag's value with attribute 'name'.
	 * But this introduce a problem with easyUI checkbox in treegrid or datagride. When a checkbox is
	 * checked, the juery serializeArray() will include it's value, this is not expected when serialize a form.
	 * So an exlude expression is used to exclude checked value from datagride, etc.
	 * E.g. $(dlgId + ' [name]').not('.datagrid [name]').serializeArray() where exlcude check box value in datagrid and treegrid.</p>
	 * So, if you want to exlude it, set regexExclude = '.datagrid [name]'
	 * This is what easyUI not that easy like the first look.
	 * @return {UpdateReq} request {a = c | u | d} formatted according to form's html.
	 */
	this.save = function (conn, crud, dlgId, tabl, pk, regexExclude) {
		dlgId = regex.sharp_(dlgId, ir.deflt.modalId);

		var nvs = $(dlgId + ' [name]').not(regexExclude).serializeArray();

		if (nvs === undefined || nvs.length === 0)
			console.warn("No saving values found in form: " + dlgId,
						"Only children with name attribute of " + dlgId + " can be serrialized.");

		if (crud === jeasy.c) {
			return ssClient.insert(conn, tabl, nvs);
		}
		else {
			return ssClient.update(conn, tabl, pk, nvs);
		}
	}
};
const EasyModal = new EzModal(J);

function EzMsger() {
	this.init = function(code) {
		if (code === undefined) {
			this.msg = {};
			this.progressing = false;
		}
		else {
			this.msg[code] = undefined;
		}
	};

	this.init();

	this.progress = function() {
		if (EasyMsger.progressing === false) {
			$.messager.progress();
			EasyMsger.progressing = true;
		}
	};

	this.close = function() {
		$.messager.progress('close');
		this.progressing = false;
	};

	/** Report error to user only once. Flags are refreshed by init()
	 * Don't change this into Chinese, set another message-popping function to replace this if like to.
	 */
	this.error = function(code, resp) {
		if (EasyMsger.progressing === true) {
			EasyMsger.close();
		}

		if (EasyMsger.msg[code] === null || EasyMsger.msg[code] === undefined) {
			EasyMsger.msg[code] = code;
			console.error(resp);
			if (code === jvue.Protocol.MsgCode.exSession)
				//$.messager.alert('warn', 'Session Error! Please re-login.');
				$.messager.alert('warn', resp.error);
			else if (code === jvue.Protocol.MsgCode.exIo)
				$.messager.alert('warn', 'Network Problem!');
			else $.messager.alert('warn', resp.error);
		}
		else {
			console.warn('Error message ignored:', resp,
				'Call EasyMsger.init("' + code + '") to enable error message again.');
		}
	};

	/**easyui messager.alert('info')
	 * @param {string} code alerting code to supress duplicated alarm.
	 * @param {function} m message code, one of EzMsger.m.
	 * Function type is checked here to prevent users send string parameter anywhere when they want to.
	 */
	this.info = function (m, style) {
		if (style === undefined)
			style = 'info';
		if (typeof m === 'function') {
			$.messager.alert(style, m(), style);
			return;
		}
		else {
			console.warn("Your message is not found.", m,
				"We check m's existence in EzMsger.m because including message string anywhere is not encouraged in jeasy.",
				"You can replace EasyMsger.m with your m object to update and extend messages, in one place.");
		}
	};

	/**See info()
	 * @param {function} m
	 */
	this.alert = function (m) {
		this.info(m, 'warn');
	};

	this.ok = function (mcode) {
		if (mcode)
			this.info(mcode);
		else
			this.info(this.m.ok);
	};

	/**Replace/extend an individual message.
	 * You'd better replace the entire m if switching to another language other than English.
	 * @param {string} code
	 * @param {string} msg message
	 */
	this.setM = function (code, msg) {
		var mf = {};
		mf[code] = () => msg;
		Object.assign(this.m, mf);
	};

	/**Message strings.
	 * Use EzMsger.setM to setup application's message strings.
	 * NO CHINESE HERE for m!
	 * Replace m with your varialbe or call setM(msg-func) if you'd like to,
	 * in ir-jeasy-engcost.js
	 */
	this.m = {
		ok: () => "OK!",
		fail: () => "Operation Failed!",
		// NO CHINESE HERE !
	 	// Replace m with your variable or call setM(msg-func) if you'd like to,
		// in ir-jeasy-engcost.js/jconsts.initMsg (or jeasy-api.js sample project config section)
		saved: () => "Saved Successfully!",
		none_selected: () => "Please select a record!",
		deleted: () => "Delete Successfully!",

		cheap_started: () => "Workflow Started.",
		cheap_no_rights: () => "You don't have the command rights.",
		cheap_competation: () => "Can't step to target nodes. Already exists?",
	};
};
const EasyMsger = new EzMsger(J);
// call message initializer
jconsts.initMsg(EasyMsger);