a

(function(){ // jquery find performance bad, use querySelectorAll instead if($.browser.msie && $.browser.version <= 8){ $.fn.findAll = function(selector){ var list = []; this.each(function() { var queryList = this.querySelectorAll(selector); list.merge(queryList); }); return $(list); }; }else{ $.fn.findAll = $.fn.find; }

$('<div id="loading-block" style="display: none;"></div><div id="loading" style="display: none;"></div>').appendTo($(document.body));

})(); // file ng.config.js (function(angular){ // ajax filter in $HttpBackendProvider (not $HttpProvider - responseInterceptors) if(!angular.completeRequestInterceptor){ angular.completeRequestInterceptor = function(url, status, response, headersString){ window.console.log('Request filter status : ' + status);

		var headers = Consts.requestGetHeader(headersString);

		if('200' != status){
			window.console.log('Error when request filter : not 200 response!');
			if(Consts.requestFilterStatus && !Consts.requestFilterStatus(status, response, headers)){
				return false;
			}
		}

		if('200' == status && response){
			// save token
			var tokenHeader = headers[Consts.tokenHeaderKey];
			if(tokenHeader){
				TokenLocal.setDataByUrl(Consts.getPathWithoutApp(url), tokenHeader);
			}

			var r;
			if(angular.isString(response)){
				// need to skip ng-include
				var firstChar = response.substring(0, 1);
				// json, if not json it may be html code or jsonp javascript expression, just return true and continue
				if(firstChar == '{' || firstChar == '['){
					try{
						r = JSON.parse(response);
					}catch(e){
						window.console.log('Error when request filter json parse : ' + response);
						return false;
					}
				}else if(response.contains('script_umlogin')){
					// um relogin TODO
					PortalAuth.login();
					return false;
				}
			}else if(angular.isObject(response)){
				r = response;
			}

			if(Consts.requestFilter){
				return Consts.requestFilter(r);
			}
		}

		return true;
	};
} // /completeRequestInterceptor

if(!angular.preSendRequestInterceptor){
	angular.preSendRequestInterceptor = function(config, reqHeaders){
		// check session
		if(Consts.sendRequestFilter && !Consts.sendRequestFilter(config, reqHeaders)){
			return false;
		}

		// add token
		var url = config.url;
		var token = TokenLocal.getToken(Consts.getPathWithoutApp(url),
			config.data || config.params);
		if(token){
			reqHeaders[Consts.tokenHeaderKey] = token;
		}else{
			window.console.log('No token get : ' + url);
		}

		return true;
	};
} // /preSendRequestInterceptor

var moduleName = 'ng.config';
window.console.log('Begin init module ' + moduleName);
if(angular.isModuleExists && angular.isModuleExists(moduleName)){
	window.console.log('Module already exists!' + moduleName);
	return;
}

// define this angular module as a simple javascript object, add configurations
var conf = {};

// ... it sucks when using absolute url path
// copy properties from Consts so that you only need to change one file
var keys = ['appname', 'contextPre', 'context',
	'logLevel', 'logQueueFlushSize', 'logPostMsg', 'logServerUrl',
	'useToken', 'tokenHeaderKey'];
var i = 0, len = keys.length;
for(; i < len; i++){
	var key = keys[i];
	conf[key] = Consts[key];
}

// add servlet container context path to url
conf.url = function(url){
	return this.appname + url;
};

conf.validClass = 'ng-valid';
conf.invalidClass = 'ng-invalid';
conf.dirtyClass = 'ng-dirty';
conf.pristineClass = 'ng-pristine';

// jquery / plugins parameters
// tipsStyle = 'poshytip' => change tips style
conf.tipsStyle = 'default';
conf.tipsXoffset = -12;
conf.tipsYoffset = 8;

conf.tipOptions = {
	showOn: 'focus', 
	alignTo: 'target',
	alignX: 'left',
	alignY: 'center',
	offsetX: 5
};

conf.date = {
	duration: 'fast', 
	dateFormat: 'yy-mm-dd',
	changeMonth: true,
	changeYear: true,
	showMonthAfterYear: false,
	yearSuffix: ''};

conf.autocomplete = {minChars: 3, maxItemsToShow: 10, finishOnBlur: true};

conf.defaultPagiBtnClass = 'btn';
conf.defaultPagiCurrentBtnClass = 'btn btn-blue';

// dialog
conf.dialogDraggable = {
	opacity: 0.6,
	containment: 'document'
};

conf.contextMenu = {
	contextMenuClass: 'contextMenuPlugin',
	gutterLineClass: 'gutterLine',
	headerClass: 'header',
	seperatorClass: 'divider',
	title: ''
};

conf.dropdownOptions = {
	valueField: 'code', 
	labelField: 'label', 
	widthDiff: 16,
	widthMultipleInput: 150, 

	zIndex: 1010
};

// user can overwrite
for(var keyConsts in Consts){
	if(keyConsts.startsWith('conf_')){
		conf[keyConsts.substr('conf_'.length)] = Consts[keyConsts];
	}
}

var md = angular.module(moduleName, []).config(function($sceProvider){
	$sceProvider.enabled(false);
});
md.value('ng.config', conf);

})(angular);

// file ng.filter.js (function(angular){ var moduleName = 'ng.filter'; window.console.log('Begin init module ' + moduleName); if(angular.isModuleExists && angular.isModuleExists(moduleName)){ window.console.log('Module already exists!' + moduleName); return; }

var md = angular.module(moduleName, []).config(function($sceProvider){
	$sceProvider.enabled(false);
});
// filter function will be called twice, refer :
// http://stackoverflow.com/questions/11676901/is-this-normal-for-angularjs-filtering

// use ngClassEven/Odd instead
md.filter('trBg', function(){
	return function(index, oddStyleSuf) {
		var pre = index % 2 == 0 ? 'odd' : 'even';
		if(oddStyleSuf)
			pre += oddStyleSuf;
		return pre;
	};
});

})(angular);

// file ng.service.js (function(angular){ var moduleName = 'ng.service'; window.console.log('Begin init module ' + moduleName); if(angular.isModuleExists && angular.isModuleExists(moduleName)){ window.console.log('Module already exists!' + moduleName); return; }

// change plugin settings
if($.dialog){
	$.dialog.setting.path = Consts.context + 'images/lhgdialog/';
	$.dialog.setting.zIndex = 900;
	$.dialog.setting.padding = '2px';
}

var md = angular.module(moduleName, ['ng.config']).config(function($sceProvider){
	$sceProvider.enabled(false);
});

md.factory('safeApply', function($rootScope){
	return function(scope, fn){
		var phase = scope.$root.$$phase;
		if(phase == '$apply' || phase == '$digest'){
			if(fn && (typeof (fn) === 'function')){
				fn();
			}
		}else{
			scope.$apply(fn);
		}
	}
});

md.factory('uiPortalUtils', ['$window', 'uiLog', function(win, log){
	return {
		openTab: function(tabId, url, title, opts){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');

				// open new window if not in portal
				if(opts && opts.openForce){
					win.open(url, tabId);
				}

				return;
			}else{
				win.parent.PortalTab.open(tabId, encodeURI(url), title, opts);
			}
		},
		
		openTabByPost: function(tabId, url, title, opts, data){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');

				// open new window if not in portal
				if(opts && opts.openForce){
					win.open(url, tabId);
				}

				return;
			}else{
				win.parent.PortalTab.openByPost(tabId, url, title, opts, data);
			}
		},

		removeCurrent: function(donotShowTip){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');

				win.close();
			}else{
				win.parent.PortalTab.removeCurrent(donotShowTip);
			}
		}, 

		removeAll: function(){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');

				win.close();
			}else{
				win.parent.PortalTab.removeAll();
			}
		}, 

		removeTabByTabId: function(tabId, donotShowTip){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');
			}else{
				win.parent.PortalTab.removeTabByTabId(tabId, donotShowTip);
			}
		}, 

		openNav: function(){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');
			}else{
				win.parent.PortalTab.openNav();
			}
		}, 

		collapseNav: function(){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');
			}else{
				win.parent.PortalTab.collapseNav();
			}
		}, 

		getIframe: function(id){
			if(!win.parent || !win.parent.PortalTab){
				log.w('No PortalTab defination!');
			}else{
				return win.parent.PortalTab.getIframe(id);
			}
		}
	};
}]);

// log
md.factory('uiLog', ['ng.config', '$window', function(conf, win){
	var logQueue = [];
	var levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
	return {
		// format to string
		getMsg: function(msg, level){
			if(!angular.isString(msg))
				msg = JSON.stringify(msg);

			level = level || 'INFO';

			var dat = new Date().format();
			return '[' + dat + '][' + level + ']' + msg;
		},

		// send to server
		postMsg: function(){
			if(!conf.logPostMsg)
				return;

			var url = conf.logServerUrl;
			var _this = this;
			$.ajax({
				url: url,
				type: 'POST',
				async: false,
				data: 'msg=' + JSON.stringify(logQueue),
				error: function(xhr){
					_this.e('POST log failed : ' + url);
				},

				success: function(){
					_this.i('POST log ok : ' + url);
					logQueue.clear();
				}
			});
		},

		isLevelEnabled: function(level){
			return levels.indexOf(conf.logLevel) <= levels.indexOf(level);
		},
			
		log: function(msg, level){
			if(win.console && win.console.log && this.isLevelEnabled(level)){
				win.console.log(this.getMsg(msg, level));
			}
		},

		d: function(msg){
			this.log(msg, 'DEBUG');
		},

		i: function(msg){
			this.log(msg, 'INFO');
		},

		w: function(msg){
			this.log(msg, 'WARN');
		},

		e: function(msg){
			this.log(msg, 'ERROR');
		},

		audit: function(msg){
			var str = this.getMsg(msg);
			if(win.console && win.console.log){
				win.console.log(str);
			}

			logQueue.push(str);
			if(logQueue.length > conf.logQueueFlushSize){
				this.postMsg();
			}
		}
	};
}]);

// local storage adaptor
md.factory('uiStorage', function(){
	return Storage.newInstance();
});

// use jquery ajax when do synchronized requests
// pre filter after filter
md.factory('jqueryAjax', ['$rootScope', 'safeApply', 'uiLog', function($rootScope, safeApply, uiLog){
	var isSuccess = function(status){
		return 200 <= status && status < 300;
	};

	var jqueryAjax = function(config){
		var method = config.method ? angular.uppercase(config.method) : 'GET';
		var reqData = config.params || config.data;
		var reqHeaders = config.headers || {};

		// token
		var preFilterResult =  true;
		if(angular.preSendRequestInterceptor){
			preFilterResult = angular.preSendRequestInterceptor(config, reqHeaders);
		}

		if(!preFilterResult){
			// just return
			return {
				success: function(fn){
					// skip
					return this;
				},
				error: function(fn){
					fn(data, -1);
					safeApply($rootScope);

					return this;
				}
			};
		}

		var data;
		var headersString;
		var status = 200;

		var props = {
			url: config.url,
			type: method,
			data: reqData,
			async: false,

			success: function(txt, status, jqXHR){
				headersString = jqXHR.getAllResponseHeaders();
				try{
					data = angular.isObject(txt) ? txt : JSON.parse(txt);
				}catch(e){
					data = txt;
				}
			},

			error: function(jqXHR, error, msg){
				status = jqXHR.status;
				headersString = jqXHR.getAllResponseHeaders();
				data = jqXHR.responseText || msg;
			}
		};
		if(reqHeaders.contentType)
			props.contentType = reqHeaders.contentType;

		if('post' === method.toLowerCase()){
			props.contentType = 'application/json';
			props.data = JSON.stringify(props.data);
		}

		$.ajax(props);

		var filterResult =  true;
		if(angular.completeRequestInterceptor){
			filterResult = angular.completeRequestInterceptor(config.url, status, data, headersString);
		}

		return {
			success: function(fn){
				if(isSuccess(status)){
					fn(data, status);
					safeApply($rootScope);
				}

				return this;
			},
			error: function(fn){
				if(isSuccess(status))
					return;

				fn(data, status);
				safeApply($rootScope);

				return this;
			}
		};
	};

	jqueryAjax.get = function(url, params){
		return jqueryAjax({method: 'get', url: url, params: params});
	};

	jqueryAjax.post = function(url, data){
		return jqueryAjax({method: 'post', url: url, data: JSON.stringify(data), 
			headers: {contentType: 'application/json'}});
	};

	return jqueryAjax;
}]);

// pagination model helper
md.factory('uiPager', ['ng.config', 'uiLog', function(conf, log){
	return {
		gen: function(pager, opts){
			var pagi = {};
			if(!pager){
				pagi.totalPageLl = [];
				pagi.totalPage = 0;
				pagi.totalCount = 0;
				pagi.pageNum = 0;
				pagi.pageSize = 0;

				pagi.style = {};
				pagi.style.btnClass = conf.defaultPagiBtnClass;

				// first previous next last / buttons disabled
				pagi.ctrl = {};
				pagi.ctrl.isChoosePageDisabled = true;
				pagi.ctrl.isFirstPageDisabled = true;
				pagi.ctrl.isPrevPageDisabled = true;
				pagi.ctrl.isNextPageDisabled = true;
				pagi.ctrl.isLastPageDisabled = true;

				return pagi;
			}

			// current page
			pagi.pageNum = pager.pageNum || 0;
			// number per page
			pagi.pageSize = pager.pageSize || 10;
			pagi.totalCount = pager.totalCount || 0;
			// no records
			if(!pagi.totalCount)
				pagi.pageNum = 0;

			pagi.totalPage = this.getTotalPage(pagi.totalCount, pagi.pageSize);
			pagi.totalPageLl = this.getTotalPageLl(pagi.totalPage, pagi.pageNum, opts);

			pagi.targetPageChoosed = _.find(pagi.totalPageLl, function(it){
				return it.pageNum == pagi.pageNum;
			});

			pagi.style = {};
			pagi.style.btnClass = conf.defaultPagiBtnClass;

			// first previous next last
			pagi.ctrl = {};
			pagi.ctrl.isFirstPageDisabled = pagi.totalCount == 0 || pagi.pageNum == 1;
			pagi.ctrl.isPrevPageDisabled = pagi.pageNum <= 1;
			pagi.ctrl.isNextPageDisabled = pagi.pageNum == pagi.totalPage;
			pagi.ctrl.isLastPageDisabled = pagi.totalPage <= 1 || pagi.pageNum == pagi.totalPage;
			pagi.ctrl.isChoosePageDisabled = pagi.totalPage <= 1;

			return pagi;
		},

		refresh: function(pagi){
			pagi.totalPage = this.getTotalPage(pagi.totalCount, pagi.pageSize);
			pagi.totalPageLl = this.getTotalPageLl(pagi.totalPage, pagi.pageNum);					
		},

		getTotalPageLl: function(totalPage, pageNum, opts){
			return _.map(_.range(1, totalPage + 1), function(it){
				var pagiBtnClass = conf.defaultPagiBtnClass;
				if(opts && opts.defaultBtnClass)
					pagiBtnClass = opts.defaultBtnClass;

				// model with button style
				// use ng-repeat to render different buttons
				var one = {};
				one.btnClass = pagiBtnClass;
				one.pageNum = it;

				if(pageNum == it){
					one.btnClass = conf.defaultPagiCurrentBtnClass;
					if(opts && opts.currentBtnClass)
						one.btnClass = opts.currentBtnClass;
				}

				return one;
			});	
		},

		getTotalPage: function(totalCount, pageSize){
			var r = totalCount % pageSize;
			var r2 = totalCount / pageSize;
			var result = r == 0 ? r2 : r2 + 1;
			return Math.floor(result);
		}
	};
}]);

// filter ajax request parameters
md.factory('uiRequest', ['$http', 'uiLog', 'uiTips', 'uiStorage', function($http, log, uiTips, uiStorage){
	return {
		// parameters flatten
		// eg. {list: [{name: 'kerry']} -> {'list[0].name': 'kerry'}
		pairs: function(obj, name){
			if(obj == null)
				return null;

			if(!name)
				name = '';

			var sub, pairs = {};
			var type = Object.prototype.toString.call(obj);
			if(type == "[object Array]"){
				var i = 0, len = obj.length;
				for(; i < len; i++){
					var item = obj[i];
					sub = this.pairs(item, name + '[' + i + ']');
					_.extend(pairs, sub);
				}
				return pairs;
			}else if(type == "[object Object]"){
				for(key in obj){
					var subName = name == '' ? key : name + '.' + key;
					sub = this.pairs(obj[key], subName);
					_.extend(pairs, sub);
				}
				return pairs;
			}else{
				pairs[name] = obj;
				return pairs;
			}
		},

		// url query parameters
		params: function(url){
			return Utils.params(url);
		},

		genKey: function(data){
			var str = '';
			if(!data)
				return str;

			var sp = '_';
			for(key in data){
				str += key + sp + data[key] + sp;
			}
			return str;
		},

		req: function(url, params, msg, callback, method){
			method = method || 'get';
			var key = url + '__' + this.genKey(params);

			var saved = uiStorage.get(key);
			if(saved){
				callback(saved);
				return;
			}

			uiTips.loadingFn(function(){
				$http[method](Consts.getAppPath(url), params).success(function(data){
					uiStorage.set(key, data);

					callback(data);
					uiTips.unloading();
				}); 
			}, msg);
		}
	};
}]);

// tips
md.factory('uiTips', ['ng.config', 'uiLog', function(conf, log){
	return {
		filterClass: function(elm, invalid){
			if(invalid){
				elm.removeClass(conf.validClass).removeClass(conf.pristineClass).addClass(conf.invalidClass).addClass(conf.dirtyClass);
			}else{
				elm.removeClass(conf.invalidClass).addClass(conf.validClass);
			}
		}, 

		on: function(el, msg, notHoverShow){
			log.d('tips on...');

			// check if already executed uiTips.on
			var lastTip = el.data('last-tip');
			if(lastTip && lastTip === msg){
				return;
			}
			el.data('last-tip', msg);

			// select2 rebuild dom
			var select2Container = el.prev('.select2-container:first');
			if(select2Container.length){
				this.filterClass(el, true);
				el = select2Container;
			}

			// dropdown rebuild dom
			var dropdownContainer = el.closest('.pui-dropdown');
			if(dropdownContainer.length){
				this.filterClass(el, true);
				el = dropdownContainer;
			}

			var dropdownMultipleContainer = el.prev('.pui-autocomplete-multiple');
			if(dropdownMultipleContainer.length){
				this.filterClass(el, true);
				el = dropdownMultipleContainer;
			}

			this.filterClass(el, true);

			var id = el.attr('ui-valid-id');
			if(!id){
				id = Math.guid();
				el.attr('ui-valid-id', id);
			}

			if(id.contains('.')){
				id = id.replace(/\./g, '_');
			}

			if(notHoverShow){
				if('poshytip' == conf.tipsStyle){
					el.poshytip('destroy');
				}else{
					$("#vtip_" + id).remove();
					el.unbind('mouseenter mouseleave');
				}
				return;
			}

			var genTips = function(){
				// already exists, change css style
				var _tip = $("#vtip_" + id);
				if(_tip.length){
					_tip.html('<img class="vtip_arrow " src="' + conf.context + 'images/vtip_arrow.png" />' + msg)
						.css({"display": "none"});
				}else{
					// generate new and append
					var html = '<p id="vtip_' + id + '" class="vtip"><img class="vtip_arrow" src="' + conf.context + 'images/vtip_arrow.png" />' + msg + '</p>';
					$(html).css({"display": "none"}).appendTo($('body'));
				}
			};

			var bindTipsShow = function(){
				genTips();
				el.unbind('mouseenter mouseleave').bind('mouseenter', _.throttle(function(e){
					var _tip = $("#vtip_" + id);
					_tip.css({left: (e.pageX + conf.tipsXoffset) + 'px', top: (e.pageY + conf.tipsYoffset) + 'px'});
					if(_tip.is(':hidden'))
						_tip.show();
				}, 100)).bind('mouseleave', function(){
					$("#vtip_" + id).hide();
				});
			};

			if('poshytip' == conf.tipsStyle){
				// destroy first
				el.poshytip('destroy');
				el.poshytip({content: msg});
			}else{
				bindTipsShow();
			}
		},

		off: function(el){
			el.data('last-tip', '');

			// select2 rebuild dom
			var select2Container = el.prev('.select2-container:first');
			if(select2Container.length){
				this.filterClass(el);
				el = select2Container;
			}

			// dropdown rebuild dom
			var dropdownContainer = el.closest('.pui-dropdown');
			if(dropdownContainer.length){
				this.filterClass(el, true);
				el = dropdownContainer;
			}

			var dropdownMultipleContainer = el.prev('.pui-autocomplete-multiple');
			if(dropdownMultipleContainer.length){
				this.filterClass(el, true);
				el = dropdownMultipleContainer;
			}

			this.filterClass(el);

			var id = el.attr('ui-valid-id');
			if(!id){
				log.w('No ui-valid-id when call tips off!');
				return;
			}
			if(id.contains('.')){
				id = id.replace(/\./g, '_');
			}

			if('poshytip' == conf.tipsStyle){
				el.poshytip('destroy');
			}else{
				$("#vtip_" + id).remove();
				el.unbind('mouseenter mouseleave');
			}
		},

		// remove all tips div in a speicfic jQuery context
		// TIPS other tips style TODO
		offInContext: function(_context){
			if(!_context || !_context.length){
				return;
			}

			_context.findAll('[ui-valid]').each(function(){
				var validId = $(this).attr('ui-valid-id');
				if(validId){
					$('#vtip_' + validId).hide();
				}
			});
		},

		// *** loading block
		unloading: function(){
			$('#loading, #loading-block').hide();
		},

		loadingFn: function(fn, msg, sync){
			$('#loading').text(msg);
			$('#loading, #loading-block').show();

			if(sync){
				setTimeout(fn, 50);
			}else{
				fn();
			}
		},

		alert: function(msg, fn){
			if(!$.dialog)
				return;

			$.dialog.alert(msg, fn);
		},

		confirm: function(msg, fn, fn2){
			if(!$.dialog)
				return;

			$.dialog.confirm(msg, fn, fn2);
		},

		prompt: function(msg, fn){
			if(!$.dialog)
				return;

			$.dialog.prompt(msg, fn);
		},
			
		tips: function(msg, delay, img, fn){
			if(!$.dialog)
				return;

			$.dialog.tips(msg, delay, img, fn);
		}
	};
}]);

// valid
md.factory('uiValid', ['ng.config', 'uiLog', 'uiTips', '$parse', function(conf, log, tips, $parse){
	return {
		setElementInvalid: function(el){
			el.removeClass(conf.validClass).addClass(conf.invalidClass);
		},
		setElementValid: function(el){
			el.removeClass(conf.invalidClass).addClass(conf.validClass);
		},

		checkForm: function($form){
			return this.checkFormWithVal($form, false);
		}, 

		// call this method before submit your form or do a ajax request
		// because angular directive donot trigger auto
		checkFormWithVal: function($form, returnRequiredModel, $index){
			var formName = $form.$name;
			var _context = $('form[name="{0}"],[ng-form="{0}"],[data-ng-form="{0}"]'.format(formName));
			// ng-repeat create forms with same name, use one
			if($index != null)
				_context = _context.eq($index);

			var _elLl = _context.findAll('[ui-valid]');
			if(!_elLl.length)
				return true;

			// angular unshift value="?" option
			var isSelectNull = function(one, val){
				return '?' == val && one.is('select');
			};

			var _this = this;
			var flags = [];
			// no break -> show all tips of form inputs that require value
			_elLl.each(function(){
				var _el = $(this);

				var val = _el.val();
				if(angular.isString(val))
					val = val.trim();

				if(val && !isSelectNull(_el, val))
					return;

				var rules = _el.attr('ui-valid');
				if(!rules)
					return;

				var arr = rules.split(' ');
				// 'r' means required
				if(arr.contains('r')){
					var modelName = _el.attr('ng-model') || _el.attr('data-ng-model');
					flags.push(modelName);
					tips.on(_el, _el.attr('ui-valid-tips') || _this.getMsg('r'));
				}
			});
			return returnRequiredModel ? flags : !flags.length;
		},

		// 根据排除特定模型、规则后,获取$form的是否验证成功
		validForm: function($form, skipedList, ruleList, index){
			// 用于保存去掉ruleList后各个model对应的违背规则列表,用于调节样式
			// 行转列,之前是规则对应false or NgModelController列表
			// 转换后,变成modelName对应的规则列表
			var modelRuleItems = {};

			// 当$form是$dirty false时候,用dom校验必填项
			var requiredFlags = this.checkFormWithVal($form, true, index);
			var requiredFlagsTarget = requiredFlags;

			// 排除skipedList的必填项规则
			if(ruleList.contains('r')){
				requiredFlagsTarget = _.difference(requiredFlags, skipedList);

				_.each(_.intersection(skipedList, requiredFlags), function(modelName){
					if(!modelRuleItems[modelName])
						modelRuleItems[modelName] = [];

					modelRuleItems[modelName].push('r');
				});
			}
			// 排除之后还有违背必填规则的模型,required就未被校验通过
			var isRequiredFlag = !requiredFlagsTarget.length;


			// 看下是不是因为modelList非必填的导致的$valid是false
			var isValidForm = $form.$valid;
			if(!isValidForm){
				var errorToken = [];

				var errors = $form.$error;
				for(var key in errors){
					if(!errors.hasOwnProperty(key) || !key.contains('__')){
						continue;
					}

					var arr = key.split(/__/);
					var modelName = arr[0];
					var rule = arr[1];

					// === false就是校验通过
					if(errors[key] === false){
						continue;
					}

					if(!modelRuleItems[modelName]){
						modelRuleItems[modelName] = [];
					}
					modelRuleItems[modelName].push(rule);

					var needSkip = ruleList.contains(rule) && skipedList.contains(modelName);
					if(needSkip){
						continue;
					}

					// 如果有非skipedList的model产生的error,就验证不通过
					errorToken.push(key);
				}

				isValidForm = errorToken.length === 0;
			}

			this.filterTipsOff(modelRuleItems, skipedList, ruleList, index);

			return isRequiredFlag && isValidForm;		
		}, 

		// 去掉tips
		filterTipsOff: function(modelRuleItems, skipedList, ruleList, index){
			for(var modelName in modelRuleItems){
				if(!skipedList.contains(modelName)){
					continue;
				}

				// 违反规则去掉skip的rule list
				var rules = modelRuleItems[modelName];
				var rulesSliced = _.difference(rules, ruleList);

				if(!rulesSliced.length){
					var targetEl = $('[ng-model="{0}"],[data-ng-model="{0}"]'.format(modelName));
					if(index != null)
						targetEl = targetEl.eq(index);

					tips.off(targetEl);
				}
			}
		}, 

		// check if val fit these valid rules
		check: function(val, rules, $scope, defaultTips, extendParam){
			// no rules
			if(!rules)
				return {flag: true};

			var arr = rules.split(' ');
			// 'r' means required
			// multiple select blank array == '' -> true, use string to compare
			var isBlank = val === null || val === undefined || val === '' || ('' + val === '');
			if(!arr.contains('r') && isBlank)
				return {flag: true};

			var i = 0, len = arr.length;
			for(; i < len; i++){
				var rule = arr[i];
				if(!rule)
					continue;

				var flag = true;
				if('r' == rule){
					// multiple select blank array == '' -> true
					// so return false
					flag = !isBlank;
				}else if(rule.contains(':')){
					// rules that is complex
					flag = this.checkRule(val, rule.split(/:/), $scope, extendParam);
				}else{
					var pat = this.pats[rule];
					if(pat instanceof RegExp){
						if(angular.isString(val)){
							flag = this.mat(val, pat);
						}
					}else if(angular.isFunction(pat)){
						flag = pat(val);
					}else{
						// only support regexp and function
						flag = false;
					}
				}

				// if get string means valid failed, just show
				if(angular.isString(flag)){
					return {flag: false, msg: flag, rule: rule};
				}

				if(flag === false){
					var msg = this.getMsg(rule, defaultTips) || this.getMsg('tips.valid');
					return {flag: false, msg: msg, rule: rule};
				}
			}

			return {flag: true};
		},
		
		// eg. "fn:checkTarget" -> customized valid function
		// eg. "num:range:target_id:+100" -> return true when val - model val(target_id) < 100
		// eg. "date:range:target_id:+2" -> return true when val - model val(target_id) < 2
		// eg. "date:rangeout:target_id:+2" -> return true when val - model val(target_id) > 2
		// eg. "minlen:char:3"
		// eg. "maxval:float:3.23"
		checkRule: function(val, ruleArr, $scope, extendParam){
			var len = ruleArr.length;
			var pre = ruleArr[0];

			// customized valid function defined in controller $scope
			var getter, targetVal, rangeVal;
			if('fn' == pre){
				var fnName = ruleArr[1];
				getter = $parse(fnName);
				var fn = getter($scope);
				if(!fn){
					return true;
				}

				// execute's context is current scope
				return fn.call($scope, val, extendParam);
			}else if('num' == pre){
				if(len != 4){
					log.i('Invalid rules : ' + ruleArr);
					return false;
				}

				// val targetVal is string, usually generated by user's input
				getter = $parse(ruleArr[2]);
				targetVal = getter($scope);
				if(!targetVal)
					return false;

				var currentVal = parseFloat(val);
				var targetNumVal = parseFloat(targetVal);
			
				rangeVal = parseInt(ruleArr[3], 10);
				if(ruleArr[1] == 'range' && currentVal > targetNumVal + rangeVal)
					return false;
				if(ruleArr[1] == 'rangeout' && currentVal < targetNumVal + rangeVal)
					return false;

				return true;
			}else if('date' == pre){
				if(len != 4){
					log.i('Invalid rules : ' + ruleArr);
					return false;
				}

				// val targetVal is better as a Date object, but it's much more complex
				// here targetVal is a string
				getter = $parse(ruleArr[2]);
				targetVal = getter($scope);
				if(!targetVal)
					return false;

				rangeVal = parseInt(ruleArr[3], 10);
				if(ruleArr[1] == 'range' && Date.parse2(val) > Date.parse2(targetVal).add(rangeVal))
					return false;
				if(ruleArr[1] == 'rangeout' && Date.parse2(val) < Date.parse2(targetVal).add(rangeVal))
					return false;

				return true;
			}else if('minlen' == pre || 'maxlen' == pre){
				if(len != 3){
					log.i('Invalid rules : ' + ruleArr);
					return false;
				}

				var lenVal = parseInt(ruleArr[2], 10);
				if(ruleArr[0] == 'minlen' &&
					(('byte' == ruleArr[1] && val.length < lenVal) ||
					('char' == ruleArr[1] && val.charlen() < lenVal)))
					return false;
				if(ruleArr[0] == 'maxlen' &&
					(('byte' == ruleArr[1] && val.length > lenVal) ||
					('char' == ruleArr[1] && val.charlen() > lenVal)))
					return false;
				return true;
			}else if('minval' == pre || 'maxval' == pre){
				if(len != 3){
					log.i('Invalid rules : ' + ruleArr);
					return false;
				}

				targetVal = 'float' == ruleArr[1] ? parseFloat(ruleArr[2]) : parseInt(ruleArr[2], 10);
				var currentVal = 'float' == ruleArr[1] ? parseFloat(val) : parseInt(val, 10);
				if(pre == 'minval' && currentVal < targetVal)
					return false;
				if(pre == 'maxval' && currentVal > targetVal)
					return false;
				return true;
			}else if('ac' == pre){
				// autocomplete valid check
				if(len != 2 && len != 3){
					log.i('Invalid rules : ' + ruleArr);
					return false;
				}
				getter = $parse(ruleArr[1]);
				targetVal = getter($scope);
				
				// tips: label-value (format)
				var spliter = len == 3 ? ruleArr[2] : '-';
				return targetVal && val.split(spliter)[0] == targetVal;
			}else{
				return true;
			}
		},

		mat: function(val, pat){
			if(!pat)
				return true;

			return pat.test(val);
		},

		getMsg: function(rule, tips){
			// if develeper giving tips (ui-valid-tips) when using this directive, return giving tips
			// if ui-valid-tips="label:Your model label", prepend 'Your model label' to tips and return
			tips = tips || '';
			if(tips && !tips.contains(':')){
				return tips;
			}

			var msg = this.msgs[rule];
			if(rule.contains(':')){
				var ruleFirst = rule.split(':')[0];
				if(['ac', 'maxval', 'minval', 'maxlen', 'minlen'].contains(ruleFirst)){
					msg = this.msgs[ruleFirst];
				}
			}

			if(msg){
				var params0 = tips.contains(':') ? tips.split(/:/)[1] : '';
				var params1 = '';
				if(rule.startsWith('min') || rule.startsWith('max')){
					var ruleArr = rule.split(/:/);
					// eg. rule = "maxval:float:3.23" -> show tips with 3.23
					params1 = ruleArr[ruleArr.length - 1];
				}

				return msg.format(params0, params1);
			}else{
				log.w('No tips for : ' + rule);
				return tips;
			}
		},

		// add your valid function using this
		/*
		eg.
		var myModule = angular.module('myModule', ['ng.service']);
		myModule.run(['uiValid', function(uiValid){
			uiValid.regPat('rule.test', /^\d{2,3}$/, '数字两三个');
		}]);
		*/
		regPat: function(code, pat, msg){
			if(this.pats[code])
				return;

			this.pats[code] = pat;
			this.msgs[code] = msg;
		},

		// default rule / tips defination
		msgs: {
			'r': '{0}不能为空',
			'date': '{0}不正确的日期格式格式,日期格式应该为yyyy-MM-dd',
			'time': '{0}不正确的时间格式,时间格式应该为hh:mm',
			'datetime': '{0}不正确的时间格式,时间格式应该为yyyy-MM-dd hh:mm:ss',
			
			'mobile': '{0}手机号码格式不正确',
			'phone': '{0}不正确的固定电话格式,固定电话格式应该为:区号-固定电话号码',
			
			'int': '{0}必须为整数',
			'posint': '{0}必须为正整数',
			'float': '{0}必须为数字',
			'float1': '{0}格式不正确(最多1位小数)',
			'float2': '{0}格式不正确(最多2位小数)',

			'idno': '{0}身份证号码为15或18位,18位除最后一位可为英文字符“X”外其它位数均为数字',
			'email': '{0}电子邮件只允许字母、数字、“-”、“_”、“.”、@,且所有数字、字母、符号都为半角,字母可以大小写,有且仅包含一个@,字符长度不少于6位',

			'carcode': '{0}车牌号必须是以汉字+大写字母开头,数字部分为4-8位',
			'policy.no': '{0}不是有效的保单号码',
			'report.no': '{0}不是有效的报案号码',
			'claim.no': '{0}不是有效的赔案号码',

			'mac': '{0}请输入正确MAC地址格式,包含数字0-9、字母A-F,如00-33-2D-7B-37-FE',

			'minlen': '{0}字符数不到规定长度{1}',
			'maxlen': '{0}字符数超过规定长度{1}',
			'maxval': '{0}值超过上限{1}',
			'minval': '{0}值小于下限{1}',
			'ac': '请选择自动填充内容',
			'tips.valid': '校验不通过'
		},

		// default rule -> regex/function defination
		pats: {
			'date': function(val){
				return Date.isDateValid(val);
			},
			'time': function(val){
				return Date.isTimeValid(val);
			},
			'datetime': function(val){
				return Date.isDateTimeValid(val);
			},
			
			// 'mobile': /^0?(13[0-9]|15[012356789]|18[0236789]|14[57]|17[0-9])[0-9]{8}$/,
			'mobile': /^1(3|4|5|7|8)\d{9}$/,
			'phone': /^([\d]{3,4}(-|\/))?[\d]{6,8}(-[\d]{1,6})?$/,
			
			'int': /^[\-\+]?([0-9]+)$/,
			'posint': /^\d+$/,
			'float': /^[\-\+]?([0-9]+\.?([0-9]+)?)$/,
			'float1': /^[\-\+]?([0-9]+(\.[0-9]{1})?)$/,
			'float2': /^[\-\+]?([0-9]+(\.[0-9]{2})?)$/,

			'idno': /^\d{6}(((19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])\d{3}([0-9]|x|X))|(\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])\d{3}))$/,
			'email': /^([a-zA-Z\d\-\._]+)@([a-zA-Z\d\-\._]+)$/,

			'carcode': /^(([^\x00-\xff]|[A-Za-z0-9]){2}([^\x00-\xff]|[A-Za-z0-9]){2,6})|([*]-[*])$/,
			'policy.no': /^\d{19,20}$/,
			'report.no': /^\d{19,20}$/,
			'claim.no': /^\d{19,20}$/,

			'mac': /^([0-9A-F]{2})(([-][0-9A-F]{2}){5})$/
		}
	};
}]);

})(angular);

// file ng.ui.js (function(ag){ var moduleName = 'ng.ui';

window.console.log('Begin init module ' + moduleName);
if(ag.isModuleExists && ag.isModuleExists(moduleName)){
	window.console.log('Module already exists!' + moduleName);
	return;
}

var md = ag.module(moduleName, ['ng.config', 'ng.service', 'ng.filter']);

// tips use poshytips
md.directive('uiTip', ['ng.config', 'uiLog', function(conf, log){
	var options = {};
	if(ag.isObject(conf.tipOptions)){
		ag.extend(options, conf.tipOptions);
	}

	return {
		restrict: 'A', 
		link: function(scope, el, attrs){
			if(!attrs.uiTip){
				log.i('Skip tips');
				return;
			}

			var opts = ag.extend(ag.copy(options), scope.$eval(attrs.uiTipOptions));
			// use asychronized function better
			opts.content = attrs.uiTip;

			el.poshytip(opts);
		}
	};
}]);

// datepicker
// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
md.directive('uiDate', ['ng.config', 'uiLog', function(conf, log){
	'use strict';
	var options = {};
	if(ag.isObject(conf.date)){
		ag.extend(options, conf.date);
	}
	return {
		restrict: 'A',
		require: 'ngModel',
		link: function(scope, el, attrs, ctrl){
			var getOptions = function(){
				return ag.extend(ag.copy(options), scope.$eval(attrs.uiDate));
			};

			var init = function(){
				var opts = getOptions();
				log.i('Init datepicker : ' + attrs.ngModel);
				log.i(opts);

				opts.onSelect = function(value, picker){
					scope.$apply(function(){
						ctrl.$setViewValue(el.val());
					});
				};

				el.datepicker('destroy');
				el.addClass('date');
				if(opts.timeFormat){
					el.datetimepicker(opts);
				}else{
					el.datepicker(opts);
				}
			};

			// change format auto
			// add strikethrough or add 0
			var format = function(){
				var val = el.val();
				if(!val)
					return;

				var arr = val.split(' ');
				var ymd = arr[0];

				var ymdNew = ymd;

				// add 0 to month/day
				if(ymd.contains('-')){
					var subArr = ymd.split('-');
					if(subArr.length != 3)
						return;

					ymdNew = subArr[0] + 
						'-' + 
						(subArr[1].length == 1 ? '0' + subArr[1] : subArr[1]) + 
						'-' + 
						(subArr[2].length == 1 ? '0' + subArr[2] : subArr[2]);
					if(arr.length > 1)
						ymdNew += ' ' + arr[1];
				}else{

// if(!ymd.match(/^\d{8}$/)) // return; // ymdNew = ymd.substr(0, 4) + '-' + ymd.substr(4, 2) + '-' + ymd.substr(6, 2); ymdNew = new Date().format('yyyy-MM-dd'); if(arr.length > 1) ymdNew += ' ' + arr[1]; }

				if(ymdNew != val){
					el.val(ymdNew);
					scope.$apply(function(){
						ctrl.$setViewValue(ymdNew);
					});
				}
			};

			el.blur(format);
			// return
			el.keyup(function(e){
				if(e.keyCode == 13)
					format();
			});

			// Watch for changes to the directives options
			// under one condition it's datepicker, another it's datetimepicker
			scope.$watch(getOptions, init, true);
		}
	};
}]);

// autocomplete, use dropdown better
// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
md.directive('uiAutocomplete', ['ng.config', 'uiLog', '$parse', function(conf, log, $parse){
	'use strict';
	var options = {};
	if(ag.isObject(conf.autocomplete)){
		ag.extend(options, conf.autocomplete);
	}

	$(document).click(function(e) {
		var target = $(e.target);
		var shouldHideAc = $('.autoComplete:visible');
		if(shouldHideAc.length && target.is('input[ui-autocomplete]')){
			var extClassNameThis = target.attr('ac-result-class');
			shouldHideAc = shouldHideAc.not('.' + extClassNameThis);
		}

		shouldHideAc.each(function(){
			var extClassName = $(this).attr('class').split(' ')[2];
			$('[ac-result-class="' + extClassName + '"]').trigger('ac-finish');
		});
	});

	var cc = 0;

	return {
		restrict: 'A',
		require: 'ngModel',
		link: function(scope, el, attrs, ctrl){
			var getOptions = function(){
				return ag.extend(ag.copy(options), scope.$eval(attrs.uiAutocomplete));
			};

			var extClassName = 'auto-complete' + (++cc);
			el.attr('ac-result-class', extClassName);

			var init = function(){
				var opts = getOptions();
				log.i('Init autocomplete : ' + attrs.ngModel);
				log.i(opts);

				if(!opts.url){
					log.i('Init autocomplete fail : url required : ' + attrs.ngModel);
					return;
				}

				ctrl.$render = function(){
					// show value for input
					var showLabel = ctrl.$modelValue;
					if(showLabel){
						el.val(showLabel);
					}

					if(!showLabel && opts.targetModel){
						var showValue = $parse(opts.targetModel)(scope);
						if(showValue){
							el.val(showValue);
						}
					}
				};

				var labelKey = opts.labelKey;
				var valueKey = opts.valueKey;
				// json :
				// [{data: {result: i}, value: 'Col' + i}]
				var props = {
					url: opts.url,
					minChars: opts.minChars,
					maxItemsToShow: opts.maxItemsToShow,
					finishOnBlur: opts.finishOnBlur,
					remoteDataType: 'json',
					useCache: false,

					resultsClass: 'acResults autoComplete ' + extClassName,

					// do not request when value contains '-'
					fetchRemoteDataFilter: function(val){
						return val && val.indexOf('-') === -1;
					},
					processData: function(data) {
						var i, r = [], len = data.length;
						for(i = 0; i < len; i++){
							var item = data[i];
							// change here
							var valueShow = valueKey ? item[valueKey] : item;
							var labelShow = labelKey ? (valueShow + (opts.spliter || '-') + item[labelKey]) : valueShow;
							r.push({data: {result: valueShow}, value: labelShow});
						}
						return r;
					},
					onItemSelect: function(item){
						var val = item.data.result;
						var showLabel = item.value;

						var targetModel = opts.targetModel;
						var setter;
						if(targetModel){
							var getter = $parse(targetModel);
							setter = getter.assign;
						}

						scope.$apply(function(){
							if(setter){
								setter(scope, val);
							}
							ctrl.$setViewValue(showLabel);
						});
					}
				};

				// IE8 finishOnBlur fail... because scrollbar trigger blur
				if($.browser.msie){
					props.finishOnBlur = false;
				}

				el.autocomplete(props);

				ctrl.$render();
			};

			// Watch for changes to the directives options
			scope.$watch(getOptions, init, true);
		}
	};
}]);

// select2, use dropdown better as performance is bad 
// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
md.directive('uiSelect2', ['ng.config', 'uiLog', function(conf, log){
	'use strict';
	var options = {};
	if(ag.isObject(conf.select2)){
		ag.extend(options, conf.select2);
	}
	return {
		restrict: 'A',
		require: 'ngModel',
		link: function(scope, el, attrs, ctrl){
			var isMultiple = (attrs.multiple !== undefined);
			var isSelect = el.is('select');

			var opts = ag.extend(ag.copy(options), scope.$eval(attrs.uiSelect2));
			log.i('Init select2 : ');
			log.i(opts);

			var blankText = '';

			if(isSelect){
				delete opts.multiple;
				delete opts.initSelection;

				blankText = el.findAll('option[value=""]').eq(0).text();
			}else if(isMultiple){
				opts.multiple = true;
			}

			var getNgOptionsModelValue = function(){
				return null;
			};

			var tmp = attrs.ngOptions || attrs.uiOptions;
			if(tmp){
				var pat = /^.+in (.+)$/;
				var mat = pat.exec(tmp);
				var ngOptionsModelName = mat[1];
				getNgOptionsModelValue = function(){
					return ngOptionsModelName ? scope.$eval(ngOptionsModelName) : null;
				};
			}

			var model2val = function(modelVal, newVal){
				var valueKey = opts.valueKey || 'v';
				// not use ng-options
				if(!newVal){
					newVal = el.findAll('option').map(function(){
						var item = {};
						item[valueKey] = $(this).val();
						return item;
					}).get();
				}

				// because ng-options use index but not value,
				// so get index by target value (format: [{v: 'yourModelValue'}]) first
				// eg. ng-options="one.v as one.l for one in optsTest"
				var i = 0, len = newVal.length;
				if(isMultiple && ag.isArray(modelVal)){
					var indexLl = [];
					if(!ctrl.$viewValue)
						return indexLl;

					for(; i < len; i++){
						if(ctrl.$viewValue.contains(newVal[i][valueKey])){
							indexLl.push(i);
						}
					}

					return indexLl;
				}else{
					var index = -1;
					for(; i < len; i++){
						if(newVal[i][valueKey] == ctrl.$viewValue){
							index = i;
							break;
						}
					}

					return index;
				} // end if
			};

			ctrl.$render = function(){
				if(!isSelect)
					return;

				var select2index = model2val(ctrl.$modelValue, getNgOptionsModelValue());
				// IE8 need setTimeout
				if($.browser.msie){
					setTimeout(function(){
						el.select2('val', select2index);
						if(attrs.uiOptions && select2index == -1){
							el.prev('.select2-container:first').findAll('.select2-chosen').text(blankText);
						}
					}, 0);
				}else{
					el.select2('val', select2index);
				}
			};

			if(ngOptionsModelName){
				// watch ng-option model
				scope.$watch(ngOptionsModelName, function(newVal, oldVal){
					if(!newVal)
						return;

					// isArray -> multiple
					if(!ctrl.$viewValue){
						el.select2();
						return;
					}

					var index = model2val(ctrl.$viewValue, newVal);
					if((ag.isArray(index) && index.length) || (ag.isNumber(index) && index != -1)){
						el.select2('val', index);
					}else{
						el.select2();
					}
				}, true); // end $watch
			}

			attrs.$observe('disabled', function(val){
				el.select2(val && 'disable' || 'enable');
			});

			// IE8 select val bug
			if($.browser.msie){
				el.bind('select2-selected', function(e){
					if('' == e.val){
						$(this).attr('value', '');
					}
				});
			}

			el.select2(opts);
		}
	};
}]);


md.service('uiDropdownHelper', function(){
	// panel begin zindex
	this.zindex = 1000;
	this.defaultPanelScrollHeight = 150;
	this.highlightClass = 'ui-state-highlight';

	this.setPanelPosition = function(panel, el, opts){
		var offset = el.offset();
		panel.css({
			zindex: this.zindex++, 
			width: el.width(), 
			top: offset.top + el.height(), 
			left: offset.left
		});

		var height = (opts.height || this.defaultPanelScrollHeight) + 'px';
		panel.findAll('.pui-dropdown-items-wrapper').css({height: height});
	};

	this.bindHoverEvent = function(el, selector){
		var selector = 'li';
		el.delegate(selector, 'mouseenter', function(e){
			var hovered = $(e.target);
			if(!hovered.is(selector))
				hovered = hovered.closest(selector);

			el.findAll(selector + '.ui-state-hover').removeClass('ui-state-hover');

			if(!hovered.is('.ui-state-active') && !hovered.is('.ui-state-disabled'))
				hovered.addClass('ui-state-hover');
		}).on('mouseleave', function(){
			el.findAll(selector).removeClass('ui-state-hover');
		});
	};

	this.bindDelegateDocumentEvent = function(attrId){
		$(document).click(function(e){
			var target = $(e.target);

			$('.pui-dropdown-panel').each(function(){
				var panel = $(this);
				var panelDropdownId = panel.attr(attrId);

				if(target.is('.pui-dropdown-filter') || target.is('.ui-dropdown-multiple-input')){
					// hide others panel
					var targetDropdownId = target.attr(attrId);
					if(targetDropdownId !== panelDropdownId)
						panel.triggerHandler('uiDropdownHide');
				}else{
						panel.triggerHandler('uiDropdownHide');
				}
			});

			// multiple choosed label delete span
			// is a span <ul><li><span></span></li></ul>
			if(target.is('[data-pui-ac-close-span]')){
				var acId = target.attr('data-pui-ac-close-span');
				$('.pui-dropdown-panel').each(function(){
					var panel = $(this);
					var panelAcId = panel.attr(attrId);
					if(acId === panelAcId){
						var targetVal = target.parent().attr('data-raw-value');
						panel.triggerHandler('puiAcDelVal', [targetVal]);
					}
				});

				target.parent().remove();
			}
		});
	};

	this.wrapMultipleLi = function(targetVal, targetLabel, acId){
		return '<li data-raw-value="' + targetVal + 
			'" class="pui-autocomplete-token ui-state-active ui-corner-all ui-helper-hidden" ' + 
			'style="display: list-item;"><span data-pui-ac-close-span="' + acId + 
			'" class="pui-autocomplete-token-icon ui-icon ui-icon-close"></span>' + 
			'<span class="pui-autocomplete-token-label" title="' + targetLabel + '">' + targetLabel + '</span></li>';
	};
});

md.directive('uiDropdownPanel', ['$http', 'ng.config', 'uiLog', 'uiDropdownHelper', function($http, conf, log, uiDropdownHelper){
	var tplLi = '<li data-id="{0}" class="pui-dropdown-item pui-dropdown-list-item ui-corner-all">{1}</li>';
	return {
		scope: {
			list: '=', 
			valueField: '@', 
			labelField: '@',
			blankLabel: '@',
			queryUrl: '@',
			modelVal: '=',
			labelWithVal: '@'
		}, 
		link: function(scope, el, attrs){
			var isLabelWithVal = 'true' == scope.labelWithVal;
			var filterList = function(list, targetVal, cb, match){
				if(!list)
					return cb([]);

				if(match && !targetVal)
					return cb(list);

				var filteredList = _.filter(list, function(it){
					var val = '' + it[scope.valueField];
					var label = '' + it[scope.labelField];

					if(match && !(val.contains(targetVal) || 
						val.toLowerCase().contains(targetVal.toLowerCase()) || 
						label.contains(targetVal) || 
						label.toLowerCase().contains(targetVal.toLowerCase())
						)){
						return false;
					}

					var isChoosedAlready = angular.isArray(scope.modelVal) ? 
						scope.modelVal.contains(val) : scope.modelVal == val;
					return !isChoosedAlready;
				});
				cb(filteredList);
			};

			// use dom
			el.on('uiDropdownUnique', function(e, modelVal){
				e.preventDefault();
				e.stopPropagation();

				renderList({list: scope.list, queryUrl: scope.queryUrl, querySearch: scope.querySearch});
			});

			var renderList = function(obj, match, triggerReady){
				var list = obj.list;
				var querySearch = obj.querySearch;
				var queryUrl = obj.queryUrl;

				var tpl = scope.blankLabel ? tplLi.format('', scope.blankLabel) : '';
				var cb = function(ll){
					var i = 0, len = ll.length, one;
					for(; i < len; i++){
						one = ll[i];
						tpl += tplLi.format(one[scope.valueField], 
							isLabelWithVal ? one[scope.valueField] + '-' + one[scope.labelField] : one[scope.labelField]);
					}

					el.findAll('ul').html(tpl);

					if(triggerReady)
						el.triggerHandler('uiDropdownListReady');
				};

				if(queryUrl){
					if(!querySearch){
						cb([]);
					}else{
						$http.get(Consts.getAppPath(queryUrl) + '?q=' + encodeURI(querySearch)).success(function(data){
							cb(data);
						});
					}
				}else{
					filterList(list, querySearch, cb, match);
				}
			};

			scope.$watch(function(){
				return {list: scope.list, queryUrl: scope.queryUrl, querySearch: scope.querySearch};
			}, function(obj){
				renderList(obj, true, true);
			}, true);

			var focusByCalIndex = function(calFn){
				var liList = el.findAll('li');
				var liCurrent = liList.filter('.ui-state-hover');
				liCurrent.removeClass('ui-state-hover');

				var index = liList.index(liCurrent);

				var targetIndex = calFn(index, liList.length);
				liList.eq(targetIndex).addClass('ui-state-hover');
			};

			var focusPrev = function(){
				focusByCalIndex(function(index, len){
					return index <= 0 ? len - 1 : index - 1;
				});
			};
			var focusNext = function(){
				focusByCalIndex(function(index, len){
					return index < len - 1 ? index + 1 : 0;
				});
			};
			var chooseCurrent = function(){
				var li = el.findAll('li.ui-state-hover');
				// no hover, choose first by default
				if(!li.length)
					li = el.findAll('li').eq(0);
				if(!li.length)
					return;

				var targetId = li.attr('data-id');
				var targetLabel = li.text();
				el.triggerHandler('uiDropdownChoose', [targetId, targetLabel]);
			};
			var hidePanel = function(){
				el.triggerHandler('uiDropdownHide');
			};

			el.findAll('.pui-dropdown-filter').keyup(function(e){
				e.stopPropagation();

				var keyCode = e.keyCode;
				switch(keyCode){
					// up
					case 38: 
						focusPrev();
						break;
					// down
					case 40: 
						focusNext();
						break;
					// return
					case 13: 
						chooseCurrent();
						break;
					// esc
					case 27: 
						hidePanel();
						break;
					default:
						break;
				}
			});
		}
	};
}]);

// dropdown
md.directive('uiDropdown', ['$parse', '$compile', 'ng.config', 'uiLog', 'uiDropdownHelper', function($parse, $compile, conf, log, uiDropdownHelper){
	var countNum = 0;
	var attrId = 'data-dropdown-id';

	var dropdownPaneTpl ='<div ui-dropdown-panel="" label-with-val="{6}" model-val="{5}" query-url="{4}" blank-label="{3}" label-field="{2}" value-field="{1}" list="{0}" ' + 
		'class="pui-dropdown-panel ui-widget-content ui-corner-all ui-helper-hidden pui-shadow">' + 
		' <div class="pui-dropdown-filter-container">' + 
		'	<input type="text" ng-model="querySearch" class="pui-dropdown-filter pui-inputtext ui-widget ui-state-default ui-corner-all" />' + 
		'	<span class="ui-icon ui-icon-search"></span>' + 
		' </div>' + 
		' <div class="pui-dropdown-items-wrapper">' + 
		'	<ul class="pui-dropdown-items pui-dropdown-list ui-widget-content ui-widget ui-corner-all ui-helper-reset">' + 
		'	</ul>' + 
		' </div>' + 
		'</div>';

	uiDropdownHelper.bindDelegateDocumentEvent(attrId);

	return {
		restrict: 'A',
		require: 'ngModel',
		transclude: true, 

		compile: function(el, attrs, transcludeFn){
			return function(scope, el, attrs, ctrl){
				var opts = scope.$eval(attrs.uiDropdown) || {};
				opts = angular.extend(angular.copy(conf.dropdownOptions), opts);
				var listModel = opts.list;
				if(!listModel && !opts.queryUrl){
					log.w('No listModel or queryUrl given!');
					return;
				}

				var getList = function(){
					return $parse(listModel)(scope);
				};

				var isMultiple = !!opts.multiple;
				var isEditable = !!opts.editable;

				var cc = countNum++;

				if(isEditable){
					attrs.$observe('uiEditable', function(val){
						if(val === undefined)
							return;

						if('true' === val){
							el.parent().addClass('ui-helper-editable');
						}else{
							renderLabel(el.val());
							el.parent().removeClass('ui-helper-editable');
						}
					});
				}

				attrs.$observe('disabled', function(val){
					if(val === undefined)
						return;

					// disabled -> true
					if(val){
						hidePanel();

						if(isMultiple){
							var input = el.prev('.pui-autocomplete-multiple').findAll('.ui-dropdown-multiple-input');
							input.attr('disabled', true);
							var ul = input.parent().parent();
							ul.findAll('.ui-icon-close').hide();
							ul.findAll('.pui-autocomplete-token-label').addClass('ui-state-disabled');
						}else{
							el.parent().parent().addClass('ui-state-disabled');
						}
					}else{
						if(isMultiple){
							var input = el.prev('.pui-autocomplete-multiple').findAll('.ui-dropdown-multiple-input');
							input.removeAttr('disabled');
							var ul = input.parent().parent();
							ul.findAll('.ui-icon-close').show();
							ul.findAll('.pui-autocomplete-token-label').removeClass('ui-state-disabled');
						}else{
							el.parent().parent().removeClass('ui-state-disabled');
						}
					}
				});

				var tplPanel = dropdownPaneTpl.format(listModel, opts.valueField, opts.labelField, 
					opts.blankLabel || '', opts.queryUrl || '', attrs.ngModel, opts.labelWithVal);
				var panel = $compile(tplPanel)(scope);
				panel.attr(attrId, cc).css('z-index', opts.zIndex);
				panel.findAll('.pui-dropdown-filter').attr(attrId, cc);
				panel.appendTo($(document.body));

				// use enter/tab trigger
				panel.on('uiDropdownChoose', function(e, targetVal, targetLabel){
					e.preventDefault();
					e.stopPropagation();

					chooseCurrent(targetVal, targetLabel);
				});

				// when set list after model set
				panel.on('uiDropdownListReady', function(e){
					e.preventDefault();
					e.stopPropagation();

					ctrl.$render();
				});


				panel.on('uiDropdownHide', function(e){
					e.preventDefault();
					e.stopPropagation();

					if(isActive)
						hidePanel();
				});

				if(isMultiple){
					panel.on('puiAcDelVal', function(e, targetVal){
						var input = el.prev('.pui-autocomplete-multiple').findAll('.ui-dropdown-multiple-input');

						// remove one from array
						var targetValList = ctrl.$modelValue || [];
						var index = targetValList.indexOf(targetVal);
						if(index >= 0){
							targetValList.splice(index, 1);
							el.val(targetValList.toString());

							scope.$apply(function(){
								ctrl.$setViewValue(targetValList);
								if(attrs.uiChange){
									scope.$eval(attrs.uiChange);
								}
							});
						}
					});
				}

				uiDropdownHelper.bindHoverEvent(panel, 'li');

				panel.delegate('li', 'click', function(e){
					e.preventDefault();
					e.stopPropagation();

					var li = $(e.target);
					var targetVal = li.attr('data-id');
					var targetLabel = li.text();
					chooseCurrent(targetVal, targetLabel);
				});

				if(isMultiple){
					transcludeFn(scope, function(clone){
						el.hide();

						// help input
						var input = $('<input type="text" class="pui-textfield ui-dropdown-multiple-input" />')
							.attr(attrId, cc).width(opts.widthMultipleInput + 'px');
						el.before(input);

						input.wrap('<li class="pui-autocomplete-input-token"></li>');
						input.parent().wrap('<ul class="pui-autocomplete-multiple ui-widget pui-inputtext ui-state-default ui-corner-all"></ul>');
						if(opts.widthWrapper){
							input.parent().parent().width(opts.widthWrapper + 'px');
						}
					});
				}else if(isEditable){
					transcludeFn(scope, function(clone){
						el.wrap('<div class="ui-helper-hidden-accessible ui-helper-editable"></div>');
						var elParent = el.parent();

						opts.width = el.width();
						elParent.wrap('<div class="pui-dropdown ui-widget ui-state-default ui-corner-all ui-helper-clearfix" style="width: ' + opts.width + 'px;"></div>');
						elParent.after('<div class="pui-dropdown-trigger ui-state-default ui-corner-right"><span class="ui-icon ui-icon-triangle-1-s"></span></div>');
						elParent.after('<label class="pui-dropdown-label pui-inputtext ui-corner-all" style="width: ' + (opts.width - opts.widthDiff) + 'px;">' + (opts.blankLabel || '--/--') + '</label>');

						el.css({border: 'none', 'background-color': '#fff', height: (conf.inputHeight || 27) + 'px'});
					});
				}else{
					transcludeFn(scope, function(clone){
						el.wrap('<div class="ui-helper-hidden-accessible"></div>');
						var elParent = el.parent();

						opts.width = el.width();
						elParent.wrap('<div class="pui-dropdown ui-widget ui-state-default ui-corner-all ui-helper-clearfix" style="width: ' + opts.width + 'px;"></div>');
						elParent.after('<div class="pui-dropdown-trigger ui-state-default ui-corner-right"><span class="ui-icon ui-icon-triangle-1-s"></span></div>');
						elParent.after('<label class="pui-dropdown-label pui-inputtext ui-corner-all" style="width: ' + (opts.width - opts.widthDiff) + 'px;">' + (opts.blankLabel || '--/--') + '</label>');
					});
				}

				var renderLabelMultiple = function(valList, labelList, clear){
					var input = el.prev('.pui-autocomplete-multiple').findAll('.ui-dropdown-multiple-input');
					var li = input.parent();
					if(clear){
						li.siblings().remove();
					}

					valList = valList || [];
					el.val(valList.toString());
					if(!valList.length)
						return;

					labelList = labelList || [];

					_.each(valList, function(targetVal, i){
						var targetLabel = labelList[i];

						if(!targetLabel){
							var item = _.find(getList(), function(it){
								return it[opts.valueField] == targetVal;
							});
							targetLabel = item ? item[opts.labelField] : targetVal;

							if(opts.labelWithVal && item){
								targetLabel = item[opts.valueField] + '-' + targetLabel;
							}
						}

						li.before(uiDropdownHelper.wrapMultipleLi(targetVal, targetLabel, cc));
					});

					// if disabled
					if(attrs.disabled){
						var ul = li.parent();
						ul.findAll('.ui-icon-close').hide();
						ul.findAll('.pui-autocomplete-token-label').addClass('ui-state-disabled');
					}
				};

				var renderLabel = function(modelVal, labelVal){
					el.val(modelVal || '');

					var label = opts.blankLabel || '';
					if(labelVal){
						label = labelVal;
					}else if(modelVal){
						if(isEditable){
							label = modelVal;
						}else{
							// not ===
							var item = _.find(getList(), function(it){
								return it[opts.valueField] == modelVal;
							});
							label = item ? item[opts.labelField] : modelVal;

							if(opts.labelWithVal && item){
								label = item[opts.valueField] + '-' + label;
							}
						}
					}
					el.parent().parent().findAll('.pui-dropdown-label').attr('title', label).text(label);
				};

				var chooseCurrent = function(targetVal, targetLabel){
					hidePanel();
					// reset input for next time choose from panel
					panel.findAll('.pui-dropdown-filter').val('');

					if(isMultiple){
						var targetValList = ctrl.$modelValue || [];
						// if not blank
						if(targetVal && !targetValList.contains(targetVal)){
							renderLabelMultiple([targetVal], [targetLabel]);
							targetValList.push(targetVal);
							scope.$apply(function(){
								ctrl.$setViewValue(targetValList);
								if(attrs.uiChange){
									scope.$eval(attrs.uiChange);
								}
							});
						}
					}else{
						renderLabel(targetVal, targetLabel);
						scope.$apply(function(){
							ctrl.$setViewValue(targetVal);
							if(attrs.uiChange){
								scope.$eval(attrs.uiChange);
							}
						});
					}
				};

				ctrl.$render = function(){
					isMultiple ? renderLabelMultiple(ctrl.$modelValue, null, true) : renderLabel(ctrl.$modelValue);
				};

				var isActive = false;
				var hidePanel = function(){
					panel.hide();
					isActive = false;
				};

				var showPanel = function(){
					var relativeEl = isMultiple ? el.prev('.pui-autocomplete-multiple').findAll('.ui-dropdown-multiple-input') : 
						el.parent().parent();
					uiDropdownHelper.setPanelPosition(panel, relativeEl, opts);

					panel.show();
					panel.findAll('.pui-dropdown-filter').focus();
					panel.triggerHandler('uiDropdownUnique', [ctrl.$modelValue]);
					isActive = true;
				};

				if(isMultiple){
						el.prev('.pui-autocomplete-multiple').findAll('.ui-dropdown-multiple-input').on('focus', function(e){
						e.stopPropagation();
						showPanel();
					});
				}else{
					el.parent().parent().click(function(e){
						e.stopPropagation();
						if(attrs.disabled)
							return;

						// editable
						if(isEditable){
							var target = $(e.target);
							if(!target.is('input')){
								isActive ? hidePanel() : showPanel();
							}
						}else{
							isActive ? hidePanel() : showPanel();
						}
					});
				}
			}; // end return link
		}
	};
}]);

// lhgdialog style
// use uiDialog2 instead
md.directive('uiDialog', ['ng.config', 'uiLog', function(conf, log){
	'use strict';
	return {
		restrict: 'A',

		templateUrl: conf.context + 'tpl/lhgdialog.html',
		replace: true,
		transclude: true,

		scope: {
			// titler not title, or raw browser will make it title like
			titler: '@',
			visible: '@',

			// using parent method
			onOk: '&',
			onCancel: '&'
		},

		compile: function(el, attrs, transclude){
			return {
				post: function(scope, el, attrs, ctrl){
					var opts = scope.$eval(attrs.uiDialog) || {};
					log.i('Compile dialog ui : ');
					log.i(opts);

					el.findAll('.ui_min').click(function(e){
						el.find('.ui_icon, .ui_main, .ui_buttons').hide();
						el.find('.ui_res').css('display', 'inline-block');
						$(this).hide();
						return false;
					});
					el.findAll('.ui_res').click(function(e){
						el.find('.ui_icon, .ui_main, .ui_buttons').show();
						el.find('.ui_min').css('display', 'inline-block');
						$(this).hide();
						return false;
					});

					// jquery ui drag
					// locate center, after binding width changed, so need location center again
					// use jquery to get document.width as IE fails
					var innerTbl = el.findAll('table').eq(0);
					// for div not with parent body, location center by tblWidth/tblHeight parameter
					var tblW = opts.tblWidth || innerTbl.width();
					var tblH = opts.tblHeight || innerTbl.height();

					var left = Math.floor($(window).width() - tblW) / 2;
					var top = Math.floor($(window).height() - tblH) / 2;

					// if targetElementModel assign, location bellow that element
					if(opts.targetElementModel){
						var targetEl = $('[ng-model="{0}"],[data-ng-model="{0}"]'.format(opts.targetElementModel));
						if(targetEl.length){
							var leftFix = 20;
							var topFix = 20;

							var offsetEl = targetEl.eq(0).offset();
							left = offsetEl.left + leftFix;
							top = offsetEl.top + topFix;
						}
					}

					if(left <= 0)
						left = 10;
					if(top <= 0)
						top = 10;

					var pos = {left: left + 'px', top: top + 'px'};
					el.findAll('div.ng-ui-dialog-wrapper').css(pos);

					// need lock, add block div
					if(opts.lock === true){
						var tplBlock = '<div class="ng-ui-dialog-block"></div>';
						el.prepend(tplBlock);
					}
					if(opts.draggable === true){
						var daggableOpts = {handle: '.ui_title_bar'};
						if(ag.isObject(conf.dialogDraggable)){
							ag.extend(daggableOpts, conf.dialogDraggable);
						}
						ag.extend(daggableOpts, opts);

						el.findAll('div.ng-ui-dialog-wrapper').draggable(daggableOpts);
					}

					// min-height
					if(opts.height || opts.width){
						var props = {overflow: 'auto'};
						if(ag.isNumber(opts.height))
							props.height = '' + opts.height + 'px';
						if(ag.isNumber(opts.width))
							props.width = '' + opts.width + 'px';

						el.findAll('.ui_content').css(props);
					}
				} // /post
			};
		} // /compile
	};
}]);

// scope: true better
md.directive('uiDialog2', ['$parse', '$compile', 'ng.config', 'uiLog', 'safeApply', function($parse, $compile, conf, log, safeApply){
	'use strict';

	var cc = 0;

	var fixCenter = function(dialog, fixDelay){
		// setTimeout -> locate center after $digest -> dom rebuild
		// donot use $timeout as need not $digest again
		setTimeout(function(){
			var wrap = dialog.DOM.wrap[0];
			var left = ($(window).width() - wrap.offsetWidth) / 2;
			var top = ($(window).height() - wrap.offsetHeight) / 2;
			dialog.position(left, top);
		}, fixDelay || 200);
	};
	return {
		restrict: 'A',
		link: function(scope, el, attrs){
			var opts = scope.$eval(attrs.uiDialog2) || {};
			log.i('Compile dialog2 ui : ');
			log.i(opts);

			if(!opts.showModel){
				log.w('No show model given!');
				return;
			}

			// one page has more than one dialogs with same dialog id
			opts.dialogId = (opts.dialogId || '') + '_' + (++cc);

			var subScope;

			// lhgdialog properties
			var props = {};
			if(ag.isObject(conf.dialog2)){
				ag.extend(props, conf.dialog2);
			}

			props.id = opts.dialogId;
			props.title = opts.titleModel ? ('{{' + opts.titleModel + '}}'): opts.title;
			props.content = el.html();
			props.init = function(){
				var targetScope = scope;
				if(opts.closeForce)
					subScope = targetScope = scope.$new();

				// in watch
				$compile(this.DOM.wrap.findAll('.ui_dialog'))(targetScope);
				if(opts.fixPosition){
					var that = this;
					fixCenter(that, opts.fixDelay);
				}
			};

			// a flag that make sure lhgdialog close only once
			// because model true -> false trigger close again
			var isInClose = false;
			props.close = function(){
				isInClose = true;

				// use close in dialog toolbar will execute twice
				// use button in dialog user defined will execute once which trigger by watch list
				var getter = $parse(opts.showModel);
				var isShow = getter(scope);
				if(isShow){
					var setter = getter.assign;
					// trigger watch again
					safeApply(scope, function(){
						setter(scope, false);
						if(opts.closeSettings){
							var key = opts.closeSettings.key;
							var val = opts.closeSettings.value;
							$parse(key).assign(scope, val);
						}
						if(opts.closeFn){
							var fnTarget = $parse(opts.closeFn)(scope);
							if(ag.isFunction(fnTarget)){
								fnTarget();
							}
						}
					});
				};

				isInClose = false;

				if(opts.closeForce && subScope){
					subScope.$destroy();
					subScope = null;
				}

				// not really close
				return opts.closeForce ? true : false;
			};

			// @depricated, use ext instead
			_.each(['lock', 'drag', 'fixed', 'resize'], function(it){
				if(angular.isDefined(opts[it]))
					props[it] = opts[it];
			});
			_.each(['width', 'height', 'left', 'top'], function(it){
				if(opts[it])
					props[it] = opts[it];
			});

			// overwrite other properties
			if(opts.ext)
				ag.extend(props, opts.ext);

			scope.$watch(opts.showModel, function(val){
				// show
				if(val){
					var target = $.dialog.list[opts.dialogId];
					if(target){
						if(target.config.lock){
							target.lock();
						}else{
							target.zindex();
						}
						if(opts.fixPosition){
							fixCenter(target);
						}
						target.show();
					}else{
						$.dialog(angular.copy(props));
					}
				}else{
					// hide
					var target = $.dialog.list[opts.dialogId];
					if(target){
						if(opts.closeForce){
							if(!isInClose)
								target.close();
						}else{
							target.hide();
						}
					}
				}
			}); // end $watch showModel
		} // end link
	};
}]);

// use template better, the angular way
// jquery dom way -> support compile template lazy
md.directive('uiTabs', ['$compile', '$parse', 'safeApply', 'uiLog', 'uiTips', 
	function($compile, $parse, safeApply, log, tips){
	'use strict';
	return {
		restrict: 'A',
		link: function(scope, el, attrs){
			var opts = scope.$eval(attrs.uiTabs) || {};

			// new style based on bootstrap
			var contentsNews = el.siblings('.tabs');
			var isStyleNew = !!contentsNews.length;

			var navs, contents;
			if(isStyleNew){
				contents = contentsNews;
				navs = el.findAll('li');
			}else{
				navs = el.findAll('.ng-ui-tabs').eq(0).findAll('li');
				contents = el.findAll('.ng-ui-tab-content').eq(0).findAll('.ng-ui-tab-pane');
			}

			if(!navs.length || !contents.length || navs.length != contents.length){
				log.i('Compile ui-tabs failed : tabs length not match!');
				return;
			}

			navs.findAll('a').click(function(e){
				e.preventDefault();
				e.stopPropagation();

				var navLinkLl = navs.findAll('a');
				var index = navLinkLl.index(this);

				var triggerIndex = navs.index(navs.filter('.active'));

				var flag = true;
				if(opts.beforeFn){
					var fnTarget = $parse(opts.beforeFn)(scope);
					if(fnTarget){
						if(opts.digest){
							safeApply(scope, function(){
								flag = fnTarget(index, triggerIndex);
							});
						}else{
							flag = fnTarget(index, triggerIndex);
						}
					}
				}
				if(!flag)
					return;

				navLinkLl.not(':eq(' + index + ')').parent().removeClass('active');
				navLinkLl.eq(index).parent().addClass('active');

				// tips off
				var lastVisitedContent = contents.not(':eq(' + index + ')').filter('.active');
				tips.offInContext(lastVisitedContent);

				contents.not(':eq(' + index + ')').removeClass('active');
				
				var targetPane = contents.eq(index);
				targetPane.addClass('active');

				// if link delay
				var isLinkDelay = targetPane.attr('is-link');
				if(isLinkDelay){
					(function(){
						var tplEl = targetPane.findAll('script').eq(0);
						if(!tplEl.length)
							return;

						var inner = targetPane.findAll('.tpl');
						// compile only once
						if(isLinkDelay !== 'repeat' && inner.length)
							return;

						// empty div first
						inner.remove();
						
						// compile and link
						var compiledEl = $compile(tplEl.html())(scope);
						compiledEl.addClass('tpl').appendTo(targetPane);
					})();
				}
				safeApply(scope, function(){
					scope.$broadcast('TabFocus', index);
				});
				
				return false;
			});

			// trigger first
			navs.findAll('a').eq(opts.targetIndex || 0).trigger('click');
		}
	};
}]);

// context menu
md.directive('uiContextMenu', ['$parse', 'ng.config', 'uiLog', function($parse, conf, log){
	'use strict';

	$(document).click(function(e){
		$('.contextMenuPlugin').hide();
	});

	var attrId = 'ui-context-menu-id';
	var options = {};
	if(ag.isObject(conf.contextMenu)){
		ag.extend(options, conf.contextMenu);
	}

	return {
		restrict: 'A',
		require: 'ngModel',

		link: function(scope, el, attrs, ctrl){
			var opts = ag.extend(ag.copy(options), scope.$eval(attrs.uiContextMenu));
			log.i('Create context menu...' + JSON.stringify(opts));

			if(!ctrl)
				return;

			// add uuid
			if(!el.attr(attrId)){
				el.attr(attrId, Math.guid());
			}
			
			var createMenu = function(){
				if(!ctrl.$modelValue || !ag.isArray(ctrl.$modelValue)){
					log.w('Context menu model must be an array!');
					return null;
				}

				var menuTpl = '<ul id="{2}" class="{0}"><div class="{1}"></div></ul>';
				var inner = menuTpl.format(opts.contextMenuClass, opts.gutterLineClass, el.attr(attrId));
				var menu = $(inner).appendTo(document.body);
				if(opts.title){
					 $('<li class="{0}"></li>'.format(opts.headerClass)).text(opts.title).appendTo(menu);
				}

				var itemTpl = '<li><a href="javascript:void(0);" ui-context-item-id="{0}"><span></span></a></li>';
				var i = 0;
				for(; i < ctrl.$modelValue.length; i++){
					var item = ctrl.$modelValue[i];
					if(item){
						var row = $(itemTpl.format(item.id || ''));

						var rowSpan = row.findAll('span');
						rowSpan.text(item.label);

						if(item.icon){
							var icon = $('<img>');
							icon.attr('src', conf.contextPre + '/' + item.icon);
							icon.insertBefore(rowSpan);
						}

						row.appendTo(menu);
					}else{
						 $('<li class="{0}"></li>'.format(opts.seperatorClass)).appendTo(menu);
					}
				}

				menu.bind('contextmenu', function(){return false;});
				return menu;
			};

			ctrl.$render = function(){
				var contextMenuId = el.attr(attrId);
				if(!contextMenuId){
					log.w('No context menu id!');
					return;
				}

				// dom already exists, remove first
				$('#' + contextMenuId).remove();

				el.unbind('contextmenu').bind('contextmenu', function(e){
					var thisContextMenu = $('#' + contextMenuId);
					var isAlreadyExists = !!thisContextMenu.length;

					var menu = isAlreadyExists ? thisContextMenu : createMenu();
					if(!menu){
						log.w('No context menu model!');
						return;
					}

					var left = e.pageX + 5;
					var top = e.pageY;
					if (top + menu.height() >= $(window).height()){
						top -= menu.height();
					}
					if (left + menu.width() >= $(window).width()){
						left -= menu.width();
					}

					menu.css({zIndex: opts.zIndex || 1000001, left: left, top: top}).show();
					if(!isAlreadyExists){
						// Cover rest of page with invisible div that when clicked will cancel the popup.
						var bg = $('<div></div>')
							.css({left: 0, top: 0, width: '100%', height: '100%', position: 'absolute', zIndex: 1000000})
							.appendTo(document.body)
							.bind('contextmenu click', function(){
								bg.remove();
								menu.remove();
								return false;
							});

						menu.findAll('a').click(function(e){
							bg.remove();
							menu.remove();

							// call back, id as parameter
							var itemId = $(this).attr('ui-context-item-id');
							if(itemId && opts.fn){
								var getter = $parse(opts.fn);
								var fnTarget = getter(scope);
								if(fnTarget){
									scope.$apply(function(){
										fnTarget(itemId);
									});
								}
							}

							return false;
						});
					}
					// cancel browser context menu
					return false;
				});
			};// \ctrl.render
		}// \link
	};
}]);

// progress bar
md.directive('uiProgressBar', ['uiLog', function(log){
	'use strict';
	return {
		require: 'ngModel',
		restrict: 'A',
		replace: true,
		template: '<div class="box ng-ui-progressbar">' +
			'<div class="ng-ui-progressbar-text"></div>' +
			'<div class="ng-ui-progressbar-value"></div>' +
			'</div>',

		link: function(scope, el, attrs, ctrl){
			var opts = scope.$eval(attrs.uiProgressBar) || {};
			var textPre = opts.textPre || 'Completed';

			ctrl.$render = function(){
				if(!ag.isNumber(ctrl.$modelValue) || ctrl.$modelValue < 0 || ctrl.$modelValue > 100){
					log.w('Progress bar model must be a number between 0-100!');
					return;
				}

				var textDiv = el.findAll('.ng-ui-progressbar-text');
				var valDiv = el.findAll('.ng-ui-progressbar-value');
				textDiv.text(textPre + ctrl.$modelValue + '%');
				valDiv.css({width: ctrl.$modelValue + '%'});
			};
		}// \link
	};
}]);

// key enter
md.directive('uiEnter', function(){
	return {
		restrict: 'A',
		link: function(scope, el, attrs){
			el.keyup(function(e){
				 // return
				 if(13 != e.keyCode)
					 return;

				 scope.$apply(function(){
					 scope.$eval(attrs.uiEnter);
				 });
			});
		}
	};			
});

// raw event when do dom operation only, no $digest
md.directive('uiRawBind', function(){
	return {
		restrict: 'A',
		link: function(scope, el, attrs){
			el.on(attrs.bindType || 'click', function(e){
				scope.$eval(attrs.uiRawBind)
			});
		}
	};
});

// shortcuts
md.directive('uiShortkey', ['safeApply', 'uiLog', function(safeApply, log){
	'use strict';
	return {
		restrict: 'A',
		link: function(scope, el, attrs, ctrl){
			if(!attrs.uiShortkey){
				return;
			}

			log.i('Init shortkey : ');
			log.i(attrs.uiShortkey);

			$.hotkeys.add(attrs.uiShortkey, function(){
				safeApply(scope, function(){
					scope.$eval(attrs.uiShortkeyFn);
				});
			});
		}// \link
	};
}]);

// placeholder for IE9-, not prefer
md.directive('uiPlaceholder', ['uiLog', function(log){
	'use strict';

	return {
		restrict: 'A',
		require: 'ngModel',

		link: function(scope, el, attrs){
			// only ie
			if(!$.browser.msie){
				log.i('No IE not neccessory!');
				return;
			}

			if('INPUT' != el[0].nodeName){
				log.w('Init extPlaceholder failed : not a INPUT element!');
				return;
			}

			var placeholderTxt = el.attr('placeholder');
			if(!placeholderTxt){
				log.w('No placeholder set!');
				return;
			}

			var opts = scope.$eval(attrs.extPlaceholder) || {};
			log.i('Init extPlaceholder ' + placeholderTxt + ' - ' + JSON.stringify(opts));

			// bind click/blur/change
			el.bind({
				click: function(e){
					var val = $(this).val().trim();
					if(val == placeholderTxt)
						el.val('').removeClass('ng-blur-placeholder');
				},

				focus: function(e){
					var val = $(this).val().trim();
					if(val == placeholderTxt)
						el.val('').removeClass('ng-blur-placeholder');
				},

				blur: function(e){
					var val = $(this).val().trim();
					if(val == '')
						el.val(placeholderTxt).addClass('ng-blur-placeholder');
				},

				change: function(e){
					var val = $(this).val().trim();
					if(val == '')
						el.val(placeholderTxt).addClass('ng-blur-placeholder');
				},

				keyup: function(e){
					var val = $(this).val().trim();
					if(!val || val == placeholderTxt)
						el.val(placeholderTxt).addClass('ng-blur-placeholder');
				}
			});

			// add watch when model is not undefined
			scope.$watch(attrs.ngModel, function(val){
				if(!val && !el.val()){
					el.val(placeholderTxt).addClass('ng-blur-placeholder');
				}else{
					el.removeClass('ng-blur-placeholder');
				}
			});
		}// \link
	};
}]);

// list watch
// use ng-init/ng-change work with ng-repeat better
// this $watch support edit form with model assigned, fire $digest, ng-change do not as user not trigger browser event yet
md.directive('uiListWatch', ['$parse', 'uiLog', function($parse, log){
	'use strict';

	return {
		restrict: 'A',
		require: 'ngModel',

		// begin link ***
		link: function(scope, el, attrs, ctrl){
			var opts = scope.$eval(attrs.uiListWatch) || {};
			if(!opts.targetProperty || !opts.fn){
				log.w('Directive list watch targetProperty/fn required!');
				return;
			}

			var getter = $parse(opts.fn);
			var fnTarget = getter(scope);
			if(!fnTarget || !ag.isFunction(fnTarget)){
				log.w('Directive list watch fn should be a function!');
				return;
			}

			scope.$watch(attrs.ngModel, function(val){
				var getter = $parse(opts.targetProperty);
				var setter = getter.assign;
				setter(scope, fnTarget(val));
			});
		}
		// end link ***

	}; // end return
}]);

// validation -> donot watch $validity/$required (binding ng-show etc.), use tips instead
// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
md.directive('uiValid', ['$parse', 'ng.config', 'uiLog', 'uiValid', 'uiTips', function($parse, conf, log, valid, tips){
	'use strict';
	var uiValidAttrIdName = 'ui-valid-id';
	var uiValidRefered = {};
	return {
		restrict: 'A',
		require: 'ngModel',
		link: function(scope, el, attrs, ctrl){
			// add guid to this element
			var validId = el.attr(uiValidAttrIdName);
			if(!validId){
				validId = Math.guid();
				el.attr(uiValidAttrIdName, validId);
			}

			var getRules = function(){
				return attrs.uiValid;
			};

			// require not show tips
			var notHoverShow = 'true' == attrs.uiValidNotHover;

			var lastOldRules;
			var validFn = function(value, oldRules){
				var sp = '__';

				var rules = getRules();
				var r = valid.check(value, rules, scope, attrs.uiValidTips, {thisModel: attrs.ngModel});

				if(lastOldRules && !oldRules)
					oldRules = lastOldRules;

				if(r.flag && oldRules){
					rules = rules ? rules + ' ' + oldRules : oldRules;
				}

				if(rules){
					// set form $error
					var arrInner = rules.split(' ').unique();
					var i = 0;
					for(; i < arrInner.length; i++){
						var oneRule = arrInner[i];
						if(!oneRule.trim())
							continue;
						ctrl.$setValidity(attrs.ngModel + sp + oneRule, r.flag ? true : oneRule != r.rule);
					}
				}

				if(!r.flag){
					tips.on(el, r.msg, notHoverShow && 'r' == r.rule);
				}else{
					tips.off(el);
				}
				return r.flag;
			};

			var init = function(){
				var rules = getRules();
				log.i('Init valid : ' + attrs.ngModel);
				log.i(rules);

				if(!rules)
					return;

				// clear ctrl.$parsers, use uiTips.on/off instead $watch form's $error, as ng-show effect layout
				// donot use angluar valid function (in $parse array)
				// tips: donot use email/url directives provided by angular
				if(ctrl.$parsers && ctrl.$parsers.length > 0){
					ctrl.$parsers.clear();
				}
				if(ctrl.$formatters && ctrl.$formatters.length > 0){
					ctrl.$formatters.clear();
				}

				ctrl.$parsers.unshift(function(value){
					return validFn(value) ? value : undefined;
				});

				// set model value directly need not validate again unless ctrl.$invalid === true
				ctrl.$formatters.unshift(function(value){
					if(value !== undefined && 
						(ctrl.$invalid || el.hasClass(conf.invalidClass))){
						validFn(value);
					}
					return value;
				});
			};

			// validation relative to other model
			// if rules is dynamical, make sure that rules first set has target model declaration
			// because bellow block only run once
			var rules = getRules();
			if(rules){
				var arr = rules.split(' ');
				var watchedLl = [];

				// it sucks...
				var i = 0;
				for(; i < arr.length; i++){
					if(!arr[i].contains(':'))
						continue;

					var ruleArr = arr[i].split(':');
					if(!['num', 'date', 'watch'].contains(ruleArr[0]))
						continue;
					
					// eg. num:range:targetModelName/date:range:targetModelName/watch:targetModelName1,targetModelName2
					var modelName = ruleArr['watch' == ruleArr[0] ? 1 : 2];
					var modelArr = modelName.split(/,/);
					var j = 0;
					for(; j < modelArr.length; j++){
						var targetModelName = modelArr[j];
						// already watched
						if(watchedLl.contains(targetModelName))
							continue;

						log.i('Add watch for valid check : ' + targetModelName);
						scope.$watch(targetModelName, function(){
							/*
							if you donot want to valid if it's not dirty, add function bellow:
							valid.filterWatchValid = function(ctrl){
								return ctrl.$dirty;
							};
							*/
							if((valid.filterWatchValid && valid.filterWatchValid(ctrl, attrs)) || 
								!valid.filterWatchValid){
								// valid again
								ctrl.$setViewValue(ctrl.$viewValue);
							}
						}, true);
						watchedLl.push(targetModelName);
						uiValidRefered[attrs.ngModel] = targetModelName + '|' + ruleArr[0];
					}// \for inner
				}// \for outer
			}

			// Watch this model change and check if last bind failed (ctrl.$invalid === true)
			// eg.
			/*
			<select ng-model="optionVal" ui-valid="r"
				ng-options="one.code as one.name for one in optionList">
				<option value="">--to be choosed--</option>
			</select>
			var MyCtrl = function($scope){
				$scope.optionList = [];
				
				$scope.changeOptionListAndVal = function(){
					$scope.optionList = [{code: 'A', name: 'A'}];
					$scope.optionVal = 'A';

					// tips div should be removed
				};
			};
			*/
			// donot use $formaters as always show raw value, even $modelValue valid failed
			// change on 2014-08-26.. $watch make performance bad

// scope.$watch(attrs.ngModel, function(){ // if(ctrl.$modelValue !== undefined && // (ctrl.$invalid || el.hasClass(conf.invalidClass))){ // validFn(ctrl.$modelValue); // } // });

			// Watch for changes to the directives options
			// if validation rules change, initialize again
			scope.$watch(getRules, function(newRules, oldRules){
				init();

				oldRules = oldRules || '';
				if(lastOldRules)
					oldRules += ' ' + lastOldRules;

				lastOldRules = oldRules;

				// not bind yet (validate failed or first initialization) include ngModelController initialize value : NaN
				if(ctrl.$modelValue === undefined || 
					ctrl.$modelValue === null || 
					ctrl.$modelValue !== ctrl.$modelValue){
					// bind failed
					// check tips has showed
					var needValid = false;

					if(el.hasClass(conf.invalidClass)){
						needValid = true;
					}

					if(!needValid){
						// NaN need not valid
						// null need valid (ctrl.$invalid || ctrl.$viewValue === null)
						var isValNaN = ctrl.$viewValue !== ctrl.$viewValue;
						if(ctrl.$invalid || 
							(ctrl.$viewValue !== undefined && !isValNaN)){
							needValid = true;
						}
					}

					if(needValid){
						ctrl.$setViewValue(ctrl.$viewValue);
					}
				}else{
					if(!ctrl.$dirty && attrs.dirtyCheck){
						log.i('Skip valid if need not check when undirty...');
					}else{
						validFn(ctrl.$modelValue, oldRules);
					}
				}
			}, true);
		}
	};
}]);

// layout tips : most of time you donot need this directive
// because it uses some class defined to add width to td/th
// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
md.directive('uiLayoutCol', ['ng.config', 'uiLog', function(conf, log){
	'use strict';
	return {
		restrict: 'A',
		link: function(scope, el, attrs, ctrl){
			if('TR' != el[0].nodeName){
				log.w('Init uiLayoutCol failed : not a TR element!');
				return;
			}

			log.i('Relayout...');

			var _tds = el.children('td');
			if(_tds.length == 2){
				_tds.filter(':first').addClass('l');
				_tds.filter(':last').addClass('r');
			}else if(_tds.length == 4){
				_tds.filter(':even').addClass('l2');
				_tds.filter(':odd').addClass('r2');
			}else if(_tds.length == 6){
				_tds.eq(0).addClass('l3');
				_tds.eq(1).addClass('r3');
				_tds.eq(2).addClass('l3');
				_tds.eq(3).addClass('r3');
				_tds.eq(4).addClass('l3');
				_tds.eq(5).addClass('r3last');
			}

			// siblings tr set td text-align to right if exists label
			_tds = el.siblings('tr').children('td');
			_tds.filter(function(){
				return $(this).findAll('label').length > 0 && !$(this).hasClass('al');
			}).addClass('ar');
			_tds.filter(function(){
				return $(this).findAll('label').length == 0;
			}).addClass('al p_left5');
		}
	};
}]);

// pagination view
md.directive('uiPagi', ['ng.config', 'uiPager', function(conf, pager){
	return {
		restrict: 'A',

// templateUrl: conf.context + 'tpl/pagi.html', templateUrl: conf.context + 'tpl/pagisel.html', replace: false,

		scope: {
			pager: '=', 

			onChangePage: '&'
		},

		link: function(scope, el, attrs){
			var opts = scope.$eval(attrs.uiPagi) || {};
			scope.$watch('pager', function(it){
				scope.pagi = pager.gen(it, opts);
			}, true);
		}
	};
}]);

// change new style base on bootstrap, use this instead uiPagi
// you can also merge two in one, using template cache, compile different template
md.directive('uiPagiNew', ['ng.config', 'uiPager', function(conf, pager){
	return {
		restrict: 'A',

		templateUrl: conf.context + 'tpl/pagisel_new.html',
		replace: false,

		scope: {
			pager: '=', 

			onChangePage: '&'
		},

		link: function(scope, el, attrs){
			var opts = scope.$eval(attrs.uiPagi) || {};
			scope.$watch('pager', function(it){
				scope.pagi = pager.gen(it, opts);
			}, true);
		}
	};
}]);

// change new style base on bootstrap, use this instead uiPagi
// you can also merge two in one, using template cache, compile different template
md.directive('uiPagiNews', ['ng.config', 'uiPager', function(conf, pager){
	return {
		restrict: 'A',

		templateUrl: conf.context + 'tpl/pagiNew.html',
		replace: false,

		scope: {
			pager: '=', 

			onChangePage: '&'
		},

		link: function(scope, el, attrs){
			var opts = scope.$eval(attrs.uiPagi) || {};
			scope.$watch('pager', function(it){
				scope.pagi = pager.gen(it, opts);
			}, true);
		}
	};
}]);

// sort
md.directive('uiSort', ['ng.config', 'uiLog', '$parse', function(conf, log, $parse){
	'use strict';
	return {
		restrict: 'A',
		link: function(scope, el, attrs, ctrl){
			var nodeName = el[0].nodeName;
			if('TD' != nodeName && 'TH' != nodeName){
				log.w('Sort bind failed : not a TD/TH element!');
				return;
			}

			var opts = scope.$eval(attrs.uiSort) || {};
			log.i('Init sort : ');
			log.i(opts);

			if(!opts.targetModel){
				log.w('Init sort fail : targetModel required!');
				return;
			}
			el.addClass('ng-ui-sort-all');

			var sortModel = function(isUp){
				var targetModel = opts.targetModel;
				var getter = $parse(targetModel);
				var model = getter(scope);
				if(!model || !ag.isArray(model)){
					log.w('Event trigger sort fail : targetModel required and must be a list!');
					return;
				}

				var fnCompareCallback;
				if(opts.fnCompare){
					var getterCompare = $parse(opts.fnCompare);
					fnCompareCallback = getterCompare(scope);
				}

				var sortedModel;
				// use string localeCompare
				if(opts.sortLocale){
					var fnSortLocale = function(a, b){
						if(!opts.field)
							return 0;
						if(!a)
							return -1;
						if(!b)
							return 1;

						var val1 = a[opts.field];
						var val2 = b[opts.field];

						if(!ag.isString(val1))
							val1 = '' + val1;
						if(!ag.isString(val2))
							val2 = '' + val2;

						return val1.localeCompare(val2);
					};
					model.sort(function(a, b){
						return isUp ? fnSortLocale(a, b) : fnSortLocale(b, a);
					});
					sortedModel = model;
				}else{
					sortedModel = _.sortBy(model, function(it, index){
						if(fnCompareCallback){
							return fnCompareCallback(it, index, isUp);
						}else{
							if(!opts.field){
								return 0;
							}else{
								var val = it[opts.field];
								if(!val){
									return 0;
								}else if(ag.isDate(val)){
									return isUp ? val.getTime() : (0 - val.getTime());
								}else if(ag.isNumber(val)){
									return isUp ? val : (0 - val);
								}else if(ag.isString(val)){
									try{
										var intVal = parseFloat(val);
										return isUp ? intVal : (0 - intVal);
									}catch(e){
										log.e(e);
										return 0;
									}
								}else{
									return 0;
								}
							}
						}
					});
				}
				scope.$apply(function(){
					var setter = getter.assign;
					setter(scope, sortedModel);
				});
			};

			var resetSortedClass = function(element, suf1, suf2, addedSuf3){
				var pre = 'ng-ui-sort-';
				element.removeClass(pre + suf1).removeClass(pre + suf2).addClass(pre + addedSuf3);
			};

			var eventTriggerType = opts.eventTriggerType || 'click';
			el.unbind(eventTriggerType).bind(eventTriggerType, function(e){
				e.preventDefault();

				var isUp = !$(this).hasClass('ng-ui-sort-down');
				sortModel(isUp);

				resetSortedClass(el, 'all', isUp ? 'up' : 'down', isUp ? 'down' : 'up');

				// reset others' style
				var others = el.siblings('td,th').filter('.ng-ui-sort-down,.ng-ui-sort-up');
				resetSortedClass(others, 'up', 'down', 'all');

				return false;
			});
		}
	};
}]);

// PortalTab integration, open a new iframe tab when alinks/button click triggered
md.directive('uiTab', ['ng.config', 'uiLog', 'uiPortalUtils', function(conf, log, PortalUtils){
	'use strict';
	return {
		restrict: 'A',
		link: function(scope, el, attrs, ctrl){
			if('A' != el[0].nodeName){
				log.w('Rebind open tab failed : not a A element!');
				return;
			}

			// need tabId
			var opts = scope.$eval(attrs.uiTab) || {};

			log.i('Rebind open tab : ');
			log.i(opts);

			el.click(function(e){
				e.preventDefault();
				e.stopPropagation();

				var tabId = el.attr('tab-id') || opts.tabId;
				if(!tabId){
					log.w('Skip open tab as no tabId given!');
					return false;
				}

				var url = el.attr('href');
				var title = el.attr('title');
				PortalUtils.openTab(tabId, url, title, opts);

				return false;
			});
		}
	};
}]);

// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
// delegate dom event binding
md.directive('uiDelegateBind', ['$parse', 'uiLog', function($parse, log){
	'use strict';

	// link
	return {
		restrict: 'A',
		link: function(scope, el, attrs){
			var opts = scope.$eval(attrs.uiDelegateBind) || {};
			log.i('Init delegate bind : ');
			log.i(opts);

			if(!opts.tag || !opts.fn){
				log.w('Init delegate bind fail : tag/fn required!');
				return;
			}

			el.delegate(opts.tag, opts.eventType || 'click', function(){
				var getter = $parse(opts.fn);
				var fnTarget = getter(scope);
				if(fnTarget){
					// pass attribute info as parameter
					var attrMap = {};
					var i = 0;
					for(; i < this.attributes.length; i++){
						var attr = this.attributes[i];
						attrMap[attr.name] = attr.value;
					};

					scope.$apply(function(){
						fnTarget(attrMap);
					});
				}
			});
		}// \link
	};
}]); // end directive

// *** *** *** *** *** *** *** *** *** ***
// *** *** *** *** *** *** *** *** *** ***
// donot use select directive... data-raw="true" hack angular
// not the angular way but performance better
md.directive('uiOptions', ['$parse', 'uiLog', function($parse, log){
	'use strict';
	var UI_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+([^\.]+)\s+in\s+(\S+)$/;
	// link
	return {
		restrict: 'A',
		require: 'ngModel',
		priority: 8000, 
		link: function(scope, el, attrs, ctrl){
			var optionsExp = attrs.uiOptions;
			log.i('Init options : ' + optionsExp);
			var match = optionsExp.match(UI_OPTIONS_REGEXP);
			if(!match){
				log.w('Not options regexp match!');
				return;
			}

			var isMulti = attrs.multiple;
			var valueKey = match[1];
			var displayKey = match[2];
			var tmpVal = match[3];
			var listVal = match[4];

			if(!displayKey && valueKey){
				displayKey = valueKey;
				valueKey = null;
			}

			if(!displayKey && !valueKey){
				log.w('No display / value key given!');
				return;
			}

			var pre = tmpVal + '.';
			if(displayKey && displayKey.startsWith(pre))
				displayKey = displayKey.substring(pre.length);
			else
				displayKey = null;

			if(valueKey && valueKey.startsWith(pre))
				valueKey = valueKey.substring(pre.length);
			else
				valueKey = null;

			var rawHtml = el.html().trim();
			// comment this just make sure user will not set $modelValue -> null / not in options list
			if(!rawHtml)
				rawHtml = '<option value="" selected></option>';
			scope.$watch(listVal, function(list){
				if(!list){
					el.empty();
					el.html(rawHtml);
					return;
				}else{
					var pendHtml = '';

					// reuse option node
					var sel = el[0];
					var optionList = sel.getElementsByTagName('option');
					var i = 0, len = list.length;
					for(; i < len; i++){
						var it = list[i];
						var title = displayKey ? it[displayKey] : it;
						var value = i;

						if(attrs.linkValue && valueKey)
							title = it[valueKey] + attrs.linkValue + title;

						if(optionList.length > i){
							var one = optionList[i];
							one.value = value;
							one.text = title;
							one.title = title;
						}else{
							pendHtml += '<option title="' + title + '" value="' + value + '">' + title + '</option>';
						}
					}

					if(optionList.length > len){
						while(optionList.length > len)
							sel.remove(len);
					}

					el.prepend(rawHtml);
					el.append(pendHtml);

					if(ctrl.$modelValue){
						el.val(val2view(ctrl.$modelValue));
					}else{
						// el.val('') will not make first option selected
						el.findAll('option[value=""]').eq(0).attr('selected', true);
					}
				}
				resetTitle();
			}, true);

			var resetTitle = function(){
				var option = el.find('option:selected:first');
				el.attr('title', option.length ? option.text() : '');
			};

			ctrl.$render = function(){
				if(el.data('norender'))
					return;

				var targetVal = val2view(ctrl.$modelValue);
				if(targetVal == -1){
					el.findAll('option[value=""]').eq(0).attr('selected', true);
				}else{
					el.val(targetVal);
				}
				resetTitle();
			};

			// model to view (select index)
			var getIndexByKey = function(list, val, key){
				var indexLl = getIndexListByKey(list, [val], key);
				return indexLl.length ? indexLl[0] : -1;
			};

			var getIndexListByKey = function(list, valList, key){
				var indexLl = [];
				if(!list || !list.length || !valList || !valList.length)
					return indexLl;

				var i = 0;
				for(; i < list.length; i++){
					var one = list[i];
					if((key && valList.contains(one[key])) || (!key && valList.contains(one)))
						indexLl.push(i);
				}

				return indexLl;
			};

			// view to model
			var getValueByIndex = function(list, index, key){
				var valueLl = getValueListByIndex(list, [index], key);
				return valueLl.length ? valueLl[0] : null;
			};

			var getValueListByIndex = function(list, indexList, key){
				var valList = [];
				if(!list || !list.length || !indexList || !indexList.length)
					return valList;

				var i = 0;
				for(; i < indexList.length; i++){
					var index = indexList[i];
					// '0' is string -> true
					if(!index)
						continue;

					var one = list[index];
					if(one){
						valList.push(key ? one[key] : one);
					}
				}

				return valList;
			};

			var val2view = function(modelVal){
				var targetVal;
				var listModelVal = $parse(listVal)(scope);
				if(isMulti){
					targetVal = getIndexListByKey(listModelVal, modelVal, valueKey);
				}else{
					targetVal = getIndexByKey(listModelVal, modelVal, valueKey);
				}
				return targetVal;
			};

			var val2model = function(selectedVal){
				var modelVal;
				var listModelVal = $parse(listVal)(scope);
				if(isMulti){
					modelVal = getValueListByIndex(listModelVal, selectedVal, valueKey);
				}else{
					modelVal = getValueByIndex(listModelVal, selectedVal, valueKey);
				}
				return modelVal;
			};

			el.change(function(e){
				var selectedVal = el.val();
				var modelVal = val2model(selectedVal);
				el.data('norender', 'true');
				scope.$apply(function(){
					ctrl.$setViewValue(modelVal);
				});
				el.removeData('norender');

				resetTitle();
			});
		}// \link
	};
}]); // end directive

})(angular);

// cmd adaptor for pawa old version define('ng/ng.config', {init: function(){}}); define('ng/ng.service', {init: function(){}}); define('ng/ng.ui', {init: function(){}});

转载于:https://my.oschina.net/u/2001205/blog/1812645

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值