# Source: M_.js

    "use strict";

    moment().localeData("fr");
    moment().utcOffset(0);
    // console.log("moment().utcOffset()",moment().utcOffset());

    Array.prototype.m_remove = function(val) {
    	var pos = this.indexOf(val);
    	if (~pos) this.splice(pos, 1);
    };

    /**
     * M_ is a javascript application framework to build single page web applications, rather graphic than theory.
     * @namespace M_
     * @version 1.0
     * @author David Miglior <david@miglior.fr>
     * @todo Renomer les balises HTML utilisés en "data-" (par convention)
     * @todo Si le dernier argument de M_.App.open est false alors ne pas appeler d'action (permet de faire un bouton cancel)
     * @todo  Donner un nom : Bootstrap commander
     *
     */
    M_ = {
    	_registeredModules: {}
    };

    M_.registerModule = (name, module) => {
    	// console.log("name,module",name,module);
    	M_._registeredModules[name] = module;
    };

    /**
     * The Morphine application, instancied
     * @class
     * @property {boolean} inDevelopment - display more errors or helps
     * @property {boolean} useWebsocket - use websocket for M_.Utils.[get,post,put,delete]Json functions
     * @property {string} name - application name
     * @property {type} routes - Todo
     * @property {string} defaultController - the first controller to call
     * @property {array} tabController -
     * @property {type} tabOutlets
     * @property {type} container
     * @property {type} currentController
     * @property {type} namespace
     * @property {type} templates
     * @property {type} models
     * @property {type} scripts
     * @property {type} app
     * @property {type} beforeAllAjax
     * @property {type} afterAllAjax
     * @property {type} lastModule
     * @property {type} lastAction
     * @property {type} executeNextAction
     * @property {type} controllersDir
     */
    M_.App = new class {
    	/**
    	 * Already instancied
    	 * @return {type} [description]
    	 */
    	constructor() {}
    	/**
    	 * To document
    	 * @param {type} opts
    	 */
    	create(opts) {
    		this.defaults = {
    			inDevelopment: true,
    			useWebsocket: false,
    			name: "",
    			routes: {},
    			defaultController: "Home",
    			tabController: [],
    			tabOutlets: {},
    			container: null,
    			currentController: null,
    			namespace: "",
    			templates: null,
    			models: {},
    			scripts: null,
    			app: null,
    			_tabScriptsLoaded: [],
    			_tabModelsLoaded: [],
    			beforeAllAjax: null,
    			afterAllAjax: null,
    			_cmptAjax: 0,
    			_lastHashBis: "-------",
    			_urlArgs: [],
    			lastModule: "-------",
    			lastAction: "-------",
    			// _hashIsLoading: false,
    			executeNextAction: true,
    			_ready: null,
    			_beforeReady: null,
    			controllersDir: "app/controllers/",
    			moduleChange: null,
    			beforeModuleChange: null
    		};
    		$.extend(this, this.defaults, opts);

    		$(window).resize(() => {
    			if (this.M_tsResize) window.clearTimeout(this.M_tsResize);
    			this.M_tsResize = window.setTimeout(() => {
    				this.resize();
    			}, 300);
    		});

    		if ("onhashchange" in window) {
    			$(window).on("hashchange", () => {
    				this._hashChanged();
    			});
    			// } else {
    			// 	this.lastHash = window.location.hash;
    			// 	window.setInterval(()=> {
    			// 		if (window.location.hash != this.lastHash) {
    			// 			this.lastHash = window.location.hash;
    			// 			this._hashChanged();
    			// 		}
    			// 	}, 100);
    		}

    		$.ajaxPrefilter((options, originalOptions, jqXHR) => {
    			//log("c un ajax 1 ",arguments)
    			this.onBeforeAllAjax(options, originalOptions, jqXHR);
    			jqXHR.always(data => {
    				//log("c un ajax 2 ",arguments)
    				this.onAfterAllAjax(data, options, originalOptions, jqXHR);
    			});
    		});

    		return this;
    	}
    	/**
    	 * @memberOf! M_.App
    	 * @instance
    	 * @param {Object} options
    	 * @param {Object} originalOptions
    	 * @param {jqXHR} jqXHR
    	 */
    	onBeforeAllAjax(options, originalOptions, jqXHR) {}
    	/**
    	 * To document
    	 * @memberOf! M_.App
    	 * @param {Object} data
    	 * @param {Object} options
    	 * @param {Object} originalOptions
    	 * @param {jqXHR} jqXHR
    	 */
    	onAfterAllAjax(data, options, originalOptions, jqXHR) {}
    	/**
    	 * To document
    	 * @return {M_.App}
    	 */
    	resize() {
    		var tabController = this.tabController;
    		for (var i = 0; i < tabController.length; i++) {
    			tabController[i].onResize();
    		}
    		return this;
    	}
    	/**
    	 * @param  {Function}
    	 * @return {M_.App}
    	 */
    	beforeReady(fn) {
    		this._beforeReady = fn;
    		return this;
    	}
    	/**
    	 * @param  {Function}
    	 * @return {M_.App}
    	 */
    	ready(fn) {
    		// log("ready")
    		this._ready = fn;
    		M_.Help.createMHelp();
    		if (this._beforeReady) {
    			this._beforeReady(() => {
    				this._reallyReady();
    			});
    		} else this._reallyReady();
    		return this;
    	}
    	_reallyReady() {
    		if (document.readyState == "complete") {
    			this._ready();
    			this._hashChanged();
    		} else {
    			$(document).ready(() => {
    				this._ready();
    				this._hashChanged();
    			});
    		}
    	}
    	/**
    	 * Open a page
    	 * @param  {String} module
    	 * @param  {String} action
    	 * @return {M_.App}
    	 */
    	open(module, action) {
    		// log("open", this.currentController)
    		this._lastHashBis = window.location.hash;
    		var url = "";
    		var args = [];
    		this.executeNextAction = true;
    		if (module.substring(0, 1) == "/") {
    			url = "#" + module;
    		} else {
    			for (var i = 0; i < arguments.length; i++) {
    				if (arguments[i] !== false) args.push(arguments[i]);
    				else this.executeNextAction = false;
    			}
    			url = "#/" + args.join("/");
    		}
    		//log("module,url,args",module, ";", url,";",args)
    		if (url == window.location.hash) {
    			this._hashChanged();
    		} else {
    			window.open(url, "_self");
    		}
    		return this;
    	}
    	_getHashArgs() {
    		var hash = window.location.hash.substring(2, window.location.hash.length);
    		var t = hash.split("/");
    		return t;
    	}
    	_hashChanged() {
    		var args = this._getHashArgs();
    		var module = this.defaultController;
    		if (args[0] && args[0] !== "") module = args[0];
    		var justCreated = false;
    		if (this[module] === undefined) {
    			// var Mod = System.get('js6/controllers/'+module+".js")[module] ;
    			// this[module] = new Mod({controllerName:module, tpl:JST[module]}) ;
    			// console.log("JST",JST,M_._registeredModules);
    			// import {mymod} from './'+module+'.js' ;
    			this[module] = new M_._registeredModules[module]({
    				controllerName: module,
    				tpl: JST[module]
    			});
    			this.tabController.push(this[module]);
    			justCreated = true;
    		}
    		this[module].resolve(ok => {
    			if (ok) {
    				if (this.beforeModuleChange) {
    					let ok2 = this.beforeModuleChange(module, this.lastModule);
    					if (ok2 !== false) this.__hashChanged(justCreated);
    				} else this.__hashChanged(justCreated);
    			}
    		});
    	}
    	__hashChanged(justCreated) {
    		var tabController = this.tabController;
    		for (var i = 0; i < tabController.length; i++) {
    			tabController[i].onExit();
    		}

    		// if (this._hashIsLoading) return ;
    		// log("_hashChanged9")
    		var args = this._getHashArgs();
    		var module = this.defaultController;
    		if (args[0] && args[0] !== "") module = args[0];
    		var action = "";
    		if (args[1] && args[1] !== "") action = args[1];
    		// log("module2",module)

    		// log("currentController", this.currentController)
    		if (this.currentController && this.lastModule !== module) {
    			var currentControllerNameSaved = this.currentController.controllerName;
    			$("#part_" + this.currentController.controllerName)
    				.css("left", 0)
    				.css("z-index", 1)
    				.css("transform-origin", "5% 50%")
    				.transition(
    					{
    						// x: $(window).width(),
    						perspective: "1000px",
    						rotateY: "50deg",
    						scale: 0.7,
    						opacity: 0.3
    					},
    					500,
    					el => {
    						$("#part_" + currentControllerNameSaved).hide();
    						// this._hashChangedMore() ;
    					}
    				);
    		} else {
    			// this._hashChangedMore() ;
    		}

    		if (!justCreated) {
    			if (this.lastModule == module) {
    				this[module]._onShow(false);
    			} else {
    				// var tabController = this.tabController ;
    				for (i = 0; i < tabController.length; i++) {
    					if (this.currentController.controllerName == tabController[i].controllerName) continue;
    					// tabController[i].hide() ;
    					$("#part_" + tabController[i].controllerName).css("z-index", 0);
    				}

    				this[module]._onShow(true);
    			}
    		} else {
    			// import {mod} from 'controllers/'+module ;
    			// log("module",module, System.get('js6/controllers/'+module+".js"))
    			this[module].init.call(this[module]);
    			this[module].renderTemplate.call(this[module]);
    			this[module].create.call(this[module]);
    			this[module]._onShow.call(this[module], true);
    		}

    		if (action === "") action = this[module].defaultAction;
    		if (!this[module][action + "Action"]) {
    			log("You must define function '" + action + "Action" + "()' in " + this[module].controllerName + " controller");
    			this.open(module, "index");
    		} else {
    			args.splice(0, 2);
    			this[module][action + "Action"].apply(this[module], args);
    		}

    		if (this.moduleChange) this.moduleChange(module, this.lastModule);

    		this.lastModule = module;
    		this.lastAction = action;
    	}
    	/**
    	 * @param  {jQuery}		container
    	 * @param  {JSTemplate}	tpl
    	 * @param  {Object}		tplData
    	 * @return {jQuery}
    	 */
    	renderMustacheTo(container, tpl, tplData = {}) {
    		//, tplPartials={}
    		var jEl = $(this.renderMustache(tpl, tplData)); //, tplPartials
    		jEl.appendTo(container);
    		return jEl;
    	}
    	/**
    	 * @param  {JSTemplate}	tpl
    	 * @param  {Object}		tplData
    	 * @return {String}
    	 */
    	renderMustache(tpl, tplData = {}) {
    		//, tplPartials={}
    		// console.log("tplPartials",tplPartials)
    		// console.log('_.templateSettings.imports',_.templateSettings.imports)
    		// console.log("tpl",tpl);
    		var template = tpl(tplData);
    		return template;
    	}
    }();

    /**
    A generic controller to extends
     * @class
     * @example
    export class Calculator extends M_.Controller {
    	init() {
    	}
    	create() {
    	}
    }
    */
    M_.Controller = class {
    	/**
    	 * Generally, you don't need to extend this class. Extend create method.
    	 * @param  {object} opts Configuration object
    	 */
    	constructor(opts) {
    		var defaults = {
    			controller: "",
    			controllerName: "",
    			controllerSrc: null,
    			defaultAction: "index",
    			jEl: null,
    			tpl: null,
    			tplData: {},
    			tplPartials: {},
    			models: [],
    			templates: [],
    			scripts: []
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);
    	}
    	/**
    	 * To document
    	 */
    	init() {}
    	/**
    	 * @param  {Function}
    	 */
    	resolve(next) {
    		next(true);
    	}
    	/**
    	 * To document
    	 */
    	renderTemplate() {
    		if (this.tpl) {
    			this.jEl = M_.App.renderMustacheTo(M_.App.container, this.tpl, this.tplData, this.tplPartials);
    			this.jEl.attr("id", "part_" + this.controllerName);
    			this.jEl.addClass("M_part");
    		}
    	}
    	/**
    	 * To document
    	 */
    	render() {}
    	/**
    	 * To document
    	 */
    	indexAction() {}
    	/**
    	 * To document
    	 */
    	onResize() {}
    	/**
    	 * To document
    	 */
    	onExit() {}
    	/**
    	 * To document
    	 */
    	onControllerChange() {}
    	/**
    	 * Extend this method to initialize your UI
    	 */
    	doLayout() {}
    	/**
    	 * This method is called each time the controller is shown.
    	 * @param  {boolean} firstTime True when this function is called for the first time.
    	 */
    	show(firstTime) {}

    	_onShow(anim) {
    		// console.log("_onShow");
    		// var args = this._urlArgs.slice(0) ;
    		// args.unshift(firstTime) ;
    		// this.show.apply(this, args) ;
    		M_.App.currentController = this;
    		this.onControllerChange();

    		this.show.apply(this);
    		// console.log("_onShow");
    		this.jEl
    			// .css('scale', 1)
    			// .css('rotateX',0)
    			// .css('perspective','0px')
    			// .css('rotateY','0deg')
    			.css("transform", "rotateY(0deg)")
    			.css("transform", "perspective(0)")
    			.css("opacity", 1)
    			.css("z-index", 2)
    			.show();

    		if (anim) {
    			// console.log("anim");
    			this.jEl.css("left", $(window).width()).transition({
    				left: 0,
    				delay: 100
    			}, 500, () => {
    				// console.log("finit");
    			});
    		} else {
    			this.jEl.css("left", 0);
    		}
    	}
    	/**
    	 * onShow description
    	 * @param  {Boolean} firstTime [description]
    	 */
    	onShow(firstTime) {}
    	/**
    	 * hide description
    	 */
    	hide() {
    		if (this.jEl) this.jEl.hide();
    	}
    };

    /**
     * Usefull methods for manipulate date, string, paths, etc...
      */
    M_.Utils = class {
    	static preloadImages(preload, cb) {
    		// var preload = ["a.gif", "b.gif", "c.gif"];
    		var promises = [];
    		for (var i = 0; i < preload.length; i++) {
    			(function(url, promise) {
    				var img = new Image();
    				img.onload = function() {
    					promise.resolve();
    				};
    				img.src = url;
    			})(preload[i], (promises[i] = $.Deferred()));
    		}
    		$.when.apply($, promises).done(function() {
    			// alert("All images ready sir!");
    			cb();
    		});
    	}

    	static isEventSupported(eventName) {
    		var TAGNAMES = {
    			select: "input",
    			change: "input",
    			search: "input",
    			submit: "form",
    			reset: "form",
    			error: "img",
    			load: "img",
    			abort: "img"
    		};
    		var el = document.createElement(TAGNAMES[eventName] || "div");
    		eventName = "on" + eventName;
    		var isSupported = eventName in el;
    		if (!isSupported) {
    			el.setAttribute(eventName, "return;");
    			isSupported = typeof el[eventName] == "function";
    		}
    		el = null;
    		return isSupported;
    	}

    	static humanFileSizeString(fileSizeInBytes) {
    		var i = -1;
    		var byteUnits = [" kB", " MB", " GB", " TB", "PB", "EB", "ZB", "YB"];
    		do {
    			fileSizeInBytes = fileSizeInBytes / 1024;
    			i++;
    		} while (fileSizeInBytes > 1024);

    		return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i];
    	}

    	/**
    	 * Idem as jQuery $.isArray(obj)
    	 * @param  {object}  obj The object to test
    	 * @return {boolean}     True if obj is an array
    	 */
    	static isArray(obj) {
    		return $.isArray(obj);
    	}

    	static get_html_translation_table(table, quote_style) {
    		//  discuss at: http://phpjs.org/functions/get_html_translation_table/
    		// original by: Philip Peterson
    		//  revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    		// bugfixed by: noname
    		// bugfixed by: Alex
    		// bugfixed by: Marco
    		// bugfixed by: madipta
    		// bugfixed by: Brett Zamir (http://brett-zamir.me)
    		// bugfixed by: T.Wild
    		// improved by: KELAN
    		// improved by: Brett Zamir (http://brett-zamir.me)
    		//    input by: Frank Forte
    		//    input by: Ratheous
    		//        note: It has been decided that we're not going to add global
    		//        note: dependencies to php.js, meaning the constants are not
    		//        note: real constants, but strings instead. Integers are also supported if someone
    		//        note: chooses to create the constants themselves.
    		//   example 1: get_html_translation_table('HTML_SPECIALCHARS');
    		//   returns 1: {'"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;'}

    		var entities = {},
    			hash_map = {},
    			decimal;
    		var constMappingTable = {},
    			constMappingQuoteStyle = {};
    		var useTable = {},
    			useQuoteStyle = {};

    		// Translate arguments
    		constMappingTable[0] = "HTML_SPECIALCHARS";
    		constMappingTable[1] = "HTML_ENTITIES";
    		constMappingQuoteStyle[0] = "ENT_NOQUOTES";
    		constMappingQuoteStyle[2] = "ENT_COMPAT";
    		constMappingQuoteStyle[3] = "ENT_QUOTES";

    		useTable = !isNaN(table) ? constMappingTable[table] : table ? table.toUpperCase() : "HTML_SPECIALCHARS";
    		useQuoteStyle = !isNaN(quote_style) ? constMappingQuoteStyle[quote_style] : quote_style ? quote_style.toUpperCase() : "ENT_COMPAT";

    		if (useTable !== "HTML_SPECIALCHARS" && useTable !== "HTML_ENTITIES") {
    			throw new Error("Table: " + useTable + " not supported");
    			// return false;
    		}

    		entities["38"] = "&amp;";
    		if (useTable === "HTML_ENTITIES") {
    			entities["160"] = "&nbsp;";
    			entities["161"] = "&iexcl;";
    			entities["162"] = "&cent;";
    			entities["163"] = "&pound;";
    			entities["164"] = "&curren;";
    			entities["165"] = "&yen;";
    			entities["166"] = "&brvbar;";
    			entities["167"] = "&sect;";
    			entities["168"] = "&uml;";
    			entities["169"] = "&copy;";
    			entities["170"] = "&ordf;";
    			entities["171"] = "&laquo;";
    			entities["172"] = "&not;";
    			entities["173"] = "&shy;";
    			entities["174"] = "&reg;";
    			entities["175"] = "&macr;";
    			entities["176"] = "&deg;";
    			entities["177"] = "&plusmn;";
    			entities["178"] = "&sup2;";
    			entities["179"] = "&sup3;";
    			entities["180"] = "&acute;";
    			entities["181"] = "&micro;";
    			entities["182"] = "&para;";
    			entities["183"] = "&middot;";
    			entities["184"] = "&cedil;";
    			entities["185"] = "&sup1;";
    			entities["186"] = "&ordm;";
    			entities["187"] = "&raquo;";
    			entities["188"] = "&frac14;";
    			entities["189"] = "&frac12;";
    			entities["190"] = "&frac34;";
    			entities["191"] = "&iquest;";
    			entities["192"] = "&Agrave;";
    			entities["193"] = "&Aacute;";
    			entities["194"] = "&Acirc;";
    			entities["195"] = "&Atilde;";
    			entities["196"] = "&Auml;";
    			entities["197"] = "&Aring;";
    			entities["198"] = "&AElig;";
    			entities["199"] = "&Ccedil;";
    			entities["200"] = "&Egrave;";
    			entities["201"] = "&Eacute;";
    			entities["202"] = "&Ecirc;";
    			entities["203"] = "&Euml;";
    			entities["204"] = "&Igrave;";
    			entities["205"] = "&Iacute;";
    			entities["206"] = "&Icirc;";
    			entities["207"] = "&Iuml;";
    			entities["208"] = "&ETH;";
    			entities["209"] = "&Ntilde;";
    			entities["210"] = "&Ograve;";
    			entities["211"] = "&Oacute;";
    			entities["212"] = "&Ocirc;";
    			entities["213"] = "&Otilde;";
    			entities["214"] = "&Ouml;";
    			entities["215"] = "&times;";
    			entities["216"] = "&Oslash;";
    			entities["217"] = "&Ugrave;";
    			entities["218"] = "&Uacute;";
    			entities["219"] = "&Ucirc;";
    			entities["220"] = "&Uuml;";
    			entities["221"] = "&Yacute;";
    			entities["222"] = "&THORN;";
    			entities["223"] = "&szlig;";
    			entities["224"] = "&agrave;";
    			entities["225"] = "&aacute;";
    			entities["226"] = "&acirc;";
    			entities["227"] = "&atilde;";
    			entities["228"] = "&auml;";
    			entities["229"] = "&aring;";
    			entities["230"] = "&aelig;";
    			entities["231"] = "&ccedil;";
    			entities["232"] = "&egrave;";
    			entities["233"] = "&eacute;";
    			entities["234"] = "&ecirc;";
    			entities["235"] = "&euml;";
    			entities["236"] = "&igrave;";
    			entities["237"] = "&iacute;";
    			entities["238"] = "&icirc;";
    			entities["239"] = "&iuml;";
    			entities["240"] = "&eth;";
    			entities["241"] = "&ntilde;";
    			entities["242"] = "&ograve;";
    			entities["243"] = "&oacute;";
    			entities["244"] = "&ocirc;";
    			entities["245"] = "&otilde;";
    			entities["246"] = "&ouml;";
    			entities["247"] = "&divide;";
    			entities["248"] = "&oslash;";
    			entities["249"] = "&ugrave;";
    			entities["250"] = "&uacute;";
    			entities["251"] = "&ucirc;";
    			entities["252"] = "&uuml;";
    			entities["253"] = "&yacute;";
    			entities["254"] = "&thorn;";
    			entities["255"] = "&yuml;";
    		}

    		if (useQuoteStyle !== "ENT_NOQUOTES") {
    			entities["34"] = "&quot;";
    		}
    		if (useQuoteStyle === "ENT_QUOTES") {
    			entities["39"] = "&#39;";
    		}
    		entities["60"] = "&lt;";
    		entities["62"] = "&gt;";

    		// ascii decimals to real symbols
    		for (decimal in entities) {
    			if (entities.hasOwnProperty(decimal)) {
    				hash_map[String.fromCharCode(decimal)] = entities[decimal];
    			}
    		}

    		return hash_map;
    	}

    	static html_entity_decode(string, quote_style) {
    		//  discuss at: http://phpjs.org/functions/html_entity_decode/
    		// original by: john (http://www.jd-tech.net)
    		//    input by: ger
    		//    input by: Ratheous
    		//    input by: Nick Kolosov (http://sammy.ru)
    		// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    		// improved by: marc andreu
    		//  revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    		//  revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    		// bugfixed by: Onno Marsman
    		// bugfixed by: Brett Zamir (http://brett-zamir.me)
    		// bugfixed by: Fox
    		//  depends on: get_html_translation_table
    		//   example 1: html_entity_decode('Kevin &amp; van Zonneveld');
    		//   returns 1: 'Kevin & van Zonneveld'
    		//   example 2: html_entity_decode('&amp;lt;');
    		//   returns 2: '&lt;'

    		var hash_map = {},
    			symbol = "",
    			tmp_str = "",
    			entity = "";
    		tmp_str = string.toString();

    		if (false === (hash_map = this.get_html_translation_table("HTML_ENTITIES", quote_style))) {
    			return false;
    		}

    		// fix &amp; problem
    		// http://phpjs.org/functions/get_html_translation_table:416#comment_97660
    		delete hash_map["&"];
    		hash_map["&"] = "&amp;";

    		for (symbol in hash_map) {
    			entity = hash_map[symbol];
    			tmp_str = tmp_str.split(entity).join(symbol);
    		}
    		tmp_str = tmp_str.split("&#039;").join("'");

    		return tmp_str;
    	}

    	static getIframeDocument(jEl) {
    		var iframeDoc = jEl.get(0).contentWindow || jEl.get(0).contentDocument;
    		if (iframeDoc.document) {
    			iframeDoc = iframeDoc.document;
    		}
    		return iframeDoc;
    	}

    	// static generateAndDownloadFile(urlGenerate, urlDownload, args={}) {
    	// 	var defaults = 	{
    	// 		methodGenerate: 'POST',
    	// 		methodDownload: 'POST',
    	// 		argsGenerate: {},
    	// 		argsDownload: {},
    	// 	};
    	// 	var myargs = $.extend({}, defaults, args) ;
    	// 	this.ajaxJson(urlGenerate, myargs.argsGenerate, myargs.methodDownload, (data)=> {
    	//
    	// 	}
    	// 	this.downloadFile(urlDownload, myargs.argsDownload, myargs.methodDownload) ;
    	// }
    	static downloadFile(url, args = {}, method = "POST") {
    		$("#M_FileDownload").remove();
    		$(document.body).append("<iframe id='M_FileDownload' src='about:blank' style='display:none;'></iframe>");
    		var html = "";
    		_.each(args, (val, key) => {
    			html += `<input type="hidden" name="${key}" value="${val}">`;
    		});
    		var doc = this.getIframeDocument($("#M_FileDownload"));
    		// console.log("html", html);
    		doc.write("<html><head></head><body><form method='" + method + "' action='" + url + "'>" + html + "</form></body></html>");
    		var started = false;
    		$(doc)
    			.find("form")
    			.submit();
    	}
    	static downloadFile2(url, args = {}, method = "POST") {
    		$("#M_FileDownload").remove();
    		$(document.body).append("<iframe id='M_FileDownload' src='" + url + "' style='display:block; width:100%; height:200px;'></iframe>");
    		window.setTimeout(() => {
    			var doc = this.getIframeDocument($("#M_FileDownload"));
    			var toto = doc.toString();
    			// console.log("doc.window", doc);
    			// var txt = $(doc).html() ;
    			// console.log("txt", txt, JSON.parse(txt));
    		}, 2000);
    	}

    	static urlIze(str) {
    		str = M_.Utils.trim(str);
    		if (str.substring(0, 7) == "http://") return str;
    		return "http://" + str;
    	}

    	static limitText(str, n, useWordBoundary) {
    		var toLong = str.length > n,
    			s_ = toLong ? str.substr(0, n - 1) : this;
    		s_ = useWordBoundary && toLong ? s_.substr(0, s_.lastIndexOf(" ")) : s_;
    		return toLong ? s_ + "&hellip;" : s_;
    	}
    	/**
    	 * toggleAppFullScreen description
    	 * @return {Boolean} true if actually passed in full screen (after function executed)
    	 */
    	static toggleAppFullScreen() {
    		var element = document.body;
    		$(document.body).css("width", "100%");
    		if (!M_.Utils._appIsFullScreen) {
    			var requestMethod =
    				element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen;
    			if (requestMethod) {
    				requestMethod.call(element);
    			} else if (typeof window.ActiveXObject !== "undefined") {
    				// Older IE.
    				var wscript = new window.ActiveXObject("WScript.Shell");
    				if (wscript !== null) {
    					wscript.SendKeys("{F11}");
    				}
    			}
    		} else {
    			if (document.exitFullscreen) {
    				document.exitFullscreen();
    			} else if (document.mozCancelFullScreen) {
    				document.mozCancelFullScreen();
    			} else if (document.webkitExitFullscreen) {
    				document.webkitExitFullscreen();
    			}
    		}
    		M_.Utils._appIsFullScreen = !M_.Utils._appIsFullScreen;
    	}

    	/**
    	 * Return moment object
    	 * @return {boolean}     True if obj is an array
    	 */
    	static moment() {
    		return moment;
    	}
    	/**
    	 * Idem as jQuery $.isFunction()
    	 * @param  {object}  obj The object to test
    	 * @return {boolean}     True if obj is an function
    	 */
    	static isFunction(obj) {
    		return $.isFunction(obj);
    	}
    	/**
    	 * Idem as jQuery $.isEmptyObject()
    	 * @param  {object}  obj The object to test
    	 * @return {boolean}     True if obj is {}
    	 */
    	static isEmptyObject(obj) {
    		return $.isEmptyObject(obj);
    	}

    	/**
    	 * Check if d is a moment date
    	 * @param  {moment}  obj The object to test
    	 * @return {boolean}     True if obj is {}
    	 */
    	static isDate(d) {
    		if (moment(d).isValid()) return true;
    		return false;
    	}

    	/**
    	 * Return the extension of a path.
    	 * @param  {String} path The path
    	 * @return {String}      The extension or ''
    	 * @example
    	 * var ext = M_.Utils.getExtensionFromPath("foo/bar/my_file.html") ;
    	 * // ext = "html"
    	 */
    	static getExtensionFromPath(path) {
    		return path.split(".").pop();
    	}

    	/**
    	 * Return the base name of a path.
    	 * @param  {String} path The path
    	 * @return {String}      The name of the file
    	 * @example
    	 * var filename = getBaseNameFromPath("foo/bar/myfile.html") ;
    	 * // filename = "myfile.html"
    	 */
    	static getBaseNameFromPath(path) {
    		return path.split("/").pop();
    	}

    	/**
    	 * Return the directory of a path.
    	 * @param  {String} path The path
    	 * @return {String}      The name of the file
    	 * @example
    	 * var filename = getDirFromPath("foo/bar/myfile.html") ;
    	 * // filename = "foo/bar"
    	 */
    	static getDirFromPath(path) {
    		var tabPaths = path.split("/");
    		tabPaths.pop();
    		return tabPaths.join("/");
    	}
    	/**
    	 * Convert hexadecimal to RGB
    	 * @param  {number} hex Number to convert
    	 * @return {object}     Object contain keys : r, g, b ; {r:x, g:y, b:z}
    	 */
    	static hexToRgb(hex) {
    		//Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
    		var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    		hex = hex.replace(shorthandRegex, function(m, r, g, b) {
    			return r + r + g + g + b + b;
    		});

    		var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    		return result
    			? {
    					r: parseInt(result[1], 16),
    					g: parseInt(result[2], 16),
    					b: parseInt(result[3], 16)
    				}
    			: null;
    	}
    	/**
    	 * Convert RGB to Hexa
    	 * @param  {number} r Red
    	 * @param  {integer} g Green
    	 * @param  {integer} b Blue
    	 * @return {number}   Number of hexadecimal
    	 */
    	static rgbToHex(r, g, b) {
    		return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    	}
    	/**
    	 * Convert RGB from css (like #FFFFFF) to Hexa
    	 * @param  {number} rgb
    	 * @return {number}   Object contain keys : r, g, b ; {r:x, g:y, b:z}
    	 */
    	static rgbFromCssToHex(rgb) {
    		if (rgb.indexOf("#") === 0) return rgb;
    		if (rgb.indexOf("rgb") === 0) {
    			rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/i);
    			return (
    				"#" +
    				("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
    				("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
    				("0" + parseInt(rgb[3], 10).toString(16)).slice(-2)
    			);
    		}
    	}
    	/**
    	 * Return a uniq ID
    	 * @return {String} A incremental number start from 1
    	 */
    	static id() {
    		if (!this.cmpUniqId) this.cmpUniqId = 0;
    		this.cmpUniqId++;
    		return "_M_el_" + this.cmpUniqId;
    	}

    	/**
    	 * Don't use that...
    	 */
    	static removeEventsNotAttachedToDocument() {
    		window.setTimeout(() => {
    			// log("go")
    			var tab = $._data(document, "events").click;
    			// log("tab",tab)
    			if (!tab) return;
    			var nb = 0;
    			var tabToRemove = [];
    			for (var i = 0; i < tab.length; i++) {
    				if (tab[i].selector && $(tab[i].selector).length === 0 && tab[i].selector.substring(0, 7) == "#_M_el_") {
    					nb++;
    					tabToRemove.push(tab[i].selector);
    				}
    			}
    			// log("tabToRemove",tabToRemove)
    			for (i = 0; i < tabToRemove.length; i++) {
    				$(document).off("click", tabToRemove[i]);
    			}
    			// log("nb", nb) ;
    		}, 500);
    	}
    	/**
    	 * Set the z-index upper
    	 * @param {jQuery} jEl jQuery object
    	 */
    	static setZIndexForModal(jEl) {
    		this._cmptZIndexModal += 10;
    		jEl.css("z-index", this._cmptZIndexModal);
    		$(".modal-backdrop")
    			.last()
    			.css("z-index", this._cmptZIndexModal - 1);
    	}
    	// static getMaxZIndex() {
    	// 	var heighest = -999 ;
    	// 	$("*").each(function() {
    	// 		var current = parseInt($(this).css("z-index"), 10);
    	// 		heighest = Math.max(current, heighest) ;
    	// 	});
    	// 	if (heighest==-999) heighest = 0 ;
    	// 	return heighest ;
    	// }

    	/**
    	 * Clone an object... simple level !!!!
    	 * @param  {object} obj
    	 * @return {object} the new object
    	 */
    	static clone(obj) {
    		var target = {};
    		for (var i in obj) {
    			if (obj.hasOwnProperty(i)) {
    				target[i] = obj[i];
    			}
    		}
    		return target;
    	}
    	/**
    	 * Create a UUID with a random algorithme
    	 * @return {String} The number is like 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    	 */
    	static createUUID() {
    		return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
    			var r = (Math.random() * 16) | 0,
    				v = c == "x" ? r : (r & 0x3) | 0x8;
    			return v.toString(16);
    		});
    	}

    	/**
    	 * Clone an object... simple level !!!!
    	 * @param  {object} nb
    	 * @param  {object} singular
    	 * @param  {object} plural
    	 * @return {object} singular
    	 * @example
    var plural1 = M_.Utils.plural(3, "guitare")
    // plural1 = guitares
    var plural2 = M_.Utils.plural(3, "guitare", "guitarez")
    // plural2 = guitarez
    var plural3 = M_.Utils.plural(3, "une guitare", "une des {nb} guitares")
    // plural3 = une des 3 guitares
    	 */
    	static plural(nb, singular, plural = null, withoutnumber = false) {
    		if (nb <= 1 && nb >= -1) {
    			if (singular.indexOf("{{nb}}") >= 0) {
    				return singular.replace(/\{\{nb\}\}/, nb);
    			}
    			if (withoutnumber) return singular;
    			return nb + " " + singular;
    		}
    		if (!plural) plural = singular + "s";
    		if (plural.indexOf("{{nb}}") >= 0) {
    			return plural.replace(/\{\{nb\}\}/, nb);
    		}
    		if (withoutnumber) return plural;
    		return nb + "&nbsp;" + plural;
    	}
    	/**
    	 * Create a delay
    	 * @param  {function} callback - une fonction à appeler
    	 * @param  {number} time - un temp en millisecondes
    	 * @param  {String} [arbitralName="arbitralName"] - donner de préférence un nom unique
    	 */
    	static delay(callback, time, arbitralName = "arbitralName") {
    		if (!this.tabDelay) this.tabDelay = {};
    		if (arbitralName === "") arbitralName = "arbitralName";
    		if (this.tabDelay[arbitralName]) window.clearTimeout(this.tabDelay[arbitralName]);
    		this.tabDelay[arbitralName] = window.setTimeout(() => {
    			//log("callback")
    			callback();
    		}, time);
    	}
    	/**
    	 * Check if variable is empty : [], undef, null, false, 0, "", "0", "undefined", undefined
    	 * @param  {object} obj
    	 * @return {boolean} true if empty
    	 */
    	static isEmpty(obj) {
    		var undef, key, i, len;
    		var emptyValues = [undef, null, false, 0, "", "0", "undefined", undefined];

    		if ($.type(obj) === "date") return false;

    		for (i = 0, len = emptyValues.length; i < len; i++) {
    			if (obj === emptyValues[i] || obj == emptyValues[i]) {
    				return true;
    			}
    		}
    		if (typeof obj === "object") {
    			for (key in obj) {
    				//TODO: should we check for own properties only?
    				//if (obj.hasOwnProperty(key)) {
    				return false;
    				//}
    			}
    			return true;
    		}
    		return false;
    	}
    	/**
    	 * Format a phone number... for french format !!! to update...
    	 * @param  {String} num
    	 * @return {String} the formated number
    	 */
    	static formatPhone(num) {
    		var ret = "";
    		var toAdd = "";
    		//$phone_number = preg_replace("/[^\+0-9]/", "", $phone_number);
    		num = num.replace(/[^\+0-9]/g, "");
    		if (num.substring(0, 1) == "+") {
    			toAdd = num.substr(0, 3) + " ";
    			num = num.substr(3);
    		}
    		for (let kk = 0; kk < num.length; kk++) {
    			if (kk % 2 === 0) ret = " " + ret;
    			ret = num.substr(num.length - kk - 1, 1) + ret;
    		}
    		return toAdd + ret.trim();
    	}
    	static alphaNum(str) {
    		if (!str) return "";
    		var reg1 = /[^\+0-9]/g;
    		str = str.replace(reg1, "");
    		return str;
    	}
    	/**
    	 * Strip html tags
    	 * @param  {String} num
    	 * @param  {String} allowed
    	 * @return {String} the new string
    	 */
    	static strip_tags(input, allowed) {
    		//From: http://phpjs.org/functions
    		// console.log("input",input);
    		input += "";
    		if (!input) return "";
    		allowed = (((allowed || "") + "").toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join("");
    		var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
    			commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
    		return input.replace(commentsAndPhpTags, "").replace(tags, function($0, $1) {
    			return allowed.indexOf("<" + $1.toLowerCase() + ">") > -1 ? $0 : "";
    		});
    	}
    	/**
    	 * First char to upper case
    	 * @param  {String} str
    	 * @return {String} the new string
    	 */
    	static ucfirst(str) {
    		//  discuss at: http://phpjs.org/functions/ucfirst/
    		// original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    		// bugfixed by: Onno Marsman
    		// improved by: Brett Zamir (http://brett-zamir.me)
    		//   example 1: ucfirst('kevin van zonneveld');
    		//   returns 1: 'Kevin van zonneveld'

    		str += "";
    		str = str.toLowerCase();
    		var f = str.charAt(0).toUpperCase();
    		return f + str.substr(1);
    	}

    	/**
    	 * First char to upper case
    	 * @param  {String} str - the base string
    	 * @param  {number} len - add 'padStr' X times
    	 * @param  {String} [padStr='0'] - the string to add
    	 * @param  {number} [direction=-1] - -1=left, 1=right
    	 * @return {String} the new string
    	 */
    	static str_pad(str, len, padStr = "0", direction = -1) {
    		str += "";
    		var res = "";
    		//warning if len<str.length
    		if (direction < 0)
    			str = str
    				.split("")
    				.reverse()
    				.join("");
    		for (var i = 0; i < len; i++) {
    			var c = str.charAt(i);
    			if (c === "") res += padStr;
    			res += str.charAt(i);
    		}
    		if (direction < 0)
    			res = res
    				.split("")
    				.reverse()
    				.join("");
    		return res;
    	}
    	/**
    	 * Remove break line, space and tabulation
    	 * @param  {String} str - the base string
    	 * @return {String} the new string
    	 */
    	static trim(str) {
    		//return str.replace(/^\s+|\s+$/g, '');
    		str = $.trim(str);
    		str = str.replace(/^(\n\r)+|(\n\r)+$/g, "");
    		return str;
    	}
    	/**
    	 * Set cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @param  {String} cvalue - the value of the cookie
    	 * @param  {String} [expireDays=7] - expiration in days
    	 */
    	static setCookie(cname, cvalue, expireDays = 7) {
    		var d = new Date();
    		d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
    		var expires = "expires=" + d.toGMTString();
    		document.cookie = cname + "=" + cvalue + "; " + expires;
    	}
    	/**
    	 * Get cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @return {String} the value of the cookie
    	 */
    	static getCookie(cname) {
    		var name = cname + "=";
    		var ca = document.cookie.split(";");
    		for (var i = 0; i < ca.length; i++) {
    			var c = ca[i].trim();
    			if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
    		}
    		return null;
    	}
    	/**
    	 * Check if browser is on macos
    	 * @return {boolean} true if ok
    	 */
    	static isMacos() {
    		var isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
    		return isMac;
    	}
    	static valInArray(tab, key, keyName = "key", valName = "val") {
    		var f = this.findInArray(tab, key, keyName);
    		if (f) return _.result(f, valName);
    		else return "";
    	}
    	static findInArray(tab, key, keyName = "key") {
    		var obj = {};
    		obj[keyName] = key;
    		var f = _.find(tab, obj);
    		// console.log("f", f);
    		return f;
    	}
    	/**
    	 * Get cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @return {String} the value of the cookie
    	 */
    	static getFromSimpleArray(tab, what) {
    		for (var i2 = 0; i2 < tab.length; i2++) {
    			if (tab[i2][0] == what) return tab[i2][1];
    		}
    		return "";
    	}
    	/**
    	 * Get cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @return {String} the value of the cookie
    	 */
    	static getSimpleArray(tab, index, what) {
    		var pos = this.searchSimpleArray(tab, index, what);
    		if (pos >= 0) return tab[pos];
    		return null;
    	}
    	/**
    	 * Get cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @return {String} the value of the cookie
    	 */
    	static searchSimpleArray(tab, index, what) {
    		for (var i2 = 0; i2 < tab.length; i2++) {
    			if (tab[i2][index] == what) return i2;
    		}
    		return -1;
    	}
    	/**
    	 * Get cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @return {String} the value of the cookie
    	 */
    	static searchArrayObjects(tab, what, val) {
    		for (var i2 = 0; i2 < tab.length; i2++) {
    			if (tab[i2][what] == val) return i2;
    		}
    		return -1;
    	}
    	/**
    	 * Get cookie
    	 * @param  {String} cname - the name of the cookie
    	 * @return {String} the value of the cookie
    	 */
    	static getFromArrayObjects(tab, what, val) {
    		for (var i2 = 0; i2 < tab.length; i2++) {
    			if (tab[i2][what] == val) return tab[i2];
    		}
    		return null;
    	}
    	/**
    	 * To document
    	 */
    	static inArray(needle, haystack) {
    		var length = haystack.length;
    		for (var i = 0; i < length; i++) {
    			if (haystack[i] == needle) return true;
    		}
    		return false;
    	}
    	/**
    	 * To document
    	 * @param  {String}  str      the string to convert
    	 * @param  {Boolean} is_xhtml [description]
    	 * @return {String}           modified string
    	 */
    	static nl2br(str, is_xhtml) {
    		var breakTag = is_xhtml || typeof is_xhtml === "undefined" ? "<br " + "/>" : "<br>"; //Adjust comment to avoid issue on phpjs.org display

    		return (str + "").replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "$1" + breakTag + "$2");
    	}
    	/**
    	 * replaceCommaWithPoint description
    	 * @param  {String} txt
    	 * @return {String}
    	 */
    	static replaceCommaWithPoint(txt) {
    		txt += "";
    		/*if (typeof(txt)=="string" ) {
    			if (txt.indexOf(",")>=0) {
    				var txt1 = txt.substring(0,txt.indexOf(","))	;
    				var txt2 = txt.substring(txt.indexOf(",")+1,txt.length)	;
    				return txt1+"."+txt2 ;
    			}
    		}
    		*/
    		var exp = new RegExp(",", "g");
    		return txt.replace(exp, ".");
    	}
    	/**
    	 * Like PHP function
    	 * @param  {Array} tab
    	 * @param  {String} field
    	 * @return {Boolean}       true if is set
    	 */
    	static isset(tab, field) {
    		if (typeof tab[field] != "undefined") return true;
    		return false;
    	}
    	/**
    	 * Like PHP function
    	 * @param  {String} path
    	 * @param  {String} options
    	 * @return {String}
    	 */
    	static pathinfo(path, options) {
    		//  discuss at: http://phpjs.org/functions/pathinfo/
    		// original by: Nate
    		//  revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
    		// improved by: Brett Zamir (http://brett-zamir.me)
    		//    input by: Timo
    		//        note: Inspired by actual PHP source: php5-5.2.6/ext/standard/string.c line #1559
    		//        note: The way the bitwise arguments are handled allows for greater flexibility
    		//        note: & compatability. We might even standardize this code and use a similar approach for
    		//        note: other bitwise PHP functions
    		//        note: php.js tries very hard to stay away from a core.js file with global dependencies, because we like
    		//        note: that you can just take a couple of functions and be on your way.
    		//        note: But by way we implemented this function, if you want you can still declare the PATHINFO_*
    		//        note: yourself, and then you can use: pathinfo('/www/index.html', PATHINFO_BASENAME | PATHINFO_EXTENSION);
    		//        note: which makes it fully compliant with PHP syntax.
    		//  depends on: basename
    		//   example 1: pathinfo('/www/htdocs/index.html', 1);
    		//   returns 1: '/www/htdocs'
    		//   example 2: pathinfo('/www/htdocs/index.html', 'PATHINFO_BASENAME');
    		//   returns 2: 'index.html'
    		//   example 3: pathinfo('/www/htdocs/index.html', 'PATHINFO_EXTENSION');
    		//   returns 3: 'html'
    		//   example 4: pathinfo('/www/htdocs/index.html', 'PATHINFO_FILENAME');
    		//   returns 4: 'index'
    		//   example 5: pathinfo('/www/htdocs/index.html', 2 | 4);
    		//   returns 5: {basename: 'index.html', extension: 'html'}
    		//   example 6: pathinfo('/www/htdocs/index.html', 'PATHINFO_ALL');
    		//   returns 6: {dirname: '/www/htdocs', basename: 'index.html', extension: 'html', filename: 'index'}
    		//   example 7: pathinfo('/www/htdocs/index.html');
    		//   returns 7: {dirname: '/www/htdocs', basename: 'index.html', extension: 'html', filename: 'index'}

    		var opt = "",
    			optName = "",
    			optTemp = 0,
    			tmp_arr = {},
    			cnt = 0,
    			i = 0;
    		var have_basename = false,
    			have_extension = false,
    			have_filename = false;

    		// Input defaulting & sanitation
    		if (!path) {
    			return false;
    		}
    		if (!options) {
    			options = "PATHINFO_ALL";
    		}

    		// Initialize binary arguments. Both the string & integer (constant) input is
    		// allowed
    		var OPTS = {
    			PATHINFO_DIRNAME: 1,
    			PATHINFO_BASENAME: 2,
    			PATHINFO_EXTENSION: 4,
    			PATHINFO_FILENAME: 8,
    			PATHINFO_ALL: 0
    		};
    		// PATHINFO_ALL sums up all previously defined PATHINFOs (could just pre-calculate)
    		for (optName in OPTS) {
    			OPTS.PATHINFO_ALL = OPTS.PATHINFO_ALL | OPTS[optName];
    		}
    		if (typeof options !== "number") {
    			// Allow for a single string or an array of string flags
    			options = [].concat(options);
    			for (i = 0; i < options.length; i++) {
    				// Resolve string input to bitwise e.g. 'PATHINFO_EXTENSION' becomes 4
    				if (OPTS[options[i]]) {
    					optTemp = optTemp | OPTS[options[i]];
    				}
    			}
    			options = optTemp;
    		}

    		// Internal Functions
    		var __getExt = function(path) {
    			var str = path + "";
    			var dotP = str.lastIndexOf(".") + 1;
    			return !dotP ? false : dotP !== str.length ? str.substr(dotP) : "";
    		};

    		// Gather path infos
    		if (options & OPTS.PATHINFO_DIRNAME) {
    			var dirName = path.replace(/\\/g, "/").replace(/\/[^\/]*\/?$/, ""); // dirname
    			tmp_arr.dirname = dirName === path ? "." : dirName;
    		}

    		if (options & OPTS.PATHINFO_BASENAME) {
    			if (false === have_basename) {
    				have_basename = this.basename(path);
    			}
    			tmp_arr.basename = have_basename;
    		}

    		if (options & OPTS.PATHINFO_EXTENSION) {
    			if (false === have_basename) {
    				have_basename = this.basename(path);
    			}
    			if (false === have_extension) {
    				have_extension = __getExt(have_basename);
    			}
    			if (false !== have_extension) {
    				tmp_arr.extension = have_extension;
    			}
    		}

    		if (options & OPTS.PATHINFO_FILENAME) {
    			if (false === have_basename) {
    				have_basename = this.basename(path);
    			}
    			if (false === have_extension) {
    				have_extension = __getExt(have_basename);
    			}
    			if (false === have_filename) {
    				have_filename = have_basename.slice(
    					0,
    					have_basename.length - (have_extension ? have_extension.length + 1 : have_extension === false ? 0 : 1)
    				);
    			}

    			tmp_arr.filename = have_filename;
    		}

    		// If array contains only 1 element: return string
    		cnt = 0;
    		for (opt in tmp_arr) {
    			cnt++;
    		}
    		if (cnt == 1) {
    			return tmp_arr[opt];
    		}

    		// Return full-blown array
    		return tmp_arr;
    	}
    	/**
    	 * Format a number with good decimal and add devise
    	 * @param  {Number} nb
    	 * @param  {Number} decimal
    	 * @param  {String} devise
    	 * @return {String}
    	 */
    	static formatPrice(nb, decimal = 2, devise = M_.i18n.devise) {
    		return this.price(nb, decimal, devise);
    	}
    	/**
    	 * Alias of formatPrice
    	 * @param  {Number} nb
    	 * @param  {Number} decimal
    	 * @param  {String} devise
    	 * @return {String}
    	 */
    	static price(nb, decimal = 2, devise = M_.i18n.devise, sep = " ") {
    		return this.number_format(nb, decimal, ",", ".") + sep + devise;
    	}
    	static shortPrice(nb, devise = M_.i18n.devise, sep = " ") {
    		var res = "";
    		if (nb < 1000) res += Math.round(nb) + "";
    		else if (nb < 1000000) res += Math.round(nb / 1000) + "K";
    		else res += Math.round(nb / 1000000) + "M";
    		return res + sep + devise;
    	}
    	/**
    	 * Format number in %
    	 * @param  {Number} nb
    	 * @param  {Number} decimal
    	 * @return {String}
    	 */
    	static purcent(nb, decimal = 2, sep = " ") {
    		return this.number_format(nb, decimal, ",", ".") + sep + "%";
    	}
    	/**
    	 * Like PHP function
    	 * @param  {Number} a
    	 * @param  {Number} b Number of decimal
    	 * @param  {String} c decimal separator
    	 * @param  {String} d thousen separator
    	 * @return {String}   this formated number
    	 */
    	static number_format(a, b, c, d) {
    		var i, e, f, g, h;
    		a = Number(a);
    		a = Math.round(a * Math.pow(10, b)) / Math.pow(10, b);
    		e = a + "";
    		f = e.split(".");
    		if (!f[0]) f[0] = "0";
    		if (!f[1]) f[1] = "";
    		if (f[1].length < b) {
    			g = f[1];
    			for (i = f[1].length + 1; i <= b; i++) {
    				g += "0";
    			}
    			f[1] = g;
    		}
    		if (d !== "" && f[0].length > 3) {
    			h = f[0];
    			f[0] = "";
    			for (var j = 3; j < h.length; j += 3) {
    				i = h.slice(h.length - j, h.length - j + 3);
    				f[0] = d + i + f[0] + "";
    			}
    			j = h.substr(0, h.length % 3 === 0 ? 3 : h.length % 3);
    			f[0] = j + f[0];
    		}
    		c = b <= 0 ? "" : c;
    		return f[0] + c + f[1];
    	}
    	/**
    	 * Text selection in HTML input field
    	 * @param {jQuery} jEl
    	 * @param {Number} start from which char
    	 * @param {Number} stop  to which char
    	 */
    	static setSelectionRange(jEl, start = 0, stop = 0) {
    		if (jEl instanceof $) jEl = jEl[0];
    		if (jEl.childNodes.length) {
    			// log("jEl.childNodes[0]",jEl.childNodes[0])
    			var range = document.createRange();
    			var sel = window.getSelection();
    			range.setStart(jEl.childNodes[0], start);
    			range.collapse(true);
    			if (stop > 0) range.setEnd(jEl.childNodes[0], stop);
    			sel.removeAllRanges();
    			sel.addRange(range);
    		}
    		// log("range",range)
    	}
    	/**
    	 * Return the upper z-index
    	 * @param  {jQuery} [selectorLimit]
    	 * @return {number}
    	 */
    	static getMaxZIndex(selectorLimit = null) {
    		var max = 0;
    		$(selectorLimit || "*").each((index, el) => {
    			var jEl = $(el);
    			if (el.id == "M_Help") return true;
    			var zindex = parseInt(jEl.css("z-index"), 10) || 0;
    			max = Math.max(max, zindex);
    		});
    		// log("max",max)
    		return max;
    	}
    	/**
    	 * Get max z-index + 10
    	 * @param  {Number} [increment] number to increment
    	 * @return {number}           next z-index
    	 */
    	static getNextZIndex(increment = 10) {
    		return this.getMaxZIndex() + increment;
    	}
    	/**
    	 * Get default speed limit
    	 * @return {Number} number of milliseconds
    	 */
    	static getSpeedAnim() {
    		return 500;
    	}
    	/**
    	 * Display smoothly an element
    	 * @param  {jQuery}   jEl the element
    	 * @param  {Function} cb  callback function after finished animation
    	 */
    	static showSmoothly(jEl, cb) {
    		if (jEl.is(":hidden"))
    			jEl
    				.css("opacity", 0)
    				.show()
    				.transition({ opacity: 1 }, this.getSpeedAnim(), () => {
    					if (cb) cb();
    				});
    		else if (cb) cb();
    	}
    	/**
    	 * Hide smoothly an element
    	 * @param  {jQuery}   jEl the element
    	 * @param  {Function} cb  callback function after finished animation
    	 */
    	static hideSmoothly(jEl, cb) {
    		if (jEl.is(":visible"))
    			jEl.css("opacity", 1).transition({ opacity: 0 }, this.getSpeedAnim(), () => {
    				jEl.hide();
    				if (cb) cb();
    			});
    		else if (cb) cb();
    	}
    	/**
    	 * Do a POST of json data
    	 * @param  {String}   url  the url to call
    	 * @param  {object}   args arguments to send
    	 * @param  {Function} cb   callback function with "data" argument
    	 * @return {jqXHR}
    	 */
    	static postJson(url, args, cb) {
    		return this.ajaxJson(url, args, "POST", cb);
    	}
    	/**
    	 * Do a PUT of json data
    	 * @param  {String}   url  the url to call
    	 * @param  {object}   args arguments to send
    	 * @param  {Function} cb   callback function with "data" argument
    	 * @return {jqXHR}
    	 */
    	static putJson(url, args, cb) {
    		return this.ajaxJson(url, args, "PUT", cb);
    	}
    	/**
    	 * Do a GET of json data
    	 * @param  {String}   url  the url to call
    	 * @param  {object}   args arguments to send
    	 * @param  {Function} cb   callback function with "data" argument
    	 * @return {jqXHR}
    	 */
    	static getJson(url, args, cb) {
    		return this.ajaxJson(url, args, "GET", cb);
    	}
    	/**
    	 * Do a DELETE of json data
    	 * @param  {String}   url  the url to call
    	 * @param  {object}   args arguments to send
    	 * @param  {Function} cb   callback function with "data" argument
    	 * @return {jqXHR}
    	 */
    	static deleteJson(url, args, cb) {
    		return this.ajaxJson(url, args, "DELETE", cb);
    	}
    	/**
    	 * Do a Ajax call of json data
    	 * @param  {String}   url  the url to call
    	 * @param  {object}   args arguments to send
    	 * @param  {String}   POST|PUT|GET|DELETE
    	 * @param  {Function} cb   callback function with "data" argument
    	 * @return {jqXHR}
    	 */
    	static ajaxJson(url, args, method, cb) {
    		var opts = {
    			url: url,
    			type: method,
    			dataType: "json",
    			success: function(data) {
    				cb(data);
    			},
    			error: function(jqXHR, textStatus, errorThrown) {
    				cb(jqXHR.responseJSON);
    			}
    		};
    		if (method == "GET" || method == "DELETE") {
    			if (args) opts.data = args;
    		} else {
    			opts.data = JSON.stringify(args);
    			opts.contentType = "application/json";
    			opts.processData = false;
    			opts.cache = false;
    		}
    		// log("opts",opts)

    		if (M_.App.useWebsocket) {
    			if (method == "POST")
    				return io.socket.post(url, opts, (data, jwres) => {
    					cb(data);
    				});
    			if (method == "PUT")
    				return io.socket.put(url, opts, (data, jwres) => {
    					cb(data);
    				});
    			if (method == "GET")
    				return io.socket.get(url, opts, (data, jwres) => {
    					cb(data);
    				});
    			if (method == "DELETE")
    				return io.socket.delete(url, opts, (data, jwres) => {
    					cb(data);
    				});
    		}
    		return $.ajax(opts);
    	}

    	// exemple :
    	// M_.Utils.saveFiles([form.find('co_avatar_send').jEl.get(0)], '/avatarupdate', {}, function() {
    	// }) ;
    	/**
    	 * Save a file with Ajax technic
    	 * @param  {Array}   files    array of input html files
    	 * @param  {String}   url      url to call
    	 * @param  {Object}   moreArgs send more arguments
    	 * @param  {Function} cb       callback after finished
    	 * @return {jqXHR}
    	 * @example
    M_.Utils.saveFiles([form.find('co_avatar_send').jEl.get(0)], '/avatarupdate', {}, function() {}) ;
    	 */
    	static saveFiles(files, url, moreArgs = {}, cb = null) {
    		if (files.length === 0) return cb();
    		var formData = new FormData();
    		for (var key in moreArgs) {
    			formData.append(key, moreArgs[key]);
    		}
    		// console.log("files", files);
    		for (var i = 0; i < files.length; i++) {
    			// console.log("files[i].files", files[i].files);
    			for (var j = 0; j < files[i].files.length; j++) {
    				var name = files[i].name;
    				// if (files[i].files.length>0) name += '['+j+']' ;
    				// console.log("name", name);
    				formData.append(name, files[i].files[j]);
    			}
    		}
    		return $.ajax({
    			url: url,
    			type: "POST",
    			//headers: {Connection: close},
    			data: formData,
    			contentType: false,
    			processData: false,
    			dataType: "json",
    			cache: false,
    			success: data => {
    				cb(data);
    			}
    		});
    	}
    	/**
    	 * Check if a script is loaded (warning : check exactly the file name)
    	 * @param  {String}  url url to call
    	 * @return {Boolean}     true if already loaded
    	 */
    	static isLoadedScript(url) {
    		var ok = false;
    		$("script").each(function() {
    			if (this.src === url) ok = true;
    		});
    		return ok;
    	}
    	/**
    	 * Load a javascript file dynamically ; warning, no callback !!!
    	 * @param  {String} url url to call
    	 */
    	static loadScript(url) {
    		var script = document.createElement("script");
    		script.type = "text/javascript";
    		// script.src = 'https://maps.googleapis.com/maps/api/js?v=3.exp' +'&signed_in=true&callback=initialize';
    		script.src = url;
    		document.body.appendChild(script);
    	}
    	/**
    	 * To document
    	 */
    	static saveSelection() {
    		if (window.getSelection) {
    			var sel = window.getSelection();
    			if (sel.getRangeAt && sel.rangeCount) {
    				this._selection = sel.getRangeAt(0);
    			}
    		} else if (document.selection && document.selection.createRange) {
    			this._selection = document.selection.createRange();
    		} else this._selection = null;
    	}

    	/**
    	 * To document
    	 */
    	static restoreSelection() {
    		if (this._selection) {
    			if (window.getSelection) {
    				var sel = window.getSelection();
    				sel.removeAllRanges();
    				sel.addRange(this._selection);
    			} else if (document.selection && this._selection.select) {
    				this._selection.select();
    			}
    		}
    	}
    };
    M_.Utils._appIsFullScreen = false;

    /**
     * Observable interface
     * @interface M_.Observable
      */
    M_.Observable = {
    	/**
    	 * Call this method in init() method of your class where you implement this mixin
    	 */
    	initObservable: function() {
    		this._listeners = [];
    		if (this.listeners) {
    			for (var i = 0; i < this.listeners.length; i++) {
    				this.addListener(this.listeners[i][0], this.listeners[i][1]);
    			}
    		}
    		//log("this._listeners", this._listeners.length)
    	},
    	/**
    	 * Add a listner to this object
    	 * @param {String} evtName The event name
    	 * @param {Function} fct     A callback function to execute when event is trigged
    	 */
    	addListener: function(evtName, fct) {
    		this._listeners.push({ evtName: evtName, fct: fct });
    	},
    	/**
    	 * Remove a listner to this object
    	 * @param {String} evtName The event name
    	 * @param {Function} fct     A callback function previously saved whith addListener()
    	 */
    	removeListener: function(evtName, fct) {
    		var listenersOk = [];
    		for (var i = 0; i < this._listeners.length; i++) {
    			if (this._listeners[i].evtName == evtName && this._listeners[i].fct == fct) {
    				listenersOk.push(this._listeners[i]);
    			}
    		}
    		this._listeners = listenersOk;
    	},
    	/**
    	 * Trigger the event
    	 * @param  {String} evtName The event name previously registered with addListener()
    	 * @return {boolean}         Return the result of callback function
    	 */
    	trigger: function(evtName) {
    		var res = true;
    		for (var i = 0; i < this._listeners.length; i++) {
    			if (this._listeners[i].evtName == evtName) {
    				var args = Array.prototype.slice.call(arguments);
    				res = this._listeners[i].fct.apply(this, args.slice(1));
    			}
    		}
    		return res;
    	}
    };

    /**
     * Redefine this constants in your language
      */
    M_.i18n = {
    	// days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
    	// months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    	// daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
    	// monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    	devise: "€",
    	translate: function() {}
    };

    /**
     * Create a model class. Generally you extends this class like exemple below
     * @class
      * @implements M_.Observable
     * @property {array} fields - the fields definition
     * @property {object} row - the values
     * @property {String} primaryKey - the primary key name
     * @example
    M_.ModelKeyVal = class extends M_.Model {
    	getDefinition() {
    		return {
    			primaryKey: "key",
    			fields: [{ name: 'key' }, { name: 'val' }]
    		};
    	}
    };
     */
    M_.Model = class {
    	constructor(opts) {
    		var defaults = {
    			fields: null,
    			row: {},
    			store: null,
    			primaryKey: null
    		};
    		$.extend(this, defaults, opts, this.getDefinition());

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		if (!this.row) log("Vous devez définir opts.row");
    		// if (!this.store) log("Vous devez définir opts.store") ;
    		var fields = this.fields;
    		for (var j = 0; j < fields.length; j++) {
    			if (!fields[j].type) fields[j].type = "string";
    		}
    		this.updateRow();
    	}
    	/**
         * You must extends this class to return a definition
    	 * @function M_.Model#getDefinition
         */
    	getDefinition() {
    		return {};
    	}
    	/**
    	 * @return {type}
    	 */
    	updateRow() {
    		var fields = this.fields;
    		// log("fields",fields)
    		for (var j = 0; j < fields.length; j++) {
    			var val = "";
    			if (fields[j].type == "date") {
    				if (this.row[fields[j].name] === undefined) continue;
    				if ($.type(this.row[fields[j].name]) == "date") {
    					val = this.row[fields[j].name];
    				} else {
    					// var fd = 'Y-m-d' ;
    					if (this.row[fields[j].name]) {
    						// if (this.row[fields[j].name].length>10) fd = 'Y-m-d H:i:s' ;
    						// log("this.row[fields[j].name",this.row[fields[j].name])
    						// val = M_.Utils.parseDate(this.row[fields[j].name], fd) ;
    						val = moment(this.row[fields[j].name]);
    						// log("val",val)
    					} else val = "";
    				}
    				this.row[fields[j].name] = val;
    			}
    			if (fields[j].type == "number") {
    				val = this.row[fields[j].name];
    				//log("number",fields[j].name,val)
    				this.row[fields[j].name] = val * 1;
    			}
    		}
    	}
    	/**
         * Get an object that represent the data model
         * @returns {object} un nombre
         */
    	getData() {
    		return this.getArray();
    	}
    	getRowData() {
    		return this.getArray();
    	}
    	getArray() {
    		var ret = {};
    		var fields = this.fields;
    		for (var j = 0; j < fields.length; j++) {
    			var val = this.get(fields[j].name);
    			if (val !== null) {
    				ret[fields[j].name] = val;
    			}
    		}
    		for (var key in this.row) {
    			if (!M_.Utils.isset(ret, key)) ret[key] = this.row[key];
    		}
    		return ret;
    	}
    	/**
         * Set a object that represent the data model
    	 * @param  {object} row - The row
    	 * @param  {object} silently - Event "update" is not called
         */
    	setRow(row, silently = false) {
    		this.row = row;
    		if (silently !== true) {
    			this.trigger("update", this);
    			if (this.store) this.store.trigger("update", this.store);
    		}
    	}
    	/**
         * Set only one field of model
    	 * @param  {String} field - The field name
    	 * @param  {something} val - The value
    	 * @param  {object} silently - Event "update" is not called
         */
    	set(field, val, silently) {
    		//log("val",field,val)
    		this.row[field] = val;
    		if (silently !== true) {
    			this.trigger("update", this);
    			if (this.store) this.store.trigger("update", this.store);
    		}
    	}
    	/**
         * Create an empty model
         */
    	createEmptyRow() {
    		var fields = this.fields;
    		for (var j = 0; j < fields.length; j++) {
    			this.row[fields[j].name] = "";
    		}
    	}

    	//warning : stocker le résultat ???
    	/**
         * Get a field
    	 * @param  {String} field - The field name
         */
    	get(field) {
    		var val,
    			fields = this.fields;

    		for (var j = 0; j < fields.length; j++) {
    			// log("fields[j].name",fields[j].name)
    			if (fields[j].name == field) {
    				if (fields[j].fn) {
    					//if (this.row[field]) val = this.row[field] ;
    					//else val = eval(fields[j].fn) ;
    				} else if (this.row[field] !== undefined) {
    					//fields[j].type &&
    					if (fields[j].type == "string" || fields[j].type == "text") {
    						val = this.row[field];
    					} else if (fields[j].type == "number") {
    						val = this.row[field] * 1;
    					} else if (fields[j].type == "array") {
    						val = this.row[field];
    					} else if (fields[j].type == "boolean") {
    						if (this.row[field] == "true" || this.row[field] === true || this.row[field] === "1" || this.row[field] === 1) val = true;
    						else val = false;
    					} else if (fields[j].type == "date") {
    						if ($.type(this.row[field]) == "date") val = this.row[field];
    						else {
    							// var fd = 'Y-m-d' ;
    							if (this.row[fields[j]]) {
    								// if (this.row[fields[j]].length>10) fd = 'Y-m-d H:i:s' ;
    								// val = M_.Utils.parseDate(this.row[field]) ;
    								val = moment(this.row[field]);
    							}
    						}
    					}
    				}
    				break;
    			}
    		}
    		if (val === undefined && M_.Utils.isset(this.row, field)) val = this.row[field];
    		return val;
    	}
    	/**
         * Get the id of row (defined in primaryKey)
         */
    	getId() {
    		return this.get(this.primaryKey);
    	}
    };

    /**
     * Use a standard model key / value. No params.
     * @class M_.ModelKeyVal
      * @example
    // Very useful with combobox
    var combo = new M_.Form.Combobox({
    	name: 'of_typecook',
    	label: "Type de cuisine",
    	container: $("#here"),
    	store: new M_.Store({
    		controller: this,
    		model: M_.ModelKeyVal,
    		currentSort: ["key", 1],
    		rows: [{key:1, val:"Cuisine américaine"}, {key:2, val:"Cuisine niçoise"}]
    	})
    }) ;
     */
    M_.ModelKeyVal = class extends M_.Model {
    	getDefinition() {
    		return {
    			primaryKey: "key",
    			fields: [{ name: "key" }, { name: "val" }]
    		};
    	}
    };

    /**
     * A store is a client cache of rows. It can do ajax requests in json
     * @class
      * @implements M_.Observable
     * @property {array} [rows=[]] - the data rows
     * @property {boolean} [useWebsocket=M_.App.useWebsocket] - true if use websocket and not ajax
     * @property {array} [rowsModel=[]] - the model rows
     * @property {String} [url=""] - the model rows
     * @property {object} [args={}] - arguments object to pass to each ajax request
     * @property {String} [rootData="data"] - property where read data in json response
     * @property {array} [currentSort=null] - sort the store : ['name', 'asc'] ; the second is 'asc' or 'desc'
     * @property {M_.Model} [model=null] - the model to use
     * @property {String} [primaryKey=""] - the primary key
     * @property {number} [limit=null] - send 'limit' param to ajax request
     * @property {boolean} [sortOnRemote=false] - send 'sort' param to ajax request
     * @property {number} [skip=0] - send 'skip' param to ajax request
     * @property {array} [unshiftRows=[]] - add this rows in first position, just after load
     * @property {array} [pushRows=[]] - add this rows in last positions, just after load
     * @example
    // json store
    var store = new M_.Store({
    	controller: this,
    	model: MT_Contacts,
    	url: "/contacts",
    	limit: 200,
    	listeners: [
    		['load',(store, data)=> {
    			// data are loaded
    		}]
    	]
    }) ;
    // ajax response would have the fields define in your model
    {
    	data: [
    		{id:1, name:"David Miglior"},
    		{id:2, name:"Michael Miglior"}
    	]
    }
     */
    M_.Store = class {
    	/**
    	 * Initialize the store. If you extend this method, you should call this._super()
    	 *
    	 * @param  {object} opts The configuration object
    	 */
    	constructor(opts) {
    		var defaults = {
    			rows: [],
    			useWebsocket: M_.App.useWebsocket,
    			rowsModel: [],
    			_tabFieldsForSort: [],
    			url: "",
    			args: {},
    			rootData: "data",
    			currentSort: null,
    			model: null,
    			lastLoadArgs: {},
    			primaryKey: "",
    			limit: null,
    			sortOnRemote: false,
    			skip: 0,
    			lastTotal: 0,
    			unshiftRows: [],
    			pushRows: [],
    			_loaded: false,
    			_loading: false
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		if (this.model) {
    			var model = new this.model({ row: {}, store: {} });
    			this.primaryKey = model.primaryKey;
    		}
    		//must be last init, because update event
    		if (this.rows && this.rows.length > 0) {
    			//M_.Utils.delay("", $.proxy(function() {
    			this.setRows(this.rows);
    			//}, this), 10) ;
    			//			log("this.unshiftRows",this.unshiftRows)
    			if (this.unshiftRows && this.unshiftRows.length > 0) {
    				for (let i = 0; i < this.unshiftRows.length; i++) this.rows.unshift(this.unshiftRows[i]);
    			}
    			if (this.pushRows && this.pushRows.length > 0) {
    				for (let i = 0; i < this.pushRows.length; i++) this.rows.push(this.pushRows[i]);
    			}
    			this.useModel();
    		}
    	}

    	/**
    	 * Return the first model instance where the value of field equal val. The comparaison is do with ==, so "2"==2
    	 *
    	 * @param  {String} field The searched field name
    	 * @param  {string|number} val   The value researched.
    	 * @return {model}       Return the model instance founded or null
    	 */
    	//val can be a RegExp
    	getRowBy(field, val) {
    		var rows = this.rowsModel;
    		for (var i = 0; i < rows.length; i++) {
    			var v;
    			if (this.model) v = rows[i].get(field);
    			else v = rows[i][field];
    			if (v == val) {
    				return rows[i];
    			}
    		}
    		return null;
    	}
    	/**
    	 * @param  {type}
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getIndexBy(field, val) {
    		var rows = this.rowsModel;
    		for (var i = 0; i < rows.length; i++) {
    			var v = rows[i].get(field);
    			if (v == val) {
    				return i;
    			}
    		}
    		return -1;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getRowByIndex(index) {
    		var rows = this.rowsModel;
    		if (rows[index]) return rows[index];
    		return null;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getRow(val) {
    		return this.getRowBy(this.primaryKey, val);
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getIndexById(id) {
    		var rows = this.rowsModel;
    		for (var i = 0; i < rows.length; i++) {
    			var v = rows[i].getId();
    			if (v == id) {
    				return i;
    			}
    		}
    		return -1;
    	}

    	//val can be a RegExp
    	/**
    	 * @param  {type}
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getRowsBy(field, val) {
    		var res = [];
    		var rows = this.rowsModel;
    		for (var i = 0; i < rows.length; i++) {
    			var v = rows[i].get(field);
    			if (v == val) {
    				res.push(rows[i]);
    			}
    		}
    		return res;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getRowAt(index) {
    		return this.rowsModel[index];
    	}
    	/**
    	 * @param {type}
    	 */
    	setRows(rows) {
    		this.rows = rows;
    		this.useModel();
    		//log("this.rows1",this.rows)
    		//log("setRows",this.rowsModel)
    		this.trigger("update", this, this.rowsModel);
    	}
    	/**
    	 * @param {type}
    	 */
    	setModels(rows) {}
    	/**
    	 * @param {type}
    	 */
    	addModel(row) {}
    	/**
    	 * @param {type}
    	 */
    	addRow(row) {
    		this.addRows([row]);
    	}
    	/**
    	 * @param {type}
    	 */
    	addModels(rows) {}
    	/**
    	 * @param {type}
    	 */
    	addRows(rows) {
    		//this.rows = this.rows.concat(rows) ;
    		//this.useModel() ;
    		for (var i = 0; i < rows.length; i++) {
    			var m = null;
    			var row = rows[i];
    			if (row instanceof M_.Model) {
    				m = row;
    				row = row.getArray();
    			} else {
    				m = new this.model({ row: row, store: this });
    			}
    			this.rows.push(row);
    			this.rowsModel.push(m);
    			// this.addRow(rows[i]) ;
    			this.trigger("addedRow", this, m);
    		}
    		this.trigger("update", this, this.rowsModel);
    	}
    	/**
    	 * @return {type}
    	 */
    	getRows() {
    		return this.rowsModel;
    	}
    	/**
    	 * @return {type}
    	 */
    	count() {
    		return this.rowsModel.length;
    	}
    	/**
    	 * @return {type}
    	 */
    	countTotal() {
    		//log("countTotal",this.rowsModel,this.rows)
    		if (this.lastTotal === 0) return this.count();
    		return this.lastTotal;
    	}
    	/**
    	 * @return {type}
    	 */
    	deleteAll() {
    		this.rows = [];
    		this.rowsModel = [];
    		this.trigger("removedAll", this);
    		this.trigger("update", this, this.rowsModel);
    	}
    	/**
    	 * To document
    	 */
    	setKeysOnRows() {
    		for (var i = 0; i < this.rows.length; i++) {
    			this.rows[i]._mid = i;
    		}
    	}
    	_sortRemote(fields, direction) {
    		//log("sortRemote")
    		if (!this.lastLoadArgs) this.lastLoadArgs = {};
    		// this.lastLoadArgs.sort = this.currentSort[0] ;
    		// this.lastLoadArgs.direction = this.currentSort[1] ;
    		// this.lastLoadArgs.sort = fields ;
    		// if (direction>0) this.lastLoadArgs.sort += " ASC" ;
    		// else this.lastLoadArgs.sort += " ASC" ;
    		this.reload();
    	}
    	/**
    	 * To document
    	 * @param  {Array} fields
    	 * @param  {type} direction - direction=1 ASC, direction=-1 DESC, direction=0 toggle
    	 * @param  {type} silently
    	 * @return {type}
    	 * @example
    	 * 	store.sort('co_name') ;
    	 * 	store.sort('co_name', 1) ;
    	 * 	store.sort([['co_name', 1], ['co_date',-1]]) ;
    	 */
    	sort(fields, direction, silently) {
    		// console.log("fields", fields);
    		if (!direction) direction = 0;
    		if (_.isFunction(fields)) {
    		} else {
    			if (direction === 0) {
    				if (!this._tabFieldsForSort[fields]) {
    					this._tabFieldsForSort[fields] = 1;
    				} else {
    					this._tabFieldsForSort[fields] = this._tabFieldsForSort[fields] * -1;
    				}
    				direction = this._tabFieldsForSort[fields];
    			} else {
    				this._tabFieldsForSort[fields] = direction;
    			}
    			this.currentSort = [fields, direction];
    		}
    		if (this.trigger("beforeSort", this, fields, direction) === false) return false;
    		//log("this.currentSort",this.currentSort)
    		if (this.sortOnRemote) {
    			this._sortRemote(fields, direction);
    			return;
    		}
    		// console.log("ok");
    		this.rowsModel.sort((a, b) => {
    			// log("a,b",a,b)
    			var a0;
    			var b0;
    			if (_.isFunction(fields)) {
    				a0 = fields(a);
    				b0 = fields(b);
    				// console.log("a0, b0", a0, b0);
    			} else {
    				a0 = a.get(fields);
    				b0 = b.get(fields);
    			}
    			if (a0 === null || b0 === null) return 0;
    			if (typeof a0 == "number") {
    				if (direction == -1) return b0 - a0;
    				return a0 - b0;
    			} else if (a0 instanceof Date) {
    				if (direction == -1) return b0 - a0;
    				return a0 - b0;
    			} else if (a0 && b0 && a0.getDate && a0.getDate() instanceof Date) {
    				if (direction == -1) return b0.getDate() - a0.getDate();
    				return a0.getDate() - b0.getDate();
    			} else if (a0 && b0) {
    				if (direction == -1) return b0.localeCompare(a0);
    				return a0.localeCompare(b0);
    			} else return 0;
    		});
    		this.trigger("sort", this);
    		if (silently !== true) {
    			this.trigger("update", this, this.rowsModel);
    		}
    	}
    	// sortByFunction(fun) {
    	// 	this.rowsModel.sort((a, b)=> {
    	// 		var vala = fun(a) ;
    	// 		var valb = fun(b) ;
    	// 	}) ;
    	// 	this.trigger("sort", this);
    	// }
    	/**
    	 * To document
    	 */
    	useModel() {
    		if (this.model) {
    			this.rowsModel = [];
    			// var fields = this.model.fields ;
    			// log("this.rows",this.rows)
    			for (var i = 0; i < this.rows.length; i++) {
    				this.rowsModel[i] = new this.model({ row: this.rows[i], store: this });
    				//this.rowsModel[i].updateRow() ;
    			}
    		} else {
    			this.rowsModel = this.rows;
    		}
    	}
    	/**
    	 * @param  {Function} fn
    	 */
    	each(fn) {
    		//log("length",this.rowsModel)
    		for (var i = 0; i < this.rowsModel.length; i++) {
    			var ret = fn.call(this, this.rowsModel[i], i);
    			if (ret === false) break;
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	createEmptyRow() {
    		var mod = new this.model({ row: {}, store: this });
    		mod.createEmptyRow();
    		return mod;
    	}
    	/**
    	 * @param {type} skip
    	 */
    	setSkip(skip) {
    		this.skip = skip;
    		if (!this.lastLoadArgs) this.lastLoadArgs = {};
    		this.lastLoadArgs.skip = skip;
    		this.reload();
    	}
    	/**
    	 * @param {type} query
    	 */
    	setQuery(query) {
    		this.reload();
    	}
    	/**
    	 * @return {Boolean}
    	 */
    	isLoading() {
    		return this._loading;
    	}
    	/**
    	 * @return {Boolean}
    	 */
    	isLoaded() {
    		return this._loaded;
    	}
    	/**
    	 * @param  {type} args
    	 * @param  {Function} callback
    	 * @return {type}
    	 */
    	load(args, callback) {
    		// log("load")
    		var okArgs = {};
    		if (this.currentSort) {
    			if (_.isArray(this.currentSort)) okArgs.sort = this.currentSort[0] + " " + this.currentSort[1];
    			else okArgs.sort = this.currentSort;
    		}
    		if (this.limit && this.limit > 0) okArgs.limit = this.limit;
    		if (this.skip) okArgs.skip = this.skip;

    		if (args && (args.skip || args.skip === 0)) this.skip = args.skip;
    		// if (args && args.sort && _.isArray(args.sort)) this.currentSort[0] = args.sort ;
    		// if (args && args.sort && _.isString(args.sort)) this.currentSort = args.sort ;
    		// if (args && args.direction) this.currentSort[1] = args.direction ;
    		if (args && args.limit) this.limit = args.limit;

    		var thisArgs = this.args;
    		if (_.isFunction(thisArgs)) thisArgs = this.args();
    		$.extend(okArgs, thisArgs, args);
    		var obj = { url: this.url, args: okArgs };
    		if (this.trigger("beforeLoad", this, obj) === false) return false;
    		if (obj.args.onBeforeLoad) obj.args.onBeforeLoad(this, obj);
    		this.lastLoadArgs = obj.args;
    		this._loaded = false;
    		this._loading = true;
    		if (this.useWebsocket) {
    			io.socket.get(obj.url, okArgs, (data, jwres) => {
    				this._treatDataStore(data, callback);
    			});
    		} else {
    			// if (this.loadAjaxXHR) this.loadAjaxXHR.abort() ;
    			// log("okArgs",okArgs)
    			this.loadAjaxXHR = M_.Utils.getJson(obj.url, okArgs, data => {
    				this._treatDataStore(data, callback);
    			});
    			// this.loadAjaxXHR = $.ajax({
    			// 	url: obj.url,
    			// 	type: 'GET',
    			// 	contentType: 'application/json',
    			// 	data: okArgs,
    			// 	dataType: 'json',
    			// 	success: (data) => {
    			// 		// log("data",data)
    			// 		this._treatDataStore(data, callback) ;
    			// 	}
    			// });
    		}
    	}
    	_treatDataStore(data, callback) {
    		// log("_treatDataStore",data)
    		this._loaded = true;
    		this._loading = false;
    		this.lastdata = data;

    		this.trigger("loadraw", this, data);

    		var i;
    		//if (this.lastLoadArgs.addRows) this.rows = this.rows.concat(data.data) ;
    		//else
    		if (data.total || data.total === 0) this.lastTotal = data.total * 1;
    		//		 log("this.unshiftRows",this.unshiftRows)
    		if (this.unshiftRows.length > 0) {
    			for (i = 0; i < this.unshiftRows.length; i++) data[this.rootData].unshift(this.unshiftRows[i]);
    		}
    		if (this.pushRows.length > 0) {
    			for (i = 0; i < this.pushRows.length; i++) data[this.rootData].push(this.pushRows[i]);
    		}
    		//if (this.lastLoadArgs.add)  ;
    		//else
    		this.rows = data[this.rootData];
    		// this.lastLoadArgs.add = false ;

    		this.useModel();
    		// if (this.currentSort && !this.sortOnRemote && _.isArray(this.currentSort))
    		// 	this.sort(this.currentSort[0], this.currentSort[1], true) ;
    		//this.setKeysOnRows() ;

    		this.trigger("load", this, this.rowsModel);
    		this.trigger("update", this, this.rowsModel);
    		if (this.lastLoadArgs.onUpdate) this.lastLoadArgs.onUpdate(this.rowsModel);

    		if (this.lastLoadArgs.onBeforeLoad) this.lastLoadArgs.onBeforeLoad = null;
    		if (this.lastLoadArgs.onUpdate) this.lastLoadArgs.onUpdate = null;
    		if (callback) callback(this);
    	}
    	/**
    	 * @param  {Boolean} [reset=false]
    	 * @param  {type} [moreArgs=null]
    	 * @return {type}
    	 */
    	reload(reset = false, moreArgs = null, cb = null) {
    		if (reset) {
    			this.skip = 0;
    			this.lastLoadArgs = {};
    		}
    		if (moreArgs) $.extend(this.lastLoadArgs, moreArgs);
    		this.load(this.lastLoadArgs, cb);
    	}
    };

    /**
     * A base graphical object
     * @class
      * @implements M_.Observable
     */
    M_.Outlet = class {
    	constructor(opts) {
    		var defaults = {
    			controller: null,
    			container: null,
    			id: "",
    			view: null,
    			jEl: null,
    			help: null,
    			rendered: false,
    			top: 0,
    			left: 0
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		if (M_.Utils.isEmpty(this.id)) this.id = M_.Utils.id();

    		if (!M_.Utils.isEmpty(this.jEl) && $.type(this.jEl) == "string") this.jEl = $("#" + this.jEl);
    		// if (!M_.Utils.isEmpty(this.container) && $.type(this.container)=="string") this.container = $("#"+this.container) ;

    		if (M_.Utils.isEmpty(this.jEl)) this.create();

    		if (this.jEl && M_.Utils.isEmpty(this.jEl.attr("id"))) this.jEl.attr("id", this.id);
    		this._attachHelp();

    		// if (M_.Utils.isEmpty(this.id)) {
    		// 	if (this.jEl && !M_.Utils.isEmpty(this.jEl.attr('id'))) {
    		// 		this.id = this.jEl.attr('id') ;
    		// 	} else {
    		// 		this.id = M_.Utils.id() ;
    		// 	}
    		// }
    		// if (this.jEl && M_.Utils.isEmpty(this.jEl.attr('id'))) this.jEl.attr('id', this.id) ;
    		// M_.App.registerOutlet(this) ;
    		this.trigger("created", this);
    	}
    	_attachHelp() {
    		if (this.help && this.jEl) {
    			new M_.Help({
    				controller: this.controller,
    				text: this.help,
    				attachedObj: this.container
    			});
    		}
    	}
    	/**
    	 * Extend this class to create your UI.
    	 */
    	create() {
    		this.jEl = $("<div></div>");
    		this.container.append(this.jEl);
    	}
    	/**
    	 * Destroy the outlet
    	 */
    	destroy() {
    		if (!this.jEl) return;
    		this.jEl.remove();
    		this.container.empty();
    	}
    	/**
    	 * Return jEl element. Generally the main object.
    	 * @returns {jquery} the main jQuery element
    	 */
    	getEl() {
    		return this.jEl;
    	}

    	/**
    	 * Search a class or selector in the container, like $.find()
    	 * @param  {string} selector
    	 * @return {jQuery}
    	 */
    	findEl(selector) {
    		return this.container.find(selector);
    	}
    };

    /**
     * Implement this interface to get a store
     * @interface
      */
    M_.Stored = {
    	//autoFocus: null,
    	/**
    	 * Call this method in your init() function
    	 */
    	initStored: function() {
    		if (!this.store) log("store is mandatory in M_.Stored");
    		else
    			//if (this.store) this.setStore(this.store) ;
    			this.store.addListener("update", $.proxy(this.updateStore, this));
    	},
    	/**
    	 * Configure a new store
    	 * @param {M_.Store} store The new store used
    	 */
    	setStore: function(store) {
    		this.store = store;
    		this.store.addListener("update", $.proxy(this.updateStore, this));
    	},
    	/**
    	 * Return the store object
    	 * @return {M_.Store} The store object or null
    	 */
    	getStore: function() {
    		return this.store;
    	}
    	/**
    	 * Redefine this method to update your UI
    	 * @param  {M_.Store} store The store object
    	 */
    	// updateStore: function(store) {
    	// }
    };

    /**
     * Display an help on jquery element
     * @class
      * @property  {type} controller
     * @property  {type} text
     * @property  {type} attachedObj
     * @property  {type} shiftX
     * @property  {type} shiftY
     * @property  {type} maxWidth
     */
    M_.Help = class {
    	constructor(opts) {
    		var defaults = {
    			controller: null,
    			text: "",
    			attachedObj: null,
    			shiftX: 15,
    			shiftY: -15,
    			maxWidth: 300,
    			showNow: false
    		};
    		opts = opts ? opts : {};
    		opts = $.extend(this, defaults, opts);
    		this.create();

    		if (this.showNow) {
    			this.show();
    			$(document).on("mousemove", $.proxy(this._mousemove, this));
    		}
    	}
    	_mousemove(evt) {
    		var top = evt.pageY * 1 + this.shiftY;
    		var left = evt.pageX * 1 + this.shiftX;
    		if (left + 300 + 100 > $(window).width()) {
    			left = evt.pageX * 1 - this.shiftX - $("#M_Help").outerWidth();
    			if ($("#M_Help").hasClass("left"))
    				$("#M_Help")
    					.removeClass("left")
    					.addClass("right");
    			if (!$("#M_Help").hasClass("right")) $("#M_Help").addClass("right");
    		} else {
    			if ($("#M_Help").hasClass("right"))
    				$("#M_Help")
    					.removeClass("right")
    					.addClass("left");
    			if (!$("#M_Help").hasClass("left")) $("#M_Help").addClass("left");
    		}
    		if (top + $("#M_Help").outerHeight() - 10 > $(window).height()) {
    			top = $(window).height() - $("#M_Help").outerHeight() - 10;
    			// $('#M_Help.left:after').css('top',top) ;
    		}
    		$("#M_Help").css({
    			top: top,
    			left: left
    		});
    	}
    	/**
    	 * To document
    	 */
    	create() {
    		// console.log("this.attachedObj", this.attachedObj);
    		this.attachedObj.mouseenter(evt => {
    			this.show(evt);
    			$(document).on("mousemove", $.proxy(this._mousemove, this));
    		});
    		this.attachedObj.mouseleave(evt => {
    			this.hide(evt);
    			$(document).off("mousemove", $.proxy(this._mousemove, this));
    		});
    		this.attachedObj.click(evt => {
    			this.hide(evt);
    		});
    		if (this.attachedObj.prop("tagName").toLowerCase() == "input") {
    			this.attachedObj.focus(evt => {
    				this.hide();
    			});
    			this.attachedObj.focus(evt => {
    				this.hide();
    			});
    		}
    	}
    	/**
    	 * @param  {event} evt
    	 */
    	show(evt) {
    		$("#M_Help")
    			.html(this.text)
    			.css("max-width", this.maxWidth)
    			.show();
    	}
    	/**
    	 * @param  {event} evt
    	 */
    	hide(evt) {
    		$("#M_Help").hide();
    	}
    	/**
    	 *
    	 */
    	static createMHelp() {
    		$("body").append("<div id='M_Help'></div>");
    	}
    	/**
    	 *
    	 */
    	static hideMHelp() {
    		$("#M_Help").hide();
    	}
    };

    /**
     * Like a card layout. Use HTML to define them.
     * @class
      * @property {String} firstTab
     */
    M_.Tabs = class extends M_.Outlet {
    	constructor(opts) {
    		var defaults = {
    			firstTab: null,
    			buttons: null,
    			onChange: null
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		if (this.buttons) {
    			this.buttons.find(".M_TabButton").each((ind, el) => {
    				$(el).click(evt => {
    					var jEl = $(evt.target);
    					if (jEl.hasClass("disabled")) return;
    					// console.log("jEl.attr('for')", jEl.attr('for'));
    					this.show(jEl.attr("for"), next => {
    						if (this.onChange) {
    							this.onChange(jEl.attr("for"), next);
    						} else {
    							next();
    						}
    					});
    				});
    			});
    		}

    		// this.container.children('.M_Tab').each(function() {
    		// 	this.tabTabs.push($(this)) ;
    		// }) ;
    	}
    	// addTab(jEl) {
    	// 	this.tabTabs.push(tab) ;
    	// }
    	/**
    	 * @param  {type}
    	 * @param  {type}
    	 * @return {type}
    	 */
    	show(jElId, cbBeforeShow, anim = true) {
    		var speed = M_.Utils.getSpeedAnim();
    		// log("show",jElId)
    		var current = null;
    		this.container.children(".M_Tab").each((ind, el) => {
    			// log("$(this).attr('id')",$(this).attr('id'))
    			if ($(el).hasClass("active")) {
    				current = $(el);
    				return false;
    			}
    		});
    		// log("current",current)
    		if (current && current.attr("id") == jElId) {
    			cbBeforeShow(cbAfterShow => {
    				if (cbAfterShow === false) return;
    				if (cbAfterShow) cbAfterShow();
    			});
    			return;
    		} else if (current) {
    			if (anim) {
    				current.transition({ opacity: 0 }, speed, () => {
    					current.removeClass("active");
    					$("#" + jElId)
    						.css("opacity", 0.01)
    						.addClass("active");
    					cbBeforeShow(cbAfterShow => {
    						if (cbAfterShow === false) return;
    						$("#" + jElId).transition({ opacity: 1 }, speed, () => {
    							if (cbAfterShow) cbAfterShow();
    						});
    					});
    				});
    			} else {
    				current.removeClass("active");
    				cbBeforeShow(cbAfterShow => {
    					if (cbAfterShow === false) return;
    					$("#" + jElId)
    						.css("opacity", 1)
    						.addClass("active");
    					if (cbAfterShow) cbAfterShow();
    				});
    			}
    		} else {
    			if (anim) {
    				$("#" + jElId)
    					.css("opacity", 0.01)
    					.addClass("active");
    				cbBeforeShow(cbAfterShow => {
    					if (cbAfterShow === false) return;
    					$("#" + jElId).transition({ opacity: 1 }, speed, () => {
    						if (cbAfterShow) cbAfterShow();
    					});
    				});
    			} else {
    				cbBeforeShow(cbAfterShow => {
    					if (cbAfterShow === false) return;
    					$("#" + jElId)
    						.css("opacity", 1)
    						.addClass("active");
    					if (cbAfterShow) cbAfterShow();
    				});
    			}
    		}
    		if (this.buttons) {
    			this.buttons.find(".M_TabButton").each((ind, el) => {
    				$(el).removeClass("active");
    				if ($(el).attr("for") == jElId) $(el).addClass("active");
    			});
    		}
    	}
    };

    /**
     * A graphical tree
     * @class
      * @extends M_.Outlet
     * @property {String} autoLoad
     * @property {String} tpl
     */
    M_.Tree = class extends M_.Outlet {
    	constructor(opts) {
    		var defaults = {
    			autoLoad: false,
    			displayRootNode: true,
    			url: "",
    			cls: "M_Tree",
    			retractable: true,
    			movable: true,
    			rootNode: {
    				expended: true,
    				hidden: false,
    				label: "RootNode",
    				id: "rootnode",
    				draggable: false,
    				droppable: true,
    				nodes: []
    			}
    		};

    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    	/**
      	 * @return {type}
      	 */
    	create() {
    		this.jEl = $("<div class='" + this.cls + "'></div>");
    		this.container.append(this.jEl);
    		this._drawNodes();
    	}
    	/**
      	 * Draw the tree
      	 */
    	draw() {
    		this._drawNodes();
    	}
    	_drawNodes() {
    		var html = "";
    		html += "<ul>";
    		html += this._createChildNodes(this.rootNode);
    		html += "</ul>";
    		this.jEl.html(html);

    		this.jEl
    			.find("li:has(ul)")
    			.addClass("M_TreeParentLi")
    			.find(" > span"); //.attr('title', 'Collapse this branch')
    		if (this.retractable) {
    			this.jEl.find("li.M_TreeParentLi > span > i").on("click", evt => {
    				var target = $(evt.target);
    				var children = target.closest("li.M_TreeParentLi").find(" > ul > li");
    				if (children.is(":visible")) {
    					children.slideUp();
    					target
    						.find(" > i")
    						.addClass("fa-plus-square")
    						.removeClass("fa-minus-square"); //.attr('title', 'Expand this branch')
    				} else {
    					children.slideDown();
    					target
    						.find(" > i")
    						.addClass("fa-minus-square")
    						.removeClass("fa-plus-square"); //.attr('title', 'Collapse this branch')
    				}
    				evt.stopPropagation();
    			});
    		}
    		this.jEl.find("li > span").on("click", evt => {
    			var nodeId = $(evt.target)
    				.closest("li")
    				.attr("m_id");
    			this.setSelected(nodeId);

    			var node = this.getNode(nodeId);
    			if (node.clickable !== false) this.trigger("nodeclick", this, nodeId, node);
    		});
    		this._dragMID = null;
    		if (this.movable) {
    			this.jEl
    				.find("li.draggable > span")
    				.attr("draggable", true)
    				.on("drag", (evt, evt2) => {
    					// console.log("drag",evt);
    					this._dragMID = $(evt.target)
    						.closest("li")
    						.attr("m_id");
    				});
    			this.jEl
    				.find("li.droppable > span")
    				.on("dragover", evt => {
    					// console.log("dragover",evt);
    					var nodeDrop = $(evt.target)
    						.closest("li")
    						.attr("m_id");
    					if (nodeDrop != this._dragMID) evt.preventDefault();
    				})
    				.on("drop", evt => {
    					// console.log("drop",evt);
    					evt.preventDefault();
    					var mid = this._dragMID;
    					var nodeDrop = $(evt.target)
    						.closest("li")
    						.attr("m_id");
    					console.log("this.isNodeParentOf(mid, nodeDrop)", this.isNodeParentOf(mid, nodeDrop));
    					if (mid != nodeDrop && !this.isNodeParentOf(mid, nodeDrop)) {
    						let ok = this.trigger("beforenodemove", this, mid, nodeDrop);
    						if (ok === false) return;
    						this.moveNode(mid, nodeDrop);
    					}
    				});
    		}
    	}
    	_createChildNodes(parent) {
    		var html = "";
    		if (!parent.id) parent.id = M_.Utils.id();
    		let myclass = "";
    		if (parent.draggable) myclass += " draggable";
    		if (parent.droppable) myclass += " droppable";
    		html += "<li m_id='" + parent.id + "' class='" + myclass + "'>";
    		let fa = "";
    		if (parent.nodes && parent.nodes.length) fa = "fa fa-minus-square";
    		html += "<span><i class='" + fa + "'></i><div class='txt'>" + parent.label + "</div></span>";
    		if (parent.nodes && parent.nodes.length) {
    			html += "<ul>";
    			_.each(parent.nodes, (node, index) => {
    				html += this._createChildNodes(node);
    			});
    			html += "</ul>";
    		}
    		html += "</li>";
    		return html;
    	}
    	_iter(parent, nodeId, action) {
    		// if (action=='path') this._nodepath.push(parent.id) ;
    		if (nodeId == "rootnode") {
    			if (action == "get") {
    				this._nodeparent = null;
    				this._resNodeId = parent;
    			}
    			if (action == "add") {
    				if (!parent.nodes) parent.nodes = [];
    				parent.nodes.push(this._nodeToAdd);
    			}
    			return;
    		}
    		if (parent.nodes && parent.nodes.length) {
    			_.each(parent.nodes, (node, index) => {
    				if (!node) return;
    				if (node.id == nodeId) {
    					if (action == "remove") parent.nodes.splice(index, 1);
    					if (action == "get") {
    						this._nodeparent = parent;
    						this._resNodeId = node;
    					}
    					if (action == "add") {
    						if (!node.nodes) node.nodes = [];
    						node.nodes.push(this._nodeToAdd);
    					}
    				}
    				this._iter(node, nodeId, action);
    			});
    		}
    		// return '' ;
    	}
    	/**
      	 * @param {NodeObject} node
      	 * @param {String} parentId
      	 */
    	addNode(node, parentId, silently) {
    		this._nodeToAdd = node;
    		this._iter(this.rootNode, parentId, "add");
    		this._drawNodes();
    		if (!silently) this.trigger("nodeadd", this, node, parentId);
    	}
    	/**
      	 * @param  {String}	nodeId
      	 * @param  {String}	parentId
      	 */
    	moveNode(nodeId, parentId, silently) {
    		// log("nodeId, parentId",nodeId, parentId)
    		var node = this.getNode(nodeId);
    		this.removeNode(nodeId, true);
    		this.addNode(node, parentId, true);
    		this._drawNodes();
    		if (!silently) this.trigger("nodemove", this, nodeId, parentId);
    	}
    	/**
      	 * @param  {type}
      	 * @return {Array}
      	 */
    	getNodePath(nodeId, names) {
    		// console.log("getNodePath",nodeId);
    		var nodepath = [];
    		let node = this.getNode(nodeId);
    		if (!node) return nodepath;
    		if (names) nodepath.push(node.label);
    		else nodepath.push(node.id);
    		// this._iter(this.rootNode, nodeId, 'path') ;
    		var nodeIdStart = nodeId;
    		if (nodeId == "rootnode") return nodepath;
    		for (var i = 0; i < 100; i++) {
    			var parentNode = this.getNodeParent(nodeIdStart);
    			if (names) {
    				nodepath.unshift(parentNode.label);
    			} else {
    				nodepath.unshift(parentNode.id);
    			}
    			nodeIdStart = parentNode.id;
    			if (parentNode.id == "rootnode") break;
    		}
    		return nodepath;
    	}
    	getNodeParent(nodeId) {
    		this._nodeparent = null;
    		this._iter(this.rootNode, nodeId, "get");
    		// return M_.Utils.clone(this._nodeparent) ;
    		return this._nodeparent;
    	}
    	isNodeChildOf(nodeId, childOfNodeId) {
    		let path1 = this.getNodePath(nodeId);
    		let path2 = this.getNodePath(childOfNodeId);
    	}
    	isNodeParentOf(nodeId, childOfNodeId) {
    		let path1 = this.getNodePath(nodeId);
    		let path2 = this.getNodePath(childOfNodeId);
    		// 	console.log("path1,path2",path1,path2);
    		let dif = [];
    		let ok = false;
    		_.each(path2, (p2, i2) => {
    			if (!path1[i2]) {
    				ok = true;
    				return false;
    			}
    			if (p2 == path1[i2]) return;
    			if (p2 != path1[i2]) return false;
    		});
    		return ok;
    	}
    	/**
      	 * @param  {String}	nodeId
      	 */
    	removeNode(nodeId, silently) {
    		// this.jEl.find("li[m_id='"++"']").remove() ;
    		this._iter(this.rootNode, nodeId, "remove");
    		this._drawNodes();
    		if (!silently) this.trigger("noderemove", this, nodeId);
    	}
    	/**
      	 * @param  {String}	nodeId
      	 * @return {NodeObject}
      	 */
    	getNode(nodeId) {
    		this._resNodeId = null;
    		this._iter(this.rootNode, nodeId, "get");
    		// return M_.Utils.clone(this._resNodeId) ;
    		return this._resNodeId;
    	}
    	/**
      	 * @param {NodeObject} rootNode
      	 */
    	setRootNode(rootNode) {
    		this.rootNode = rootNode;
    		this._drawNodes();
    	}
    	getSelected() {
    		let sel = "rootnode";
    		if (this.jEl.find("span.selected").closest("li").length)
    			sel = this.jEl
    				.find("span.selected")
    				.closest("li")
    				.attr("m_id");
    		return sel;
    	}
    	setSelected(nodeId) {
    		// console.log("nodeId",nodeId);
    		this.jEl.find("span.selected").removeClass("selected");
    		this.jEl.find('li[m_id="' + nodeId + '"] > span').addClass("selected");
    	}
    	_listenDocClick(evt) {
    		// console.log("_listenDocClick", this._currentRenameId, $(evt.target).closest('li'));
    		if (
    			$(evt.target)
    				.closest("li")
    				.attr("m_id") == this._currentRenameId
    		)
    			return;
    		this.jEl.find('li[m_id="' + this._currentRenameId + '"] > span > div').attr("contenteditable", false);
    		$(document).off("click", $.proxy(this._listenDocClick, this));
    		let label = this.jEl.find('li[m_id="' + this._currentRenameId + '"] > span > div').text();
    		this.trigger("noderename", this, this._currentRenameId, label);
    	}
    	renameNode(nodeId) {
    		this._currentRenameId = nodeId;
    		let el = this.jEl.find('li[m_id="' + nodeId + '"] > span > div');
    		el.attr("contenteditable", true);
    		var range = document.createRange();
    		range.selectNodeContents(el.get(0));
    		var txtsel = window.getSelection();
    		txtsel.removeAllRanges();
    		txtsel.addRange(range);
    		window.setTimeout(() => {
    			$(document).on("click", $.proxy(this._listenDocClick, this));
    		}, 200);
    	}
    };

    /**
     * A graphical list to implement
     * @class
      * @extends M_.Outlet
     * @implements M_.Stored
     * @property {String} autoLoad
     * @property {String} tpl
     */
    M_.List = class extends M_.Outlet {
    	constructor(opts) {
    		var defaults = {
    			autoLoad: false,
    			tpl: ""
    		};

    		// if (!opts.jEl && !opts.colsDef) log("jEl is mandatory in M_.List") ;
    		// if (!opts.tpl && !opts.colsDef) log("tpl is mandatory in M_.List") ;
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		// $.extend(true, this.prototype, M_.Stored) ;
    		_.mixin(this, M_.Stored);
    		// log("store", Object.mixin)
    		this.initStored();

    		//this.render();
    	}
    	/**
    	 * @return {type}
    	 */
    	updateStore() {
    		this.render();
    	}
    	/**
    	 * Redefine this method to render your UI
    	 */
    	render() {}
    };

    // M_.Draggable = class {
    // 	constructor(el, jElHandle=null, jElHelper=null) {
    // 		//tabDraggable.push({el:el, jElHandle:jElHandle}) ;
    // 		jElHandle.mousedown({el:el}, $.proxy(function(evt) {
    // 			this.dragEl = evt.data.el ;
    // 			this.dragStart = true ;
    // 			this.dragDiff = {top: evt.pageY-this.dragEl.offset().top, left: evt.pageX-this.dragEl.offset().left}
    // 		}, this)) ;
    // 		$(document).mouseup({el:el}, $.proxy(function() {
    // 			this.dragStart = false ;
    // 		}, this)) ;
    // 		$(document).mousemove({el:el}, $.proxy(function(evt) {
    // 			if (this.dragStart && this.dragEl == evt.data.el) {
    // 				var top = evt.pageY ;
    // 				var left = evt.pageX ;
    // 				this.dragEl.offset({top: top-this.dragDiff.top, left: left-this.dragDiff.left}) ;
    // 			}
    // 		}, this)) ;
    // 	}

    /**
     * A simple list "à la Apple"
     * @class
      * @extends M_.List
     * @property {type} classItems
     * @property {type} itemValue
     * @property {type} itemKey
     * @property {type} selectionVisible
     * @property {type} multipleSelection
     * @property {type} currentSelection
     * @property {type} dynamic
     * @property {type} lineHeight
     * @property {type} loadLimit
     * @property {type} itemsDraggableTo
     */
    M_.SimpleList = class extends M_.List {
    	constructor(opts) {
    		var defaults = {
    			classItems: "M_SimpleListItem",
    			itemValue: "",
    			itemKey: "",
    			selectionVisible: false,
    			multipleSelection: false,
    			currentSelection: [],
    			dynamic: false,
    			lineHeight: 34,
    			loadLimit: 400,
    			oddEven: true,
    			// _startPosition: 0,
    			itemsDraggableTo: false,
    			_dragStarted: false,
    			_lastSelection: null
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);

    		// this.setSelection(this.currentSelection) ;
    		// this.create() ;
    	}
    	/**
    	 * @return {type}
    	 */
    	updateStore() {
    		super.updateStore();
    		this.setSelection(this.currentSelection);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		var html = "";
    		html += `<div class='M_SimpleList'>
    					<div class='M_SimpleListContent'>
    						<div class='M_SimpleListContentFake1'></div>
    						<div class='M_SimpleListContentReal'></div>
    						<div class='M_SimpleListContentFake2'></div>
    					</div>
    				</div>`;
    		this.jEl = $(html);
    		this.container.append(this.jEl);
    		if (!this.dynamic) this.jEl.find(".M_SimpleListContent").css("background-image", "none");

    		if (this.dynamic) {
    			this.jEl.scroll(evt => {
    				M_.Utils.delay(
    					() => {
    						var diff = this.jEl.offset().top - this.jEl.find(".M_SimpleListContent").offset().top;
    						var _skipPosition = Math.ceil(diff / this.lineHeight) - Math.ceil(this.loadLimit / 4);
    						if (_skipPosition < 0) _skipPosition = 0;
    						this.store.reload(false, { skip: _skipPosition, limit: this.loadLimit });
    					},
    					100,
    					"m_simplelist_scroll"
    				);
    			});
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	render() {
    		// log("render")
    		var html = "",
    			mid = this.store.primaryKey;
    		this.store.each((model, indexTemp) => {
    			// log("cg_created",model.get('cg_name'))
    			var val = "";
    			if ($.isFunction(this.itemValue)) val = this.itemValue(model);
    			else val = model.get(this.itemValue);
    			var clsTr = this.classItems;
    			if (this.oddEven) {
    				if (indexTemp % 2 === 0) clsTr += " M_TableListOdd";
    				else clsTr += " M_TableListEven";
    			}
    			html += "<div class='" + clsTr + " M_Noselectable' data-m_id='" + model.get(mid) + "'>" + val + "</div>";
    		});
    		this.jEl.find(".M_SimpleListContentReal").html(html);

    		// this.jEl.find('.'+this.classItems).m_draggable() ;

    		// $("."+this.classItems).draggable({
    		// 	cursor: "move",
    		// 	cursorAt: { top: -12, left: -20 },
    		// 	helper: function( event ) {
    		// 		return $( "<div class='ui-widget-header' style='z-index:100000;'>I'm a custom helper</div>" );
    		// 	}
    		// });

    		if (this.dynamic) {
    			var totalHeight = this.lineHeight * this.store.countTotal();
    			this.jEl.find(".M_SimpleListContent").height(totalHeight);
    			this.jEl.find(".M_SimpleListContentFake1").height(this.store.skip * this.lineHeight);
    		}
    		// this.jEl.find('.M_SimpleListContentFake2').height(totalHeight - ((this._skipPosition+100)*this.lineHeight)) ;

    		this.jEl.find("." + this.classItems).on("mousedown", evt => {
    			// log("this.currentSelection",this.currentSelection)
    			var item = $(evt.target).closest("." + this.classItems);
    			var m_id = item.attr("data-m_id");
    			var model = this.store.getRow(m_id);

    			if (this.trigger("beforeClickItem", this, m_id, model, evt) === false) return;

    			var ok = true,
    				dontremoveselection = false;
    			if (this.multipleSelection) {
    				if ((M_.Utils.isMacos && evt.metaKey) || evt.ctrlKey) {
    					// evt.preventDefault() ;
    					// evt.stopPropagation() ;
    					if (item.hasClass("selected")) ok = false;
    				} else if (evt.shiftKey) {
    					dontremoveselection = true;
    					var lastIndex = this.store.getIndexById(this._lastSelection);
    					var currentIndex = this.store.getIndexById(m_id);
    					var min = Math.min(lastIndex, currentIndex);
    					var max = Math.max(lastIndex, currentIndex);
    					// var r =
    					this.store.each((row, i) => {
    						if (i >= min && i <= max) {
    							var m_idTemp = row.getId();
    							this.jEl.find("." + this.classItems + '[data-m_id="' + m_idTemp + '"]').addClass("selected");
    							if (this.currentSelection.indexOf(m_idTemp) == -1) this.currentSelection.push(m_idTemp);
    						}
    					});
    				} else {
    					if (!item.hasClass("selected")) {
    						this.jEl.find("." + this.classItems).removeClass("selected");
    						this.currentSelection = [];
    					} else {
    						dontremoveselection = true;
    						ok = false;
    					}
    				}
    			} else {
    				this.jEl.find("." + this.classItems).removeClass("selected");
    				this.currentSelection = [];
    			}
    			if (ok) {
    				item.addClass("selected");
    				this.trigger("clickItem", this, m_id, model, evt);
    				this._lastSelection = m_id;
    				if (this.currentSelection.indexOf(m_id) == -1) this.currentSelection.push(m_id);
    			} else {
    				if (!dontremoveselection) {
    					item.removeClass("selected");
    					this.currentSelection.m_remove(m_id);
    				}
    			}
    			// log("this.currentSelection",this.currentSelection)

    			if (this.itemsDraggableTo) {
    				// this.jEl.on('mousedown', (evt)=> {
    				// 	// evt.preventDefault() ;
    				// 	// evt.stopPropagation() ;
    				$(document).on("mousemove", $.proxy(this.moveViewable, this));
    				// }) ;
    				$(document).on("mouseup", evt => {
    					// log("this.viewable",this.viewable)
    					if (this.viewable) this.viewable.remove();
    					$(document).off("mousemove", $.proxy(this.moveViewable, this));
    					$(document).off("mouseenter", this.itemsDraggableTo, $.proxy(this.enterViewable, this));
    					$(document).off("mouseleave", this.itemsDraggableTo, $.proxy(this.leaveViewable, this));
    					var elTemp = $(this.itemsDraggableTo + ".over");
    					if (elTemp.length) {
    						this.trigger("droped", elTemp, elTemp.attr("data-m_id"), this.getSelection());
    						$(this.itemsDraggableTo).removeClass("over");
    					}
    					this._dragStarted = false;
    				});
    				$(document).on("mouseenter", this.itemsDraggableTo, $.proxy(this.enterViewable, this));
    				$(document).on("mouseleave", this.itemsDraggableTo, $.proxy(this.leaveViewable, this));
    				//  (evt)=> {
    				// 	log("evt.target", $(evt.target))
    				// }) ;
    			}
    		});

    		// $( "#contacts_list_contacts .M_SimpleListContentReal" ).selectable();
    		// this.jEl.find('.M_SimpleListContentReal').selectable();
    		// this.jEl.find('.M_SimpleListContentReal').draggable();
    		this.setSelection(this.currentSelection);
    		this.trigger("render", this);
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	leaveViewable(evt) {
    		$(evt.target).removeClass("over");
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	enterViewable(evt) {
    		// log("evt.target", $(evt.target))
    		// $(this.itemsDraggableTo).removeClass('over') ;
    		$(evt.target).addClass("over");
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	moveViewable(evt) {
    		evt.preventDefault();
    		// log("this._dragStarted",this._dragStarted)
    		if (!this._dragStarted) {
    			var sel = this.getSelection();
    			this.viewable = $(
    				"<div style='width:20px; height:20px; position:relative; z-index:100000;'><div class='M_Badge' style='right:-30px;'>" +
    					sel.length +
    					"</div><span class='fa fa-group' style='font-size:30px;'></span></div>"
    			);
    			$("body").append(this.viewable);
    		}
    		this.viewable.offset({ top: evt.pageY, left: evt.pageX + 30 });
    		this._dragStarted = true;
    	}
    	/**
    	 * @param {type}
    	 */
    	setSelection(m_id) {
    		// log("setSelection",m_id)
    		// if (!m_id) return ;
    		if (!$.isArray(m_id)) m_id = [m_id];
    		if (!this.multipleSelection) this.jEl.find("." + this.classItems).removeClass("selected");
    		var goodItem = [];
    		for (var i = 0; i < m_id.length; i++) {
    			// log("m_id[i]",m_id[i])
    			goodItem = this.jEl.find("." + this.classItems + '[data-m_id="' + m_id[i] + '"]');
    			goodItem.addClass("selected");
    		}
    		// this.currentSelection = m_id ;
    		if (this.selectionVisible && goodItem.length) {
    			var posYRow = goodItem.position().top;
    			var posYParent = goodItem.parent().position().top;
    			// log(posYRow, posYParent)
    			var goodContainer = goodItem.closest(".M_SimpleList");
    			// log('goodContainer.scrollTop()',goodContainer.scrollTop())
    			if (
    				1 * (posYRow - posYParent) < goodContainer.scrollTop() ||
    				1 * (posYRow - posYParent) > goodContainer.scrollTop() + goodContainer.height() - goodItem.height() - 30
    			) {
    				goodItem.closest(".M_SimpleList").animate(
    					{
    						scrollTop: 1 * (posYRow - posYParent) - goodContainer.height() / 3
    					},
    					"slow"
    				);
    			}
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	getSelection() {
    		var tabSelection = [];
    		// log("this.currentSelection",this.currentSelection)
    		for (var i = 0; i < this.currentSelection.length; i++) {
    			var model = this.store.getRow(this.currentSelection[i]);
    			tabSelection.push(model);
    		}
    		return tabSelection;
    	}
    };

    /**
     * A multi columns list, a table
     * @class
      * @extends M_.SimpleList
     * @property {type} colsDef
     * @property {type} classHeader
     * @property {type} classItems
     * @property {type} styleTable
     * @property {type} fullHeight
     * @property {type} oddEven
     * @property {type} limitRows
     * @property {type} moreText
     * @property {type} lessText
     * @property {type} withMouseOverRaw
     */
    M_.TableList = class extends M_.SimpleList {
    	constructor(opts) {
    		var defaults = {
    			colsDef: null,
    			classHeader: "M_TableListHeader",
    			classItems: "M_TableListItem",
    			styleTable: "",
    			fullHeight: true,
    			limitRows: 0,
    			moreText: "%n lignes de plus",
    			lessText: "Masquer les lignes supplémentaires",
    			withMouseOverRaw: true,
    			headerHeight: 30,
    			_colsToHide: [],
    			draggableRows: false
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		var html = "",
    			cls = "";
    		if (this.fullHeight) cls += "M_FullHeight";
    		if (this.withMouseOverRaw) cls += " M_TableListOverRaw";
    		this._idMore = M_.Utils.id();

    		html += `<div class='M_TableList ${cls}' style='${this.styleTable}'>
    					<div>
    						<table cellpadding="0" cellspacing="0">
    							<thead></thead>
    							<tbody></tbody>
    						</table>
    					</div>
    					<div class="M_AlignRight"><a id="${this._idMore}" href='javascript:void(0);'>${this.getMoreText()}</a></div>
    				</div>`;
    		this.jEl = $(html);
    		this.container.append(this.jEl);
    		this.jEl.css("padding-top", this.headerHeight);

    		this._limitRows = true;
    		if (this.limitRows) {
    			$("#" + this._idMore)
    				.parent()
    				.show();
    			this._limitRows = true;
    		} else {
    			$("#" + this._idMore)
    				.parent()
    				.hide();
    			this._limitRows = false;
    		}
    		$("#" + this._idMore).click(() => {
    			// console.log("click");
    			this._limitRows = !this._limitRows;
    			this.render();
    		});
    	}
    	_setMoreLessText() {
    		if (this.limitRows) {
    			if (this._limitRows) {
    				$("#" + this._idMore).html(this.getMoreText());
    			} else {
    				$("#" + this._idMore).html(this.getLessText());
    			}
    			if (this._getNbRowsLimited() > 0) $("#" + this._idMore).show();
    			else $("#" + this._idMore).hide();
    		}
    	}
    	_getNbRowsLimited() {
    		return this.store.count() - this.limitRows;
    	}
    	/**
    	 * @return {type}
    	 */
    	getMoreText() {
    		var val = this.moreText;
    		val = val.replace(/%n/, this._getNbRowsLimited());
    		return val;
    	}
    	/**
    	 * @return {type}
    	 */
    	getLessText() {
    		var val = this.lessText;
    		val = val.replace(/%n/, this._getNbRowsLimited());
    		return val;
    	}
    	/**
    	 * @return {type}
    	 */
    	render() {
    		var html = "",
    			mid = this.store.primaryKey,
    			colsDef = this.colsDef;
    		html += "<tr>";
    		for (var i = 0; i < colsDef.length; i++) {
    			let cls = this.classHeader + " M_Noselectable";
    			let style = "";
    			if (colsDef[i].align && colsDef[i].align == "right") cls += " M_AlignRight";
    			if (colsDef[i].align && colsDef[i].align == "center") cls += " M_AlignCenter";
    			if (colsDef[i].align && colsDef[i].align == "left") cls += " M_AlignLeft";
    			if (!colsDef[i].align) cls += " M_AlignLeft";
    			if (colsDef[i].width) {
    				var w = "";
    				if ($.type(colsDef[i].width) === "string") w = colsDef[i].width;
    				else w = colsDef[i].width + "px";
    				style += "width:" + w + "; ";
    			}
    			html += "<th data-m-col='" + i + "' class='" + cls + "' style='" + style + "'>" + colsDef[i].label;
    			if (this.fullHeight) html += "<div style='" + style + "'>" + colsDef[i].label + "</div>";
    			html += "</th>";
    		}
    		html += "</tr>";
    		this.jEl
    			.find("thead")
    			.empty()
    			.html(html);

    		html = "";
    		this.store.each((model, indexTemp) => {
    			// log("model",model)
    			if (this.limitRows && indexTemp >= this.limitRows && this._limitRows) return true;
    			var clsTr = "";
    			if (this.oddEven) {
    				if (indexTemp % 2 === 0) clsTr += " M_TableListOdd";
    				else clsTr += " M_TableListEven";
    			}
    			var draggable = "";
    			if (this.draggableRows) draggable = "draggable='true'";
    			html += "<tr class='" + clsTr + "' " + draggable + ">";
    			for (var i = 0; i < colsDef.length; i++) {
    				let val = "",
    					cls = this.classItems + " M_Noselectable",
    					mystyle = { cls: "", style: "" };
    				if ($.isFunction(colsDef[i].val)) {
    					// console.log("model", model);
    					val = colsDef[i].val(model, mystyle);
    				} else {
    					if (model instanceof M_.Model) val = model.get(colsDef[i].val);
    					else val = model[colsDef[i].val];
    				}
    				if (colsDef[i].align && colsDef[i].align == "right") cls += " M_AlignRight";
    				if (colsDef[i].align && colsDef[i].align == "center") cls += " M_AlignCenter";
    				if (colsDef[i].align && colsDef[i].align == "left") cls += " M_AlignLeft";
    				cls += " " + mystyle.cls;
    				let datamid = "";
    				if (model instanceof M_.Model) datamid = model.get(mid);
    				else datamid = model[mid];
    				html += "<td class='" + cls + "' style='" + mystyle.style + "' data-m_id='" + datamid + "'>" + val + "</td>";
    			}
    			html += "</tr>";
    		});
    		this.jEl
    			.find("tbody")
    			.empty()
    			.html(html);

    		this.jEl.find("." + this.classHeader).on("click", evt => {
    			// console.log("evt", evt);
    			// this.store.sort()
    			var item = $(evt.target).closest("." + this.classHeader);
    			var numcol = item.attr("data-m-col");
    			var colDef = this.colsDef[numcol];
    			if (colDef) {
    				let direction = -1;
    				if (colDef._sortDirection) direction = colDef._sortDirection;
    				direction = direction * -1;
    				colDef._sortDirection = direction;
    				// console.log("colDef._sortDirection", colDef._sortDirection);

    				if (colDef.sort) this.store.sort(colDef.sort, direction);
    				else this.store.sort(colDef.val, direction);
    				// this.render() ;
    			}
    		});

    		this.jEl.find("." + this.classItems).on("click", evt => {
    			// log("dddddddddddddd")
    			var item = $(evt.target).closest("." + this.classItems);
    			var m_id = item.attr("data-m_id");
    			var model = this.store.getRow(m_id);

    			// get num col
    			var col = item.index();
    			var row = item.parent().index();

    			if (this.trigger("beforeClickItem", this, m_id, model, evt, col, row) === false) return;
    			// item.addClass('selected') ;
    			this.trigger("clickItem", this, m_id, model, evt, col, row);
    		});
    		// this.jEl.find("tbody tr").on('mouseover', (evt)=> {
    		// }) ;
    		// this.jEl.find("tbody tr").on('mouseout', (evt)=> {
    		// }) ;

    		this._setMoreLessText();

    		this.hideColumns(this._colsToHide);

    		this.trigger("render", this, this.jEl);
    	}
    	showAllColumns() {
    		this._colsToHide = [];
    		$("#" + this.id)
    			.find("tr td")
    			.show();
    	}
    	showColumns(cols) {
    		if (!$.isArray(cols)) cols = [cols];
    		this._colsToHide = _.difference(this._colsToHide, cols);
    		$("#" + this.id)
    			.find("tr td")
    			.show();
    		$("#" + this.id)
    			.find("tr th")
    			.show();
    		this.hideColumns(this._colsToHide);
    	}
    	hideColumns(cols) {
    		if (!$.isArray(cols)) cols = [cols];
    		this._colsToHide = this._colsToHide.concat(cols);
    		this._colsToHide = _.uniq(this._colsToHide);
    		// console.log("this._colsToHide", this._colsToHide);
    		_.each(this._colsToHide, col => {
    			$("#" + this.id)
    				.find("tr td:nth-child(" + col + ")")
    				.hide();
    			$("#" + this.id)
    				.find("tr th:nth-child(" + col + ")")
    				.hide();
    		});
    	}
    };

    /**
     * Implements this to get CRUD (get/post/put/delete) ajax requests
     * @interface
      */
    M_.CRUD = {
    	/**
    	 * @return {type}
    	 */
    	initCRUD() {
    		// var defaults = {
    		// 	url: "",
    		// 	args: {},
    		// 	model: null,
    		// 	useWebsocket: M_.App.useWebsocket
    		// };
    		// $.extend(this, defaults) ;
    	},
    	// onLoad: function(model) {},
    	// onSave: function(data) {},
    	// onDelete: function(data) {},
    	/**
    	 * @param  {type}
    	 * @param  {Object}
    	 * @param  {Function}
    	 * @return {type}
    	 */
    	load(id, args = {}, callback = null) {
    		this._callbackFormLoaded = callback;
    		//this.callbackLoadRow = callback ;
    		var okArgs = {};
    		//var okArgs = {action: 'get'} ;
    		//okArgs[this.primaryKey] = id ;
    		$.extend(okArgs, this.args, args);
    		if (this.onBeforeLoad) this.onBeforeLoad();
    		var obj = { url: this.url, args: okArgs };
    		if (this.trigger("beforeLoad", this, obj) === false) return false;
    		//this.lastLoadArgs = okArgs ;
    		var url = obj.url;
    		var method = "GET";
    		if (this.urls) {
    			let aaurl = this._analyseUrl(this.urls.findone);
    			method = aaurl.method;
    			url = aaurl.url;
    		}
    		// console.log("method, url",method, url);
    		if (id) url += "/" + id;
    		// alert("this.useWebsocket",this.useWebsocket)
    		if (this.useWebsocket) {
    			io.socket.get(url, okArgs, (data, jwres) => {
    				this._treatDataCrud(data, callback);
    			});
    		} else {
    			if (this.ajaxLoad) this.ajaxLoad.abort();
    			this.ajaxLoad = M_.Utils.ajaxJson(url, okArgs, method, data => {
    				this._treatDataCrud(data, callback);
    			});
    		}
    	},
    	_treatDataCrud(data, callback) {
    		// log("_treatDataCrud",data)
    		var row = null;
    		if (data && data[this.rootData]) row = data[this.rootData];
    		else row = data;
    		this.trigger("loadraw", this, row);
    		if (this.processData) this.processData(row);
    		var m;
    		if (this.model) {
    			m = new this.model({ row: row });
    		} else {
    			m = row;
    		}
    		this.dataLoaded = m;
    		if (this.onLoad) this.onLoad(m);
    		this.trigger("load", this, m);
    		if (callback) callback(m);
    	},
    	_analyseUrl(theurl) {
    		if (theurl.indexOf(" ") >= 0) {
    			var [method, url] = theurl.split(" ");
    			return { method: method, url: url };
    		} else return { method: "POST", url: theurl };
    	},
    	/**
    	 * @param  {type}
    	 * @param  {Object}
    	 * @param  {Function}
    	 * @return {type}
    	 */
    	save(modelOrData = null, args = {}, callback = null) {
    		this._callbackFormSaved = callback;
    		// if (this.validAndAlert()) {
    		// log("modelOrData",modelOrData)
    		this._isForm = false;
    		if (!modelOrData) {
    			modelOrData = this.getValues(true);
    			this._isForm = true;
    		}
    		// log("modelOrData", modelOrData)
    		var data = modelOrData;
    		var key;
    		if (data instanceof M_.Model) {
    			data = data.getArray();
    		}
    		// log("data",data)
    		// log("data", data)
    		for (key in data) {
    			// if (data[key] && data[key].toDate && data[key].toDate() instanceof Date) {
    			if (moment.isMoment(data[key])) {
    				data[key] = data[key].toISOString();
    			}
    		}
    		// log("data", data)

    		var okArgs = { action: "save" };
    		$.extend(okArgs, this.args, args, data);

    		// log("okArgs",okArgs);

    		/*var fields=this.fields ;
    			for(var j=0 ; j<fields.length ; j++) {
    				if (fields[j].type=='date') {
    					this.row[fields[j].name] = M_.Utils.formatDate(this.row[fields[j].name], 'Y-m-d H:i:s') ;
    				}
    			}
    			$.extend(okArgs, this.row) ;
    			$.extend(okArgs, this.args, args) ;
    			if (this.trigger("beforeLoad", this, {url: this.url, args:okArgs})===false) return false ;
    			this.lastLoadArgs = okArgs ;
    			*/
    		var url = this.url;
    		var method = "POST";
    		if (this.urls) {
    			let aaurl = this._analyseUrl(this.urls.create);
    			method = aaurl.method;
    			url = aaurl.url;
    		}
    		var moreUrl = "";
    		if (this.model) {
    			var modelTemp = new this.model({ row: {} });
    			if (!_.isEmpty(okArgs[modelTemp.primaryKey])) {
    				method = "PUT";
    				moreUrl = "/" + okArgs[modelTemp.primaryKey];
    				if (this.urls) {
    					let aaurl = this._analyseUrl(this.urls.update);
    					method = aaurl.method;
    					url = aaurl.url;
    				}
    			}
    			// log("this.model.primaryKey",modelTemp.primaryKey,okArgs)
    		}
    		// console.log("url,method",url,method,this.model,okArgs);

    		var obj = { url: url + moreUrl, method: method, args: okArgs };
    		if (this.trigger("beforeSave", this, obj) === false) return false;

    		// return ;
    		// var formData = new FormData() ;
    		// for(key in okArgs) {
    		// 	formData.append(key, okArgs[key]) ;
    		// }

    		M_.Utils.ajaxJson(obj.url, obj.args, obj.method, data => {
    			// log("dataaaaa",data)
    			if (data.error) {
    				let errTxt = "";
    				if (data.error == "E_VALIDATION") {
    					_.each(data.invalidAttributes, (attr, key) => {
    						errTxt += key + " : ";
    						_.each(attr, msg => {
    							errTxt += msg.message;
    						});
    						errTxt += "\n";
    					});
    				}
    				if (this.informValidReturnSails) this.informValidReturnSails(data);
    				this.trigger("error", this, data, errTxt);
    			} else {
    				this.trigger("save", this, data);
    				if (this._callbackFormSaved) this._callbackFormSaved(data);
    			}
    		});

    		// $.ajax({
    		// 	url: this.url+moreUrl,
    		// 	type: method,
    		// 	//headers: {Connection: close},
    		// 	data: JSON.stringify(okArgs),
    		// 	contentType: 'application/json',
    		// 	processData: false,
    		// 	dataType: 'json',
    		// 	cache: false,
    		// 	success: (data)=> {
    		// 		//&this.trigger("saved", data);
    		// 		this.trigger("save", this, data);
    		// 		if (this._callbackFormSaved) this._callbackFormSaved(data) ;
    		// 	}
    		// });
    		// }
    	},
    	/**
    	 * @param  {type}
    	 * @param  {Object}
    	 * @param  {Function}
    	 * @return {type}
    	 */
    	delete(id, args = {}, callback = null) {
    		this._callbackFormDelete = callback;
    		var okArgs = {};
    		$.extend(okArgs, this.args, args);

    		var url = this.url;
    		var method = "DELETE";
    		if (this.urls) {
    			let aaurl = this._analyseUrl(this.urls.destroy);
    			method = aaurl.method;
    			url = aaurl.url;
    		}

    		if (this.trigger("beforeDelete", this, { url: url, args: okArgs }) === false) return false;
    		M_.Utils.ajaxJson(url + "/" + id, okArgs, method, data => {
    			this.trigger("delete", this, data);
    			if (this._callbackFormDelete) this._callbackFormDelete(data);
    		});
    		// $.ajax({
    		// 	url: this.url+"/"+id,
    		// 	type: 'DELETE',
    		// 	//contentType: 'application/json',
    		// 	data: okArgs,
    		// 	dataType: 'json',
    		// 	success: (data)=> {
    		// 		this.trigger("delete", this, data);
    		// 		if (this._callbackFormDelete) this._callbackFormDelete(data) ;
    		// 	}
    		// });
    	}
    };

    /**
     * To do
     * @class
      * @extends M_.SimpleList
     * @implements M_.Observable
     * @property {type} jEl
     * @property {type} alignTo
     * @property {type} position
     * @property {type} height
     * @property {type} width
     * @property {type} speed
     * @property {type} visible
     * @property {type} floating
     */
    M_.Drawer = class {
    	constructor(opts) {
    		var defaults = {
    			jEl: null,
    			alignTo: null,
    			position: "bottom",
    			height: 45,
    			width: 45,
    			speed: 300,
    			visible: false,
    			floating: true
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		$(window).resize(() => {
    			if (this.tsResize) window.clearTimeout(this.tsResize);
    			this.tsResize = window.setTimeout(() => {
    				if (this.floating) this.showOrHide(this.visible, false);
    			}, 100);
    		});
    	}
    	/**
    	 * @return {Boolean}
    	 */
    	isVisible() {
    		return this.visible;
    	}
    	/**
    	 * @return {type}
    	 */
    	show() {
    		if (this.visible) return;
    		if (this.trigger("beforeShow", this) === false) return;
    		this.showOrHide(true);
    		this.trigger("show", this);
    	}
    	/**
    	 * @return {type}
    	 */
    	hide() {
    		if (!this.visible) return;
    		if (this.trigger("beforeHide", this) === false) return;
    		this.showOrHide(false);
    		this.trigger("hide", this);
    	}
    	/**
    	 * @param  {type}
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	showOrHide(ok, anim = true) {
    		var top, left, topStart, leftStart;
    		this.jEl.outerWidth(this.width);
    		this.jEl.outerHeight(this.height);

    		if (!this.floating && !ok) {
    			this.alignTo.height("100%");
    		}
    		this.jEl.css("position", "absolute");

    		if (this.position == "bottom") {
    			this.jEl.outerWidth(this.alignTo.outerWidth());
    			top = this.alignTo.offset().top + this.alignTo.outerHeight() - this.jEl.outerHeight();
    			left = this.alignTo.offset().left;
    			topStart = top + this.jEl.outerHeight();
    			leftStart = left;
    		}
    		if (this.position == "top") {
    			this.jEl.outerWidth(this.alignTo.outerWidth());
    			top = this.alignTo.offset().top;
    			left = this.alignTo.offset().left;
    			topStart = top - this.jEl.outerHeight();
    			leftStart = left;
    		}
    		if (this.position == "left") {
    			this.jEl.outerHeight(this.alignTo.outerHeight());
    			top = this.alignTo.offset().top;
    			left = this.alignTo.offset().left;
    			topStart = top;
    			leftStart = left - this.jEl.outerWidth();
    		}
    		if (this.position == "right") {
    			this.jEl.outerHeight(this.alignTo.outerHeight());
    			top = this.alignTo.offset().top;
    			left = this.alignTo.offset().left + this.alignTo.outerWidth() - this.jEl.outerWidth();
    			topStart = top;
    			leftStart = left - this.jEl.outerWidth();
    		}
    		if (anim) {
    			if (ok) {
    				this.jEl
    					.show()
    					.offset({ top: topStart, left: leftStart })
    					.transition({ top, left }, M_.Utils.getSpeedAnim(), () => {
    						// log("finis")
    					});
    			} else {
    				this.jEl.offset({ top, left }).transition({ top: topStart, left: leftStart }, M_.Utils.getSpeedAnim());
    			}
    			// if (ok) {
    			// 	this.jEl.addClass('M_TransitionNone')
    			// 			.show()
    			// 			.offset({top:topStart, left:leftStart}) ;
    			// 	this.jEl[0].offsetHeight;
    			// 	this.jEl.removeClass('M_TransitionNone')
    			// 			.offset({top, left}) ;
    			// 	this.jEl.on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd',()=> {
    			// 		this.jEl.off('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd') ;
    			// 	}) ;
    			// } else {
    			// 	this.jEl.addClass('M_TransitionNone')
    			// 			.offset({top, left}) ;
    			// 	this.jEl[0].offsetHeight;
    			// 	this.jEl.removeClass('M_TransitionNone')
    			// 			.offset({top:topStart, left:leftStart}) ;
    			// 	this.jEl.on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd',()=> {
    			// 		this.jEl.off('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd') ;
    			// 		this.jEl.hide() ;
    			// 	}) ;
    			// }
    			// if (ok) {
    			// 	this.jEl.offset({top:topStart, left:leftStart}).animate({
    			// 		top, left
    			// 	}, this.speed, ()=> {
    			// 		if (!this.floating) this.jEl.css('position', 'static') ;
    			// 	}) ;
    			// } else {
    			// 	this.jEl.offset({top, left}).animate({
    			// 		top:topStart, left:leftStart
    			// 	}, this.speed, ()=> {
    			// 	}) ;
    			// }
    		} else {
    			if (ok) this.jEl.offset({ top, left });
    			else this.jEl.offset({ top: topStart, left: leftStart });
    		}

    		if (!this.floating && ok) {
    			this.alignTo.height("calc(100% - " + this.height + "px)");
    		}

    		this.visible = ok;
    	}
    };

    /**
     * Display a background on html document
     * @class
      * @implements M_.Observable
     * @property {type} visible
     * @property {type} focusOn
     * @property {type} clickHide
     * @property {type} alpha
     * @property {type} zindex
     */
    M_.Modal = class {
    	constructor(opts) {
    		var defaults = {
    			visible: false,
    			focusOn: false,
    			clickHide: false,
    			alpha: 0.5,
    			zindex: 500,
    			isLoading: null,
    			text: ""
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		$(window).resize(() => {
    			if (this.tsResize) window.clearTimeout(this.tsResize);
    			this.tsResize = window.setTimeout(() => {
    				this.showOrHide(this.visible, false);
    			}, 100);
    		});

    		if (this.focusOn) {
    			this.jEl1 = $("<div class='M_Modal'></div>");
    			$("body").append(this.jEl1);
    			this.jEl2 = $("<div class='M_Modal'></div>");
    			$("body").append(this.jEl2);
    			this.jEl3 = $("<div class='M_Modal'></div>");
    			$("body").append(this.jEl3);
    			this.jEl4 = $("<div class='M_Modal'></div>");
    			$("body").append(this.jEl4);
    			if (this.clickHide) {
    				this.jEl1.click(() => {
    					this.hide();
    				});
    				this.jEl2.click(() => {
    					this.hide();
    				});
    				this.jEl3.click(() => {
    					this.hide();
    				});
    				this.jEl4.click(() => {
    					this.hide();
    				});
    			}
    		} else {
    			this.jEl = $("<div class='M_Modal'><p>" + this.text + "</p></div>");
    			$("body").append(this.jEl);
    			if (this.clickHide) {
    				this.jEl.click(() => {
    					this.hide();
    				});
    			}
    			if (this.isLoading) {
    				this.jEl.html(
    					"<div style='width:300px;margin:200px auto 0 auto; text-align:center;'><i style='font-size:100px;' class='fa fa-clock-o faa-spin animated'></i><br><h1 style='color:white;'>" +
    						this.isLoading +
    						"</h1></div>"
    				);
    			}
    		}
    	}
    	/**
    	 * @return {Boolean}
    	 */
    	isVisible() {
    		return this.visible;
    	}
    	/**
    	 * @return {type}
    	 */
    	show() {
    		if (this.trigger("beforeShow", this) === false) return false;
    		this.showOrHide(true);
    		this.trigger("show", this);
    	}
    	/**
    	 * @return {type}
    	 */
    	hide() {
    		if (this.trigger("beforeHide", this) === false) return false;
    		this.showOrHide(false);
    		this.trigger("hide", this);
    	}
    	/**
    	 * @param  {type}
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	showOrHide(ok, anim = false) {
    		var speed = M_.Utils.getSpeedAnim();
    		if (this.focusOn) {
    			if (ok) {
    				let posTop1 = this.focusOn.offset().top;
    				let posTop2 = this.focusOn.offset().top + this.focusOn.outerHeight();
    				let posTop3 = $(window).height();
    				let posLeft1 = this.focusOn.offset().left;
    				let posLeft2 = this.focusOn.offset().left + this.focusOn.outerWidth();
    				let posLeft3 = $(window).width();
    				this.jEl1
    					.css({
    						"z-index": this.zindex,
    						top: 0,
    						left: 0,
    						width: "100%",
    						height: posTop1
    					})
    					.transition({ opacity: this.alpha }, speed);
    				this.jEl2
    					.css({
    						"z-index": this.zindex,
    						top: posTop1,
    						left: 0,
    						width: posLeft1,
    						height: posTop2 - posTop1
    					})
    					.transition({ opacity: this.alpha }, speed);
    				this.jEl3
    					.css({
    						"z-index": this.zindex,
    						top: posTop1,
    						left: posLeft2,
    						width: posLeft3 - posLeft2,
    						height: posTop2 - posTop1
    					})
    					.transition({ opacity: this.alpha }, speed);
    				this.jEl4
    					.css({
    						"z-index": this.zindex,
    						top: posTop2,
    						left: 0,
    						width: "100%",
    						height: posTop3 - posTop2
    					})
    					.transition({ opacity: this.alpha }, speed);
    			} else {
    				this.jEl1.css({ "z-index": this.zindex }).transition({ opacity: 0 }, speed, () => {
    					this.jEl1.remove();
    					this.jEl2.remove();
    					this.jEl3.remove();
    					this.jEl4.remove();
    				});
    				this.jEl2.css({ "z-index": this.zindex }).transition({ opacity: 0 }, speed);
    				this.jEl3.css({ "z-index": this.zindex }).transition({ opacity: 0 }, speed);
    				this.jEl4.css({ "z-index": this.zindex }).transition({ opacity: 0 }, speed);
    			}
    		} else {
    			if (ok)
    				this.jEl
    					.css({ "z-index": this.zindex })
    					.show()
    					.transition({ opacity: this.alpha }, speed);
    			else
    				this.jEl.transition({ opacity: 0 }, speed, () => {
    					this.jEl.remove();
    				});
    		}
    		this.visible = ok;
    	}
    };

    /**
     * Display a window from a template
     * @class
      * @implements M_.Observable
     * @property {type} visible
     * @property {type} clickHide
     * @property {type} alpha
     * @property {type} zindex
     * @property {type} width
     * @property {type} maxWidth
     * @property {type} height
     * @property {type} html
     * @property {type} tpl
     * @property {type} tplData
     * @property {type} tplPartials
     * @property {type} cls
     * @property {type} modal
     * @property {type} animFrom
     * @property {type} top
     * @property {type} left
     * @property {type} position
     */
    M_.Window = class {
    	constructor(opts) {
    		var defaults = {
    			visible: false,
    			clickHide: false,
    			alpha: 0.5,
    			zindex: M_.Utils.getNextZIndex(),
    			width: 400,
    			maxWidth: 0,
    			height: "auto",
    			minHeight: false,
    			html: null,
    			tpl: null,
    			tplData: {},
    			tplPartials: {},
    			cls: "",
    			modal: true,
    			animFrom: null,
    			top: 100,
    			left: 0,
    			position: "center",
    			offsetPositionRight: 20,
    			offsetPositionLeft: 20,
    			offsetPositionTop: 20,
    			offsetPositionBottom: 20
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		$(window).resize(() => {
    			if (this.tsResize) window.clearTimeout(this.tsResize);
    			this.tsResize = window.setTimeout(() => {
    				// this.showOrHide(this.visible, false) ;
    			}, 100);
    		});

    		this.create();
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		this.jEl = $("<div class='M_Window'></div>").hide();
    		$("body").append(this.jEl);

    		// if (this.width=='auto') this.width = '100%' ;

    		if (this.minHeight)
    			this.jEl.css({
    				"min-height": this.minHeight
    			});

    		this.jEl.css({
    			"z-index": this.zindex,
    			width: this.width,
    			height: this.height
    		});

    		if (this.tpl) {
    			this.jEl.html(M_.App.renderMustache(this.tpl, this.tplData, this.tplPartials));
    		} else if (this.html) {
    			this.jEl.html(this.html);
    		}
    	}

    	/**
    	 * Search a class or selector in the container, like $.find()
    	 * @param  {string} selector
    	 * @return {jQuery}
    	 */
    	findEl(selector) {
    		return this.jEl.find(selector);
    	}

    	/**
    	 * @return {Boolean}
    	 */
    	isVisible() {
    		return this.visible;
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	show(anim = true) {
    		if (this.trigger("beforeShow", this) === false) return false;
    		this.showOrHide(true, anim);
    		this.trigger("show", this);
    		M_.Window._lastWindow = this;
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	hide(anim = true) {
    		if (this.trigger("beforeHide", this) === false) return false;
    		this.showOrHide(false, anim);
    		this.trigger("hide", this);
    		M_.Window._lastWindow = null;
    	}
    	maximize() {
    		this.jEl.height($(window).height() - 150);
    		var l = ($(window).width() - this.jEl.outerWidth()) / 2;
    		var t = 50;
    		this.jEl.css({
    			left: l,
    			top: t
    		});
    	}
    	/**
    	 * @return {type}
    	 */
    	center() {
    		this.jEl.height("auto");
    		var l = ($(window).width() - this.jEl.outerWidth()) / 2;
    		var t = ($(window).height() - this.jEl.outerHeight()) / 2;
    		// console.log("$(window).height()",$(window).height()-200,this.jEl.outerHeight());
    		if (this.jEl.outerHeight() > $(window).height() - 150) {
    			this.jEl.height($(window).height() - 150);
    			// console.log("setheigt",$(window).height()-200);
    			// t = ($(window).height()-this.jEl.height()-200)/2 ;
    			t = 50;
    		}
    		this.jEl.css({
    			left: l,
    			top: t
    		});
    		return this;
    	}
    	/**
    	 * @param  {type}
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	showOrHide(ok, anim = true) {
    		// log("showOrHide")
    		var speed = M_.Utils.getSpeedAnim();
    		if (ok) {
    			this.zindex = M_.Utils.getNextZIndex();
    			if (this.modal && !this.modalObj) {
    				this.modalObj = new M_.Modal({
    					clickHide: this.clickHide,
    					alpha: this.alpha,
    					zindex: this.zindex - 1,
    					listeners: [
    						[
    							"beforeHide",
    							() => {
    								if (this.trigger("beforeHide", this) === false) return false;
    							}
    						]
    					]
    				});
    				this.modalObj.show();
    			}
    			// this.jEl.height(this.height) ;
    			var l1 = this.left,
    				l2 = this.left,
    				t1 = this.top,
    				t2 = this.top,
    				w1 = this.width,
    				w2 = this.width,
    				h1 = this.height,
    				h2 = this.height,
    				s1 = 0.5,
    				s2 = 1,
    				o1 = 0,
    				o2 = 1;

    			// if (animFrom) {
    			// 	t1 = animFrom.offset().top - $(window).scrollTop() ;
    			// 	l1 = animFrom.offset().left ;
    			// 	// w1 = animFrom.width() ;
    			// 	// h1 = animFrom.height() ;
    			// 	log("t,l",t1,l1, animFrom)
    			// }

    			this.jEl
    				.addClass(this.cls)
    				.css({
    					scale: 1,
    					opacity: 0,
    					"z-index": this.zindex,
    					left: 0,
    					top: 0,
    					width: w1,
    					height: h1
    				})
    				.show();
    			// log("this.jEl.height()",this.jEl.height(), $(window).height())

    			if (this.maxWidth !== 0) {
    				// w1 = w2 = this.maxWidth ;
    				this.jEl.css({
    					"max-width": this.maxWidth
    				});
    			}
    			if (this.jEl.outerHeight() + 100 > $(window).height()) {
    				h1 = h2 = $(window).height() - 100;
    			}
    			if (this.position == "center" || this.position == "top") {
    				l1 = l2 = ($(window).width() - this.jEl.outerWidth()) / 2;
    				if (h2 == "auto") t1 = t2 = ($(window).height() - this.jEl.height()) / 2;
    				else t1 = t2 = ($(window).height() - h2) / 2;
    			}
    			if (this.position == "top") {
    				s1 = 1;
    				o1 = 1;
    				if (h2 == "auto") {
    					t1 = -1 * this.jEl.height() - this.offsetPositionTop;
    				} else {
    					t1 = h2 - 30;
    				}
    				t2 = this.offsetPositionTop;
    			}
    			this.jEl
    				.addClass(this.cls)
    				.css({ scale: s1, opacity: o1, left: l1, top: t1, width: w1, height: h1 })
    				.show()
    				.transition({ scale: s2, opacity: o2, left: l2, top: t2, width: w2 + 1, height: h2 }, speed, () => {
    					// this.jEl.outerWidth(this.width+1);
    				});
    		} else {
    			this.jEl.transition({ scale: 0.5, opacity: 0 }, speed, () => {
    				this.jEl.hide();
    			});
    			if (this.modalObj) {
    				this.modalObj.hide();
    				this.modalObj = null;
    			}
    		}
    		this.visible = ok;
    	}
    	static _escapeButtonListener() {
    		M_.Window._lastWindow = null;
    		$(document).keydown(evt => {
    			if (evt.keyCode == 27) {
    				if (M_.Window._lastWindow) {
    					M_.Window._lastWindow.hide();
    				}
    			}
    		});
    	}
    };
    M_.Window._escapeButtonListener();

    /**
     * Display a button
     * @class
      * @extends M_.Outlet
     * @property {type} text
     * @property {type} cls
     * @property {type} handler
     */
    M_.Button = class extends M_.Outlet {
    	constructor(opts) {
    		var defaults = {
    			text: "Save",
    			cls: "primary",
    			handler: null
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		var html = `<button class="M_Button ${this.cls}" id="${this.id}">${this.text}</button>`;
    		this.jEl = $(html);
    		this.container.append(this.jEl);
    		if (this.handler) this.jEl.click(this.handler);
    	}
    };

    /**
     * Display a alert dialog window or a confirm dialog window
      * @property {type} textButtonOk
     * @property {type} textButtonCancel
     */
    M_.Dialog = new class {
    	constructor(opts) {
    		this.defaults = {
    			textButtonOk: "OK",
    			textButtonCancel: "Annuler"
    		};
    		opts = opts ? opts : {};
    		$.extend(this, this.defaults, opts);
    	}
    	_createTemplate(title, text, icon = "fa-warning") {
    		var html = `<div style="overflow:auto; height:90%;">
    						<h1 style="line-height:22px;">${title}</h1>
    						<div>
    							<div class='M_FloatLeft' style='width:25%'><span class='fa ${icon} M_IconBig'></span></div>
    							<div class='M_FloatLeft' style='width:75%'>${text}</div>
    							<div class='M_Clear'></div>
    						</div>
    					</div>`;
    		return html;
    	}
    	/**
    	 * @param  {type}
    	 * @param  {type}
    	 * @param  {type}
    	 * @return {type}
    	 */
    	alert(title, text, callbackOk) {
    		// this.callbackOk = callbackOk ;
    		var html = this._createTemplate(title, text);
    		html += `<div class="M_margintop">
    						<div class='M_FloatRight'>
    							<button type="button" class='M_DialogOK M_Button primary'>${this.textButtonOk}</button>
    						</div>
    						<div class='M_Clear'></div>
    					</div>`;
    		var win = new M_.Window({
    			html: html
    		});
    		win.show();
    		win.jEl.find(".M_DialogOK").click(() => {
    			if (callbackOk) callbackOk();
    			win.hide();
    		});
    	}
    	/**
    	 * @param  {type}
    	 * @param  {type}
    	 * @param  {type}
    	 * @param  {type}
    	 * @return {type}
    	 */
    	confirm(title, text, callbackOk, callbackCancel) {
    		// this.callbackOk = callbackOk ;
    		// this.callbackCancel = callbackCancel ;
    		var html = this._createTemplate(title, text);
    		html += `<div class="M_margintop">
    						<div class='M_FloatRight'>
    							<button type="button" class='M_DialogOK M_Button primary'>${this.textButtonOk}</button>
    						</div>
    						<div class='M_FloatRight'>
    							<button type="button" class='M_DialogCancel M_ModalCancel M_Button'>${this.textButtonCancel}</button>
    						</div>
    						<div class='M_Clear'></div>
    					</div>`;
    		var winConfirm = new M_.Window({
    			html: html
    		});
    		winConfirm.show();
    		winConfirm.jEl.find(".M_DialogOK").click(() => {
    			winConfirm.hide();
    			if (callbackOk) callbackOk.apply(this);
    		});
    		winConfirm.jEl.find(".M_DialogCancel").click(() => {
    			winConfirm.hide();
    			if (callbackCancel) callbackCancel.apply(this);
    		});
    	}
    	hide() {
    		this.win.hide();
    	}
    	/**
    	 * alert user
    	 * @param  {String} text
    	 * @param  {String} position
    	 * @param  {Number} time      number of milliseconds
    	 * @param  {Function} callbackClose
    	 */
    	notify(text, time = 2000, position = "top", callbackClose = null) {
    		var html = "";
    		html += `<div class="">
    							${text}
    						</div>`;
    		var win = new M_.Window({
    			html: html,
    			modal: false,
    			position: "top"
    		});
    		win.jEl.click(() => {
    			win.hide();
    		});
    		window.setTimeout(() => {
    			if (callbackClose) callbackClose();
    			win.hide();
    		}, time);
    		win.show();
    	}
    }();

    /**
     * Create a dropdown
     * @class
      * @extends M_.Outlet
     * @property {type} destroyOnHide
     * @property {type} alignTo
     * @property {type} offsetTop
     * @property {type} offsetLeft
     * @property {type} container
     * @property {type} constraints
     * @property {type} autoSize
     * @property {type} autoShow
     * @property {type} items
     * @property {type} html
     * @property {type} alwaysDropdownBelow
     */
    M_.Dropdown = class extends M_.Outlet {
    	constructor(opts) {
    		var defaults = {
    			destroyOnHide: true,
    			_visible: false,
    			alignTo: null,
    			offsetTop: 0,
    			offsetLeft: 0,
    			container: "body",
    			alwaysDropdownBelow: false,
    			constraints: $(window),
    			autoSize: true,
    			autoShow: false,
    			items: [],
    			html: null,
    			itemsClass: "",
    			drawEachItem: null,
    			// id: null,
    			_items: [],
    			width: 0
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);

    		// this.create() ;
    	}
    	drawItem(item, i, ul, items) {
    		if (this.drawEachItem) return this.drawEachItem.call(this, item, i, ul, items);
    		if (!item.text || item.text == "_m_separation") {
    			ul.append($(`<li class="M_DropdownSeparation"></li>`));
    			return;
    		}
    		var more = "";
    		if (item.disabled) more += " disabled";
    		if (this.itemsClass) more += " " + this.itemsClass;
    		if (item.cls) more += " " + item.cls;
    		var htmlItem = `<li class="M_DropdownMenu ${more}">${item.text}</li>`;
    		var jElItem = $(htmlItem);
    		// jElItem.data('indexitem', i) ;
    		ul.append(jElItem);
    		if (item.click && !item.disabled) {
    			jElItem.click({ fn: item }, evt => {
    				// var el = $(evt.target).closest('.M_DropdownMenu') ;
    				evt.data.fn.click(evt, evt.data.fn);
    				this.hide();
    			});
    		}
    	}
    	/**
    	 * @function setItems
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @param {type}
    	 */
    	setItems(items) {
    		this.jEl.empty();
    		this.jEl.height("auto");
    		// log("setItems",this.jEl)
    		if (items.length) {
    			var ul = $("<ul>");
    			this.jEl.append(ul);
    			for (var i = 0; i < items.length; i++) {
    				this.drawItem(items[i], i, ul, items);
    			}
    			this._setPosition();
    		}
    	}
    	/**
    	 * @function create
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @return {type}
    	 */
    	create() {
    		// console.log("create");
    		var html = `<div class="M_Relative"><div id="${this.id}" class="M_Dropdown"></div></div>`;
    		// log("this.container",this.container)
    		$(this.container).append(html);
    		this.jEl = $(this.container).find(".M_Dropdown");
    		this.jEl.css("z-index", M_.Utils.getNextZIndex());
    		if (this.width > 0) this.jEl.width(this.width);

    		if (this.items.length) this.setItems(this.items);
    		else if (this.html) this.jEl.html(this.html);

    		// if (this.autoShow) this.show() ;

    		this.jEl.on("M_DropdownHide", evt => {
    			// log("M_DropdownHide")
    			// evt.stopPropagation() ;
    			this.hide();
    		});
    		// this.jEl.on('M_DropdownShow', (evt)=> {
    		// 	// evt.stopPropagation() ;
    		// 	log("show!!!")
    		// 	this.show() ;
    		// }) ;

    		// log("ddd",this.jEl)
    	}
    	/**
    	 * @function show
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @return {type}
    	 */
    	show() {
    		// log("show",this._visible)
    		// var evt = $.Event('click') ;
    		// evt.stopPropagation() ;
    		// log("show", this._visible, this.jEl)
    		// window.event.stopPropagation();
    		// M_.Dropdown.closeAllDropdown() ;
    		if (this._visible) return;
    		if (this.trigger("beforeShow", this) === false) return false;
    		this.showOrHide(true);
    		this.trigger("show", this);
    	}
    	/**
    	 * @function hide
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @return {type}
    	 */
    	hide() {
    		// log("hide", this.destroyOnHide, this.jEl.get(0).id)
    		// log("hide ",this._visible, this.jEl)
    		if (!this._visible) return;
    		if (this.trigger("beforeHide", this) === false) return false;
    		this.showOrHide(false);
    		this.trigger("hide", this);
    	}
    	/**
    	 * @function isVisible
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @return {Boolean}
    	 */
    	isVisible() {
    		return this._visible;
    	}
    	_waitEndClose() {
    		window.setTimeout(() => {
    			this._waitEndClose();
    		}, 10);
    	}
    	/**
    	 * @function realign
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @return {type}
    	 */
    	realign() {
    		this._setPosition();
    	}
    	_setPosition() {
    		// search parent overflow with auto or hidden
    		var rettemp = this.jEl
    			.parents()
    			.filter(function() {
    				if ($(this).css("overflow") === "auto" || $(this).css("overflow") === "hidden") return true;
    				return false;
    			})
    			.first();
    		// var compareTo = {top: $(window).scrollTop(), height: $(window).height()} ;
    		// if (rettemp.length) compareTo = {top: }
    		var compareTo = $("body"); // work because body height == window height !!!!
    		if (rettemp.length) compareTo = rettemp;

    		this.jEl.height("auto");

    		var t = this.alignTo.offset().top + this.alignTo.outerHeight() + this.offsetTop;
    		var l = this.alignTo.offset().left + this.offsetLeft;
    		this.jEl.show().offset({ top: t, left: l });
    		// log("this.offsetLeft",this.offsetLeft)

    		var top = this.jEl.offset().top;
    		var h = this.jEl.outerHeight();
    		var topCompareTo = compareTo.offset().top;
    		var hCompareTo = compareTo.outerHeight();
    		// console.log("hCompareTo", hCompareTo);
    		// console.log("top,h,topCompareTo,hCompareTo", top,h,topCompareTo,hCompareTo,this.jEl);
    		if (top + h > topCompareTo + hCompareTo && this.alwaysDropdownBelow !== true) {
    			// check if space above is bigger than below
    			if (top - topCompareTo > topCompareTo + hCompareTo - top) {
    				//more big above
    				// max height
    				let maxh = top - topCompareTo - 35;
    				let moreH = 0;
    				if (h > maxh && this.autoSize) {
    					h = maxh;
    					this.jEl.height(h - 50);
    				} else moreH = 30;
    				t = top - h - moreH;
    				this.jEl.offset({ top: t, left: l });
    			} else {
    				// more big bottom
    				let maxh = topCompareTo + hCompareTo - top - 25;
    				if (h > maxh && this.autoSize) {
    					h = maxh;
    					this.jEl.height(h);
    				}
    			}
    		}

    		// var hWin = $(window).height() ;
    		// var top = this.jEl.offset().top ;
    		// var h = this.jEl.height() ;
    		// if (top+h-$(window).scrollTop() > hWin) {
    		// 	// try to set dropdown on top of alignTo
    		// 	if (this.alignTo.offset().top>(hWin/2) && h<(hWin/2)) {
    		// 		t = this.alignTo.offset().top - h - 30 ;
    		// 		this.jEl.offset({top:t, left:l}) ;

    		// 	// else set dropdown height
    		// 	} else {
    		// 		var h = hWin-(top-$(window).scrollTop())-30 ;
    		// 		this.jEl.height(h) ;
    		// 	}
    		// }
    	}
    	/**
    	 * @function showOrHide
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @param  {type}
    	 * @return {type}
    	 */
    	showOrHide(ok) {
    		// log("showOrHide",ok,this.jEl)
    		var speed = M_.Utils.getSpeedAnim();
    		if (ok) {
    			// this.jEl.show().css({opacity:0}).offset({top, left}).transition({opacity:1}) ;
    			M_.Dropdown.closeAllDropdown();
    			// log("this.jEl",this.jEl)
    			this.jEl
    				.show()
    				.css({ opacity: 0.5 })
    				.transition({ opacity: 1 }, speed);
    			this._setPosition();
    			this._visible = true;
    		} else {
    			// this.jEl.transition({opacity:0}, speed, ()=> {
    			this.jEl.hide();
    			this._visible = false;
    			if (this.destroyOnHide) this.destroy();
    			// }) ;
    		}
    		// log("_visible",this._visible)
    	}
    	/**
    	 * @function destroy
    	 * @memberOf! M_.Dropdown
    	 * @instance
    	 * @return {type}
    	 */
    	destroy() {
    		// log("destroy");
    		// $(document).off('click', $.proxy(this.onDocumentClick, this)) ;
    		this.jEl.parent().remove();
    		this.trigger("destroy", this);
    	}
    	/**
    	 * @return {type}
    	 */
    	static closeAllDropdown() {
    		// log("closeAllDropdown")
    		$(".M_Dropdown").trigger("M_DropdownHide");
    		// if (this.tabDropdowns) {
    		// 	for(var i=0 ; i<this.tabDropdowns.length ; i++) {
    		// 		if (this.tabDropdowns[i]) this.tabDropdowns[i].hide() ;
    		// 	}
    		// }
    	}
    	static _goGlobalListeners() {
    		$(document)
    			.on("click", ".M_Dropdown", evt => {
    				M_.Help.hideMHelp();
    				evt.stopPropagation();
    			})
    			.on("click", evt => {
    				M_.Help.hideMHelp();
    				// log("clickDocument")
    				// if ($(evt.target).closest('.M_Dropdown').length===0) {
    				this.closeAllDropdown();
    				// $('.M_Dropdown').trigger('M_DropdownHide') ;
    				// }
    			});
    	}
    };
    M_.Dropdown._goGlobalListeners();

    /**
     * Create month calendar to display directly on page or in a M_.Form.Date
     * @class
      * @implements M_.Observable
     * @property {type} container
     * @property {type} dateViewed
     * @property {type} dateSelected
     * @property {type} renderCellDay
     * @property {type} dateFormat
     * @property {type} dateFormatFrom
     * @property {type} firstDay
     * @property {type} noDays
     * @property {type} noMonths
     * @property {type} allways6lines
     * @property {type} cls
     * @property {type} displayHeader
     * @property {type} showDateSelected
     * @property {type} showDateViewed
     */
    M_.CalendarMonth = class {
    	constructor(opts) {
    		var defaults = {
    			container: null,
    			dateViewed: moment(),
    			dateSelected: moment(),
    			renderCellDay: null,
    			// disabledDatesTo: null,
    			// disabledDatesFrom: null,
    			disabledDates: null,
    			dateFormat: "DD/MM/YYYY",
    			dateFormatFrom: "YYYY-MM-DD",
    			firstDay: 1, //0=sunday, 1=monday
    			noDays: false,
    			noMonths: false,
    			_thefirstDay: null,
    			_thelastDay: null,
    			allways6lines: false,
    			cls: "",
    			_cls: "",
    			displayHeader: true,
    			selectable: true,
    			showDateSelected: true,
    			showDateViewed: true,
    			showWeekNumber: false,
    			selectWeek: false
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();

    		this.create();
    	}
    	// tpl: 'tpl_M_CalendarMonth',

    	/**
    	 * @return {type}
    	 */
    	create() {
    		var html = `<div class="M_CalendarMonth ${this._cls} ${this.cls}">
    			<div class="M_CalendarMonth_days">
    				<div class="M_CalendarMonth_previous"><span class="fa fa-arrow-circle-left"></span></div>
    				<div class="M_CalendarMonth_date">Date</div>
    				<div class="M_CalendarMonth_next"><span class="fa fa-arrow-circle-right"></span></div>
    				<div class="M_CalendarMonth_content"></div>
    			</div>
    			<div class="M_CalendarMonth_months">
    				<div class="M_CalendarMonth_previous"><span class="fa fa-arrow-circle-left"></span></div>
    				<div class="M_CalendarMonth_date">Date</div>
    				<div class="M_CalendarMonth_next"><span class="fa fa-arrow-circle-right"></span></div>
    				<div class="M_CalendarMonth_content"></div>
    			</div>
    			<div class="M_CalendarMonth_years">
    				<div class="M_CalendarMonth_previous"><span class="fa fa-arrow-circle-left"></span></div>
    				<div class="M_CalendarMonth_date">Date</div>
    				<div class="M_CalendarMonth_next"><span class="fa fa-arrow-circle-right"></span></div>
    				<div class="M_CalendarMonth_content"></div>
    			</div>
    		</div>`;

    		this.jEl = $(html);
    		this.container.append(this.jEl);
    		this.redraw();

    		if (!this.displayHeader) this.container.find(".M_CalendarMonth_date, .M_CalendarMonth_previous, .M_CalendarMonth_next").hide();
    		else this.container.find(".M_CalendarMonth_date, .M_CalendarMonth_previous, .M_CalendarMonth_next").show();
    	}
    	/**
    	 * @param {type}
    	 * @param {type}
    	 */
    	setDateViewed(dateViewed, dateSelected) {
    		this.dateViewed = dateViewed;
    		// log("this.dateViewed",this.dateViewed)
    		if (dateSelected) this.dateSelected = dateSelected;
    		this.redraw();
    	}
    	/**
    	 * @param {type}
    	 * @param {type}
    	 */
    	setDateSelected(dateSelected, dateViewed) {
    		this.dateSelected = dateSelected;
    		if (dateViewed) this.dateViewed = dateViewed;
    		this.redraw();
    	}
    	/**
    	 * @return {type}
    	 */
    	getDateSelected() {
    		return moment(this.dateSelected);
    	}
    	/**
    	 * @return {type}
    	 */
    	redraw() {
    		this.showDays();
    	}
    	/**
    	 * @return {type}
    	 */
    	showYears() {
    		this.jEl.find(".M_CalendarMonth_days").hide();
    		this.jEl.find(".M_CalendarMonth_months").hide();
    		this.jEl.find(".M_CalendarMonth_years").show();

    		var yearStart = Math.floor(this.dateViewed.year() / 10) * 10 - 1;

    		var html = "";
    		html += "<table>";
    		for (var i = 0; i < 12; i++) {
    			var dActu = moment({ year: yearStart + i, month: this.dateViewed.month(), day: 1 });
    			if (i % 4 === 0) html += "<tr>";
    			var active = "";
    			if (dActu.year() == this.dateSelected.year()) active = "class='active'";
    			html += "<td " + active + " data-m-date='" + dActu.format("YYYY-MM-DD") + "'>" + dActu.year() + "</td>";
    			if (i % 4 === 3) html += "</tr>";
    		}
    		html += "</table>";
    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_content").html(html);

    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_date").html(yearStart + 1 + " - " + (yearStart + 11));

    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_content td").off("click");
    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_content td").on("click", evt => {
    			var d = $(evt.target).attr("data-m-date");
    			this.selectYear(d);
    		});
    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_next").off("click");
    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_next").on("click", evt => {
    			this.nextYears();
    		});
    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_previous").off("click");
    		this.jEl.find(".M_CalendarMonth_years .M_CalendarMonth_previous").on("click", evt => {
    			this.previousYears();
    		});
    		this.trigger("viewedChanged", this, moment(this.dateViewed));
    	}
    	/**
    	 * @return {type}
    	 */
    	previousYears() {
    		this.dateViewed.add(-10, "years"); //  = new Date(this.dateViewed.getFullYear()*1-10, this.dateViewed.getMonth(), 1) ;
    		this.showYears();
    		this.trigger("yearViewedChanged", this, moment(this.dateViewed));
    	}
    	/**
    	 * @return {type}
    	 */
    	nextYears() {
    		this.dateViewed.add(10, "years"); //this.dateViewed = new Date(this.dateViewed.getFullYear()*1+10, this.dateViewed.getMonth(), 1) ;
    		this.showYears();
    		this.trigger("yearViewedChanged", this, moment(this.dateViewed));
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	selectYear(date) {
    		// if ($.type(date)=="string") date = moment(date) ; //date = M_.Utils.parseDate(date, 'Y-m-d') ;
    		this.dateViewed = moment(date);
    		if (this.noMonths) {
    			date.month(0).date(1);
    			//this.selectDate(date) ;
    			this.trigger("selected", this, moment(date));
    		} else {
    			this.showMonths();
    			this.trigger("yearViewedChanged", this, moment(this.dateViewed));
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	showMonths() {
    		if (this.noMonths) {
    			this.showYears();
    			return;
    		}
    		this.jEl.find(".M_CalendarMonth_days").hide();
    		this.jEl.find(".M_CalendarMonth_months").show();
    		this.jEl.find(".M_CalendarMonth_years").hide();

    		var html = "";
    		html += "<table>";
    		var tabMonths = moment.months();
    		for (var i = 0; i < 12; i++) {
    			var dActu = moment({ year: this.dateViewed.year(), month: i, day: 1 }); //new Date(this.dateViewed.year(), i, 1) ;
    			if (i % 3 === 0) html += "<tr>";
    			var active = "";
    			if (dActu.year() == this.dateSelected.year() && dActu.month() == this.dateSelected.month()) active = "class='active'";
    			var moreD = "";
    			if (this.showWeekNumber) {
    				var dTemp3 = moment(dActu).endOf("month");
    				moreD += "<br>(S" + dActu.week() + " - S" + dTemp3.week() + ")";
    			}
    			html += "<td " + active + " data-m-date='" + dActu.format("YYYY-MM-DD") + "'>" + tabMonths[i] + moreD + "</td>";
    			if (i % 3 == 2) html += "</tr>";
    		}
    		html += "</table>";
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_content").html(html);

    		this.jEl
    			.find(".M_CalendarMonth_months .M_CalendarMonth_date")
    			.html(this.dateViewed.year() + " <span class='fa fa-arrow-circle-down'></span>");

    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_content td").off("click");
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_content td").on("click", evt => {
    			var d = $(evt.target).attr("data-m-date");
    			this.selectMonth(d);
    		});
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_next").off("click");
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_next").on("click", evt => {
    			this.nextYear();
    		});
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_date").off("click");
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_date").on("click", evt => {
    			this.showYears();
    		});
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_previous").off("click");
    		this.jEl.find(".M_CalendarMonth_months .M_CalendarMonth_previous").on("click", evt => {
    			this.previousYear();
    		});
    		this.trigger("viewedChanged", this, moment(this.dateViewed));
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	selectMonth(date) {
    		// if ($.type(date)=="string") date = moment(date) ;
    		this.dateViewed = moment(date);
    		if (this.noDays) {
    			this.dateViewed.date(1);
    			//this.selectDate(date) ;
    			this.trigger("selected", this, moment(this.dateViewed));
    		} else {
    			this.showDays();
    			this.trigger("dateViewedChanged", this, moment(this.dateViewed));
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	previousYear() {
    		var d = this.dateViewed.date();
    		if (this.dateViewed.date() * 1 > 28) d = 28;
    		this.dateViewed.subtract(1, "years");
    		this.showMonths();
    		this.trigger("yearViewedChanged", this, moment(this.dateViewed));
    	}
    	/**
    	 * @return {type}
    	 */
    	nextYear() {
    		var d = this.dateViewed.date();
    		if (this.dateViewed.date() * 1 > 28) d = 28;
    		this.dateViewed.add(1, "years");
    		this.showMonths();
    		this.trigger("yearViewedChanged", this, moment(this.dateViewed));
    	}
    	/**
    	 * @return {type}
    	 */
    	showDays() {
    		var i;
    		if (this.noDays) {
    			this.showMonths();
    			return;
    		}
    		this.jEl.find(".M_CalendarMonth_days").show();
    		this.jEl.find(".M_CalendarMonth_months").hide();
    		this.jEl.find(".M_CalendarMonth_years").hide();

    		var today = moment();
    		var year = this.dateViewed.year();
    		var month = this.dateViewed.month();
    		// var date = this.dateViewed.date() ;
    		var nbDaysInMonth = this.dateViewed.daysInMonth();
    		var firstDate = moment({ year: year, month: month, day: 1 }).startOf("day");
    		var decalage = 0;
    		// log("firstDate",firstDate,decalage)
    		for (i = 1; i <= 7; i++) {
    			if (firstDate.day() == 1) break;
    			decalage--;
    			firstDate.add(-1, "days");
    		}
    		// log("firstDate",firstDate,decalage)
    		var html = "";
    		html += "<table>";
    		html += "<thead><tr>";
    		if (this.showWeekNumber) html += "<th>S</th>";
    		var tabDaysShort = moment.weekdaysShort();
    		for (i = 0; i < 7; i++) {
    			var j = (i + 1) % 7;
    			html += "<th>" + tabDaysShort[j] + "</th>";
    		}
    		html += "</tr></thead>";
    		html += "<tbody>";
    		var tabCells = [];
    		var nbMax = Math.ceil(nbDaysInMonth + 15);
    		var nbLines = 0;
    		// log("firstDate",nbMax,firstDate, this.dateViewed)
    		for (i = 0; i < nbMax; i++) {
    			var dActu = moment()
    				.year(year)
    				.month(month)
    				.date(1 + decalage + i)
    				.startOf("day"); //{year:year, month:month, day:1+decalage+i}
    			if (i === 0) this._thefirstDay = moment(dActu);
    			if (dActu.day() == 1) {
    				html += "<tr>";
    				if (this.showWeekNumber) {
    					let tdClsWeek = "";
    					if (this.disabledDates && this.disabledDates(this, dActu)) tdClsWeek = "notselectable";
    					html +=
    						"<td class='" + tdClsWeek + "' data-m-date='" + dActu.format("YYYY-MM-DD") + "'><b>" + dActu.week() + "</b>&nbsp;&nbsp;</td>";
    				}
    				nbLines++;
    				if (this.allways6lines && nbLines == 7) {
    					this._thelastDay = moment(dActu);
    					break;
    				}
    			}
    			var tdCls = "";
    			if (
    				this.showDateViewed &&
    				dActu.year() == this.dateSelected.year() &&
    				dActu.month() == this.dateSelected.month() &&
    				dActu.date() == this.dateSelected.date()
    			)
    				tdCls += "active ";
    			if (dActu.month() != this.dateViewed.month()) tdCls += "notthegoodmonth ";
    			var isDisabled = false;
    			if (this.disabledDates) isDisabled = this.disabledDates(this, dActu);
    			if (isDisabled) tdCls += "notselectable ";
    			if (this.showDateSelected && dActu.year() == today.year() && dActu.month() == today.month() && dActu.date() == today.date())
    				tdCls += "current ";
    			var contentTemp = dActu.date();
    			var idCell = M_.Utils.id();
    			//if (this.renderCellDay) contentTemp = this.renderCellDay(this, dActu, idCell) ;
    			tabCells.push([dActu, idCell]);
    			html +=
    				"<td class='" +
    				tdCls +
    				"' id='" +
    				idCell +
    				"' data-m-date='" +
    				dActu.format("YYYY-MM-DD") +
    				"'><div class='M_CalendarMonth_txt'>" +
    				contentTemp +
    				"</div></td>";
    			if (dActu.day() === 0) html += "</tr>";
    			if (dActu.month() != month && dActu.day() === 0 && !this.allways6lines && nbLines >= 2) {
    				this._thelastDay = moment(dActu);
    				break;
    			}
    		}
    		html += "</tbody>";
    		html += "</table>";
    		var tabMonths = moment.months();
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_content").html(html);
    		this.jEl
    			.find(".M_CalendarMonth_days .M_CalendarMonth_date")
    			.html(tabMonths[this.dateViewed.month()] + " " + this.dateViewed.year() + " <span class='fa fa-arrow-circle-down'></span>");

    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_content td").off("click");
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_content td").on("click", evt => {
    			var target = $(evt.target).closest("td");
    			if (target.hasClass("notselectable")) return;
    			var d = target.attr("data-m-date");
    			var date = moment(d);
    			if (this.selectWeek) date.startOf("week");
    			//this.selectDate(d) ;
    			if (this.selectable) this.setDateSelected(date);
    			this.trigger("selected", this, date);
    		});
    		if (this.selectWeek) {
    			this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_content tr").mouseenter(evt => {
    				$(evt.target)
    					.closest("tr")
    					.addClass("alllineover");
    			});
    			this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_content tr").mouseleave(evt => {
    				$(evt.target)
    					.closest("tr")
    					.removeClass("alllineover");
    			});
    		}
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_next").off("click");
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_next").on("click", evt => {
    			this.nextMonth();
    		});
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_date").off("click");
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_date").on("click", evt => {
    			this.showMonths();
    		});
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_previous").off("click");
    		this.jEl.find(".M_CalendarMonth_days .M_CalendarMonth_previous").on("click", evt => {
    			this.previousMonth();
    		});

    		if (this.renderCellDay) {
    			for (i = 0; i < tabCells.length; i++) {
    				var el = $("#" + tabCells[i][1]);
    				var contentTemp2 = this.renderCellDay(this, tabCells[i][0], el);
    				el.html(contentTemp2);
    			}
    		}
    		this.trigger("viewedChanged", this, moment(this.dateViewed));
    	}

    	/**
    	 * @return {type}
    	 */
    	getFirstDay() {
    		return this._thefirstDay;
    	}
    	/**
    	 * @return {type}
    	 */
    	getLastDay() {
    		return this._thelastDay;
    	}
    	/**
    	 * @return {type}
    	 */
    	previousMonth() {
    		// console.log("this.dateViewed", this.dateViewed);
    		this.dateViewed.subtract(1, "month").startOf("months");
    		this.showDays();
    	}
    	/**
    	 * @return {type}
    	 */
    	nextMonth() {
    		// this.dateViewed = moment({year:this.dateViewed.year(), month:this.dateViewed.month()*1+1, day:1}) ;
    		this.dateViewed.add(1, "month").startOf("months");
    		this.showDays();
    	}
    	/**
    	 * @return {type}
    	 */
    	destroy() {
    		this.jEl.remove();
    		this.trigger("destroy", this);
    	}
    };

    M_.Calendar = {};

    M_.Calendar.MonthView = class extends M_.CalendarMonth {
    	constructor(opts) {
    		var defaults = {
    			displayHeader: false,
    			_cls: "M_CalendarMonthView",
    			cellMinHeight: 50,
    			cellHeadMinHeight: 20,
    			showDateViewed: false,
    			showDateSelected: true,
    			provideEvents: null,
    			selectable: false,
    			modelDef: {
    				key: "key",
    				start: "start",
    				end: "end",
    				text: "text",
    				cls: "cls"
    			},
    			events: [
    				// {
    				// 	id: '1',
    				// 	start: moment(),
    				// 	end: moment().add(8, 'days'),
    				// 	text: "Céline",
    				// 	bgcolor: 'red'
    				// }, {
    				// 	id: '2',
    				// 	start: moment(),
    				// 	end: moment().add(2, 'days'),
    				// 	text: "Bernard",
    				// 	bgcolor: 'green'
    				// }, {
    				// 	id: '3',
    				// 	start: moment().subtract(2, 'days'),
    				// 	end: moment().add(8, 'days'),
    				// 	text: "Bernard2",
    				// 	bgcolor: 'green'
    				// }, {
    				// 	id: '4',
    				// 	start: moment().subtract(60, 'days'),
    				// 	end: moment().subtract(15, 'days'),
    				// 	text: "Henri",
    				// 	bgcolor: 'purple'
    				// }
    			]
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);

    		super(opts);

    		_.mixin(this, M_.Stored);
    		// log("store", Object.mixin)
    		this.initStored();
    	}
    	loadEvents() {
    		if (this.store)
    			this.store.load({
    				skip: 0,
    				// where: "{'va_start':{'<': '"+moment(this.dateViewed).startOf('month').add(1, 'month').format('YYYY-MM-DD')+"'}, 'va_end':{'>':'"+moment(this.dateViewed).startOf('month').format('YYYY-MM-DD')+"'}}"
    				start: moment(this.dateViewed)
    					.startOf("month")
    					.format("YYYY-MM-DD"),
    				end: moment(this.dateViewed)
    					.startOf("month")
    					.add(1, "month")
    					.format("YYYY-MM-DD")
    			});
    	}
    	selectMonth(date) {
    		super.selectMonth(date);
    		this.loadEvents();
    	}
    	updateStore() {
    		// this.showDays() ;
    		this.drawEvents();
    		// log("this.store",this.store) ;
    	}
    	showDays() {
    		super.showDays();
    		// this._loadEvents() ;
    		// if (this.store) this.store.reload() ;
    	}
    	_loadEvents() {
    		// if (this.provideEvents) {
    		// 	this.provideEvents((events)=> {
    		// 		this.events = events ;
    		// 		this.drawEvents() ;
    		// 	}) ;
    		// } else {
    		// 	this.drawEvents() ;
    		// }
    	}
    	drawEvents() {
    		this.jEl.find(".M_CalendarMonthView_bar").remove();
    		if (this.cellMinHeight > 0) {
    			this.jEl.find(".M_CalendarMonth_days tbody tr").css("min-height", this.cellMinHeight);
    			this.jEl.find(".M_CalendarMonth_days tbody tr").css("height", this.cellMinHeight);
    		}
    		if (this.cellHeadMinHeight > 0) {
    			this.jEl.find(".M_CalendarMonth_days thead tr").css("min-height", this.cellHeadMinHeight);
    			this.jEl.find(".M_CalendarMonth_days thead tr").css("height", this.cellHeadMinHeight);
    		}
    		var widthCell = this.jEl.find("td").outerWidth();
    		// var tabNbEventsByDate = {} ; // stock le nb d'événements pour une journée
    		var tabNbEventsByWeek = {}; // stock le nb d'événements dans la semaine
    		var tabBars = [];
    		// _.each(this.events, (evt)=> {
    		this.store.each(model => {
    			var start = moment(model.get(this.modelDef.start)).startOf("day");
    			var end = moment(model.get(this.modelDef.end)).startOf("day");
    			var start2 = model.get(this.modelDef.start2) || false;
    			var end2 = model.get(this.modelDef.end2) || false;
    			var nbDays = end.diff(start, "days") + 1;
    			var dActu = moment(start);
    			for (var nj = 0; nj < nbDays; nj++) {
    				var isFirst = false;
    				var isLast = false;
    				var startOfWeek = moment(dActu)
    					.startOf("week")
    					.format("YYYY-MM-DD");
    				if (nj === 0 || dActu.day() === 1) {
    					if (!tabNbEventsByWeek[startOfWeek]) tabNbEventsByWeek[startOfWeek] = 0;
    					if (nj === 0) isFirst = true;
    					var objTemp = {
    						id: model.get(this.modelDef.key),
    						start: moment(dActu),
    						start2: start2,
    						end2: end2,
    						isFirst: isFirst,
    						isLast: isLast,
    						nbDays: 0,
    						startOfWeek: startOfWeek,
    						position: tabNbEventsByWeek[startOfWeek]
    						// text: model.get(this.modelDef.text),
    						// color: model.get(this.modelDef.color),
    						// bgcolor: model.get(this.modelDef.bgcolor)
    					};
    					if (_.isFunction(this.modelDef.text)) objTemp.text = this.modelDef.text(model);
    					else objTemp.text = model.get(this.modelDef.text);
    					if (_.isFunction(this.modelDef.cls)) objTemp.cls = this.modelDef.cls(model);
    					else objTemp.cls = model.get(this.modelDef.cls);
    					tabBars.push(objTemp);
    					tabNbEventsByWeek[startOfWeek]++;
    				}
    				tabBars[tabBars.length - 1].nbDays++;
    				if (nj === nbDays - 1) tabBars[tabBars.length - 1].isLast = true;
    				// if (!tabNbEventsByDate[dActu.format('YYYY-MM-DD')]) tabNbEventsByDate[dActu.format('YYYY-MM-DD')] = 0 ;
    				// tabNbEventsByDate[dActu.format('YYYY-MM-DD')]++ ;

    				dActu.add(1, "days");
    			}
    		});
    		// _.each(tabBars, (barEvt)=> {
    		for (let barEvt of tabBars) {
    			var bar = $("<div class='M_CalendarMonthView_bar " + barEvt.cls + "' data-m-evt='" + barEvt.id + "'>" + barEvt.text + "</div>");
    			var css = {
    				top: barEvt.position * 15 + 25
    			};
    			if (barEvt.bgcolor) css["background-color"] = barEvt.bgcolor;
    			if (barEvt.color) css.color = barEvt.color;
    			var moreLeft = 0;
    			var moreWidth = 0;
    			if (barEvt.start2 && barEvt.isFirst) moreLeft = 50; //widthCell/2
    			if (barEvt.end2 && barEvt.isLast) moreWidth = 50; //widthCell/2
    			css.left = moreLeft + "%";
    			bar
    				.width(101 * barEvt.nbDays - moreLeft - moreWidth + "%") //widthCell*barEvt.nbDays
    				.css(css)
    				.html(barEvt.text);
    			var el2 = this.jEl.find("td[data-m-date='" + barEvt.start.format("YYYY-MM-DD") + "']");
    			if (el2) el2.append(bar);
    			var tr = this.jEl.find("td[data-m-date='" + barEvt.startOfWeek + "']").parent();
    			var h = 25 + 15 * tabNbEventsByWeek[barEvt.startOfWeek] + 5;
    			tr.css("height", Math.max(h, this.cellMinHeight));
    		}
    		this.jEl
    			.find(".M_CalendarMonthView_bar")
    			.click(evt => {
    				evt.stopPropagation();
    				var bar = $(evt.target);
    				var id = bar.closest(".M_CalendarMonthView_bar").attr("data-m-evt");
    				var found = this.store.getRow(id);
    				if (found) this.trigger("clickitem", this, found, bar);
    			})
    			.mouseenter(evt => {
    				var bar = $(evt.target);
    				var id = bar.closest(".M_CalendarMonthView_bar").attr("data-m-evt");
    				var found = this.store.getRow(id);
    				this.trigger("enteritem", this, found, bar);
    			});
    	}
    	setEvents(events) {
    		this.events = events;
    		this.redraw();
    	}
    };

    /**
     * All form elements
     * @namespace
     */
    M_.Form = {};

    /**
     * Manage a form
     * @class
     * @implements M_.Observable
     * @implements M_.CRUD
     * @property {type} useWebsocket
     * @property {type} items
     * @property {type} itemsDefaults
     * @property {type} model
     * @property {type} url
     */
    M_.Form.Form = class {
    	constructor(opts) {
    		var defaults = {
    			useWebsocket: M_.App.useWebsocket,
    			items: [],
    			_items: [],
    			itemsDefaults: {},
    			_currentModel: null,
    			model: null,
    			url: "",
    			urls: null,
    			rootData: "data",
    			validBeforeSave: true
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		_.mixin(this, M_.Observable);
    		this.initObservable();
    		_.mixin(this, M_.CRUD);
    		this.initCRUD();

    		var items = this.items;
    		for (var i = 0; i < items.length; i++) {
    			this.addItem(items[i]);
    		}

    		if (this.autoUnedit) {
    			// $(document).click((evt)=> {
    			// 	if ($(evt.target).closest('.edit').length===0) {
    			// 		this.edit(false) ;
    			// 	}
    			// }) ;
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	reset() {
    		var _items = this._items;
    		for (var i = 0; i < _items.length; i++) {
    			_items[i].reset();
    		}
    		this.trigger("reset", this);
    	}
    	disable() {
    		var _items = this._items;
    		for (var i = 0; i < _items.length; i++) {
    			_items[i].disable();
    		}
    		this.trigger("disable", this);
    	}
    	enable() {
    		var _items = this._items;
    		for (var i = 0; i < _items.length; i++) {
    			_items[i].enable();
    		}
    		this.trigger("disable", this);
    	}
    	/**
    	 * @return {type}
    	 */
    	valid(err = []) {
    		var ok = true;
    		var _items = this._items;
    		for (var i = 0; i < _items.length; i++) {
    			if (!_items[i].valid()) {
    				ok = false;
    				var label = _items[i].name;
    				if (_items[i].label !== "") label = _items[i].label;
    				err.push({ key: _items[i].name, label: label });
    			}
    			// console.log("_items[i].valid(),_items[i].name", _items[i].valid(),_items[i].name);
    		}
    		var ok2 = this.trigger("valid", this, ok, err);
    		// console.log("ok2", ok2, ok);
    		return ok && ok2;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	informValidReturnSails(data) {
    		// log("informValidReturnSails",data)
    		var errTxt = "";
    		_.each(data.invalidAttributes, (attr, key) => {
    			// log("attr, key",attr, key)
    			if (this.find(key)) {
    				this.find(key).informValid(false);
    				let key2 = this.find(key).label;
    				if (key2 === "") key2 = key;
    				key = key2;
    			}
    			errTxt += "<b>" + key + "</b> : ";
    			_.each(attr, msg => {
    				errTxt += msg.message;
    			});
    			errTxt += "<br/>";
    		});
    		M_.Dialog.alert("Erreur", "Merci de corriger les champs en rouge :<br/>" + errTxt);
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	onLoad(model) {
    		if (this.model)
    			this.setValues(model); //.getData()
    		else this.setValues(model);
    	}
    	/**
    	 * @return {type}
    	 */
    	onBeforeLoad() {
    		this.resetValid();
    	}
    	/**
    	 * @param {type}
    	 */
    	setValues(dataOrModel) {
    		// console.log("this._items", this._items);
    		this.reset();
    		if (!(dataOrModel instanceof M_.Model) && this.model) dataOrModel = new this.model({ row: dataOrModel });
    		this._currentModel = dataOrModel;
    		var _items = this._items;
    		// log("_items",_items)
    		for (var i = 0; i < _items.length; i++) {
    			let v;
    			if (this.model) v = this._currentModel.get(_items[i].name);
    			else v = this._currentModel[_items[i].name];
    			// log("_items[i].name",i, _items[i].name, v)
    			if (v !== undefined) {
    				if (_items[i] instanceof M_.Form.Combobox) {
    					let v2;
    					if (this.model) v2 = this._currentModel.get(_items[i].name + "_val");
    					else v2 = this._currentModel[_items[i].name + "_val"];
    					// log("v2",v2,name+'_val')
    					if (v2) _items[i].setValueAndRawValue(v, v2);
    					else {
    						_items[i].setValue(v);
    					}
    				} else {
    					// console.log("v", v, _items[i].inputType);
    					_items[i].setValue(v);
    				}
    			} else {
    				_items[i].reset();
    			}
    		}
    		//		this._hideIfEmpty() ;
    		this.trigger("update", this);
    	}
    	//	_hideIfEmpty() {
    	//		var _items = this._items ;
    	//		for(var i=0 ; i<_items.length ; i++) {
    	//			// _items[i]._hideIfEmpty() ;
    	//			// log("hideIfEmpty",_items[i].hideIfEmpty2)
    	//			if (_items[i].hideContainerIfEmpty && M_.Utils.isEmpty(_items[i].getValue())) {
    	//				_items[i].container.hide() ;
    	//			} else {
    	//				_items[i].container.show() ;
    	//			}
    	//				// if (this._inEdit || _items[i].value!=='') _items[i].container.closest(_items[i].hideIfEmpty).show() ;
    	//			// 	else if (_items[i].value==='') _items[i].container.closest(_items[i].hideIfEmpty).hide() ;
    	//			// }
    	//		}
    	//	}
    	/**
    	 * @param  {Boolean}
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValues(serialized = false, returnModel = false) {
    		// var model = new this.model() ;
    		// log("model",model)
    		var _items = this._items,
    			data = {};
    		for (var i = 0; i < _items.length; i++) {
    			if (_items[i] instanceof M_.Form.File) continue;
    			// 	// log("AAAAAAAAAAA")
    			// 	var resTemp = _items[i].getValue(serialized) ;
    			// 	if (resTemp==="") continue ;
    			// 	else data[_items[i].name] = resTemp ;
    			// } else
    			var val = _items[i].getValue(serialized);
    			if (_items[i].name.slice(-2) === "[]") {
    				var nameTemp = _items[i].name.slice(0, -2);
    				if (!data[nameTemp]) data[nameTemp] = [];
    				data[nameTemp].push(val);
    			} else {
    				data[_items[i].name] = val;
    			}
    			// if (data[_items[i].name]==undefined) {
    			// 	data[_items[i].name] = val ;
    			// } else {
    			// 	if (Array.isArray(data[_items[i].name])) {
    			// 		data[_items[i].name].push(val) ;
    			// 	} else {
    			// 		data[_items[i].name] = [data[_items[i].name], val] ;
    			// 	}
    			// }

    			// if (moment.isMoment(data[_items[i].name])) data[_items[i].name] = data[_items[i].name].format("YYYY-MM-DD HH:mm:ss") ;
    			// if (_items[i].)
    		}
    		// log("getValues",data);
    		if (returnModel) return new this.model({ row: data });
    		return data;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	find(fieldName) {
    		var _items = this._items;
    		for (var i = 0; i < _items.length; i++) {
    			if (_items[i].name == fieldName) {
    				return _items[i];
    			}
    		}
    		return null;
    	}
    	/**
    	 * @return {type}
    	 */
    	resetValid() {
    		for (var i = 0; i < this._items.length; i++) {
    			this._items[i].informValid(true);
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	validAndAlert() {
    		var err = [];
    		var ok = this.valid(err);
    		if (!ok) this._alert(err);
    		return ok;
    	}
    	_alert(err) {
    		var errTxt = "";
    		_.each(err, er => {
    			errTxt += "• " + er.label + "<br>";
    		});
    		M_.Dialog.alert("Erreur", "Merci de corriger les champs en rouge :<br>" + errTxt);
    	}
    	validAndSave(modelOrData = null, args = {}, callback = null) {
    		var err = [];
    		var ok = this.valid(err);
    		// console.log("ok", ok);
    		if (ok) this.save(modelOrData, args, callback);
    		else this._alert(err);
    		return ok;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	populate(dataOrModel) {
    		this.setValues(dataOrModel);
    		// if (data instanceof M_.Model) {
    		// 	data = data.getArray();
    		// }
    		// this.resetSate();
    		// for (var i = 0; i < this.outlets.length; i++) {
    		// 	var outlet = this.outlets[i];
    		// 	for (var key in data) {
    		// 		if (outlet.name == key) {
    		// 			if (outlet instanceof M_.Form.Static) {
    		// 				if (data[key + "_val"])
    		// 					outlet.setValueAndText(data[key], data[key + "_val"]);
    		// 				else
    		// 					outlet.setValue(data[key]);
    		// 			} else if (outlet instanceof M_.Form.MultiCombobox) {
    		// 				outlet.reset();
    		// 				if ($.isArray(data[key]))
    		// 					outlet.setValue(data[key]);
    		// 			} else if (outlet instanceof M_.Form.Combobox) {
    		// 				if (data[key + "_val"])
    		// 					outlet.setValueAndText(data[key], data[key + "_val"]);
    		// 				else
    		// 					outlet.setValue(data[key]);
    		// 			} else {
    		// 				outlet.setValue(data[key]);
    		// 			}
    		// 		}
    		// 	}
    		// }
    	}
    	/**
    	 * @param {type}
    	 */
    	addItem(item) {
    		if (!_.isArray(item)) item = [item];
    		for (let i = 0; i < item.length; i++) {
    			var optsItem = $.extend({}, this.itemsDefaults, item[i]);
    			optsItem.form = this;
    			if (optsItem.autoContainer) {
    				var jElTemp = $("<div></div>");
    				optsItem.autoContainer.append(jElTemp);
    				optsItem.container = jElTemp;
    			}
    			if (optsItem.type === M_.Form.Div) new optsItem.type(optsItem);
    			else {
    				var item2 = new optsItem.type(optsItem);
    				this._items.push(item2);
    			}
    		}
    		// console.log("this._items",this._items);
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	deleteItem(name) {
    		var keepedItems = [];
    		for (var i = 0; i < this._items.length; i++) {
    			// log("destroy",this._items[i].name,name)
    			if (this._items[i].name == name) {
    				this._items[i].destroy();
    			} else {
    				keepedItems.push(this._items[i]);
    			}
    		}
    		this._items = keepedItems;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getItem(name) {
    		this.find(name);
    	}
    	/**
    	 * @return {type}
    	 */
    	getItems() {
    		return this._items;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	getItemsByFakeGroup(fakeGroupName) {
    		var keepedItems = [];
    		for (var i = 0; i < this._items.length; i++) {
    			if (this._items[i].fakeGroup.indexOf(fakeGroupName) >= 0) keepedItems.push(this._items[i]);
    		}
    		return keepedItems;
    	}

    	getItemsByType(type) {
    		var keepedItems = [];
    		for (var i = 0; i < this._items.length; i++) {
    			if (this._items[i] instanceof type) keepedItems.push(this._items[i]);
    		}
    		return keepedItems;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	deleteByFakeGroup(fakeGroupName) {
    		var keepedItems = [];
    		for (var i = 0; i < this._items.length; i++) {
    			// console.log("this._items[i].fakeGroup.indexOf(fakeGroupName)",this._items[i].fakeGroup.indexOf(fakeGroupName),fakeGroupName);
    			if (this._items[i].fakeGroup.indexOf(fakeGroupName) < 0) keepedItems.push(this._items[i]);
    			else this._items[i].destroy();
    		}
    		this._items = keepedItems;
    	}
    	getCurrentModel() {
    		return this._currentModel;
    	}
    	disable() {
    		for (var i = 0; i < this._items.length; i++) {
    			if (this._items[i].disable) this._items[i].disable();
    		}
    	}
    };

    /**
     * Base class to input form
     * @class
      * @extends M_.Outlet
     * @property {type} name
     * @property {type} value
     * @property {type} autoContainer
     * @property {type} hideContainerIfEmpty
     * @property {type} placeholder
     * @property {type} form
     * @property {type} inputType
     * @property {type} editable
     * @property {type} clsInput
     * @property {type} clsGroup
     * @property {type} clsLabel
     * @property {type} styleInput
     * @property {type} styleGroup
     * @property {type} styleLabel
     * @property {type} label
     * @property {type} labelPosition
     * @property {type} labelWidth
     * @property {type} labelFocusInput
     * @property {type} dontSave
     * @property {type} fakeGroup
     * @property {type} allowEmpty
     * @property {type} minLength
     * @property {type} maxLength
     * @property {type} disabled
     */
    M_.Form.Input = class extends M_.Outlet {
    	constructor(opts) {
    		var defaults = {
    			name: "",
    			value: "",
    			autoContainer: null,
    			// modelValue: '',
    			// modelKey: '',
    			hideContainerIfEmpty: false,
    			placeholder: "",
    			form: null,
    			inputType: "hidden",
    			tabindex: 0,
    			editable: true,
    			_inEdit: false,
    			_clsMore: "",
    			clsInput: "",
    			clsGroup: "",
    			clsLabel: "",
    			styleInput: "",
    			styleGroup: "",
    			styleLabel: "",
    			label: "",
    			labelPosition: "top",
    			labelWidth: 0,
    			labelFocusInput: true,
    			dontSave: false,
    			fakeGroup: [],
    			allowEmpty: true,
    			minLength: null,
    			maxLength: null,
    			disabled: false,
    			previousValue: null
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		if (typeof this.fakeGroup === "string") this.fakeGroup = [this.fakeGroup];

    		if ((this.labelPosition == "left" || this.labelPosition == "right") && this.labelWidth === 0) this.labelWidth = 0.3;

    		this.setValue(this.value);
    	}
    	/**
    	 * @param {type}
    	 */
    	setLabel(txt) {
    		this.container.find("label").html(txt);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		if (M_.Utils.isEmpty(this.id)) this.id = M_.Utils.id();
    		var v = this.value;
    		if ($.type(v) !== "string") v = "";
    		if (this.labelPosition == "left" && this.label !== "") {
    			this.clsLabel += " M_LabelLeft";
    			this.clsInput += " M_InputLeft";
    		}
    		if (this.labelPosition == "right" && this.label !== "") {
    			this.clsLabel += " M_LabelRight";
    			this.clsInput += " M_InputRight";
    		}
    		if (
    			this.labelWidth > 0 &&
    			this.label !== "" &&
    			(this.labelPosition == "left" || this.labelPosition == "right") &&
    			this.inputType != "checkbox"
    		) {
    			if (this.labelWidth <= 1) {
    				this.styleLabel += " width:" + this.labelWidth * 100 + "%;";
    				this.styleInput += " width:" + (100 - this.labelWidth * 100) + "%;";
    			} else {
    				this.styleLabel += " width:" + this.labelWidth + "px;";
    				this.styleInput += " width:calc(100% - " + this.labelWidth + "px);";
    			}
    		}

    		var html = "";
    		var readOnly = "";
    		var forattr = "";
    		var tabindex = "";
    		if (this.tabindex) tabindex = 'tabindex="' + this.tabindex + '"';
    		if (this.labelFocusInput) forattr = 'for="' + this.id + '"';
    		if (!this.editable) readOnly = "readonly";
    		if (this.inputType == "hidden") {
    			html += `<input ${readOnly} id="${this.id}" type="${this.inputType}" class="M_Input" name="${this.name}" value="${v}">`;
    		} else {
    			html += `<div style="${this.styleGroup}" class="M_FormGroup ${this.clsGroup}">`;
    			if (this.label !== "" && (this.labelPosition == "left" || this.labelPosition == "top"))
    				html += `<label style="${this.styleLabel}" class="${this.clsLabel}" ${forattr}>${this.label}</label>`;
    			// if (this._addInputGroup) html += `<div class="M_InputGroup">` ;
    			var multiple = "";
    			if (this.multiple) multiple = "multiple";
    			if (this.inputType == "file") {
    				html += `<input ${tabindex} ${readOnly} id="${this.id}" type="${this.inputType}" style="${this
    					.styleInput}" ${multiple} class="M_Input M_InputFile ${this.clsInput}" name="${this.name}" value="${v}" placeholder="${this
    					.placeholder}"><label class='M_InputFileLabel' for="${this.id}">${this.txtChooseFile}</label>`;
    			} else if (this.inputType == "textarea") {
    				if (!this.height) this.height = 100;
    				this.styleInput += " height:" + this.height + "px;";
    				html += `<textarea ${tabindex} placeholder="${this.placeholder}" ${readOnly} id="${this.id}" class="M_Input ${this
    					.clsInput}" style="${this.styleInput}" rows="${this.rows}" name="${this.name}">${v}</textarea>`;
    			} else if (this.inputType == "none") {
    				html += `<div id="${this.id}"></div>`;
    			} else {
    				var incremental = "";
    				if (this.incremental) incremental = 'incremental="incremental"';
    				html += `<input ${tabindex} ${readOnly} id="${this.id}" ${incremental} type="${this.inputType}" style="${this
    					.styleInput}" class="M_Input ${this._clsMore} ${this.clsInput}" name="${this.name}" value="${v}" placeholder="${this
    					.placeholder}">`;
    			}
    			// if (this._addInputGroup) html += `</div>` ;
    			if (this.label !== "" && (this.labelPosition == "bottom" || this.labelPosition == "right"))
    				html += `<label style="${this.styleLabel}" class="${this.clsLabel}" ${forattr}">${this.label}</label>`;
    			html += `</div>`;
    		}
    		// log("html",html)
    		this.container.append(html);
    		this.jEl = $("#" + this.id);
    		if (this.disabled) this.jEl.prop("disabled", true);
    		// this.jEl = this.container.find('.M_Input') ;
    		// this.jEl.attr('data-m-after', this.placeholder) ;
    	}
    	/**
    	 * @return {type}
    	 */
    	reset() {
    		this.setValue("");
    	}
    	/**
    	 * @return {type}
    	 */
    	disable() {
    		this.disabled = true;
    		this.jEl.prop("disabled", true);
    	}
    	/**
    	 * @return {type}
    	 */
    	enable() {
    		this.disabled = false;
    		this.jEl.prop("disabled", false);
    	}
    	/**
    	 * @return {type}
    	 */
    	valid() {
    		var ok = true,
    			val = this.getValue(),
    			err = "";
    		if (!this.allowEmpty && M_.Utils.isEmpty(val)) {
    			ok = false;
    			err = "Ce champs ne peut pas être vide\n";
    		}
    		if (this.minLength !== null && val.length < this.minLength) {
    			if (val.length === 0 && this.allowEmpty) {
    			} else {
    				ok = false;
    				err = "La longeur minimum est de " + this.minLength + " pour ce champs\n";
    			}
    		}
    		if (this.maxLength !== null && val.length > this.maxLength) {
    			ok = false;
    			err = "La longeur maximum est de " + this.maxLength + " pour ce champs\n";
    		}
    		// console.log("this.maxLength",this.maxLength);
    		this.informValid(ok);
    		return ok;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	informValid(ok) {
    		if (!ok) this.jEl.addClass("M_Error");
    		else this.jEl.removeClass("M_Error");
    	}
    	/**
    	 * @return {Boolean}
    	 */
    	isEmpty() {
    		// log("isEmpty","'"+this.value+"'")
    		var v = this.getValue();
    		if (v === "" || v === null) return true;
    		return false;
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		// console.log("setValuePrim", val);
    		this.previousValue = this.value;
    		this.value = val;
    		this.jEl.val(val);
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		this.value = this.jEl.val();
    		return this.value;
    	}
    	/**
    	 * @return {type}
    	 */
    	hide() {
    		this.container.hide();
    	}
    	/**
    	 * @return {type}
    	 */
    	show() {
    		this.container.show();
    	}
    };

    M_.Form.Slider = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			placeholder: "",
    			inputType: "perso",
    			jElCursor: null,
    			steps: 2,
    			value: 0,
    			labelLeft: "Left",
    			labelRight: "Right",
    			colorLeft: "red",
    			colorRight: "green",
    			labelWidth: 50
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    	create() {
    		if (M_.Utils.isEmpty(this.id)) this.id = M_.Utils.id();
    		var v = this.value;
    		var jEl = $(
    			"<div class='M_Slider'><label class='M_SliderLabelLeft'>" +
    				this.labelLeft +
    				"</label><div class='M_SliderBar'><div class='M_SliderCursor'></div></div><label class='M_SliderLabelRight'>" +
    				this.labelRight +
    				"</label><div class='M_Clear'></div></div>"
    		);
    		var wLabels = 0;
    		if (this.labelLeft === "") {
    			jEl.find(".M_SliderLabelLeft").remove();
    		} else {
    			wLabels += this.labelWidth;
    			jEl.find(".M_SliderLabelLeft").width(this.labelWidth);
    		}
    		if (this.labelRight === "") {
    			jEl.find(".M_SliderLabelRight").remove();
    		} else {
    			wLabels += this.labelWidth;
    			jEl.find(".M_SliderLabelRight").width(this.labelWidth);
    		}
    		this.container.append(jEl);
    		this.jEl = jEl.find(".M_SliderBar");
    		this.jElCursor = jEl.find(".M_SliderCursor");
    		this._spaceCursor = this.jElCursor.position().left;

    		this.jEl.css("width", "calc(100% - " + wLabels + "px)");

    		// this.jElCursor.width((this.jEl.width() - this._spaceCursor*2)/this.steps);
    		this.jEl.click(evt => {
    			var pos = 0;
    			if (this.steps == 2) {
    				if (!this.getValue()) pos = 1;
    				this.setValue(pos);
    			} else {
    				var parentOffset = $(evt.target)
    					.closest(".M_Slider")
    					.offset();
    				var relX = evt.pageX - parentOffset.left;
    				var w = this.jEl.width();
    				// console.log("w % this.steps", Math.ceil(relX / (w/this.steps)));
    				pos = Math.ceil(relX / (w / this.steps)) - 1;
    				// if (pos<0) pos = 0 ;
    				this.setValue(pos);
    			}
    			this.trigger("change", this, pos);
    		});
    	}
    	setPosition(pos) {
    		// console.log("pos",pos);
    		if (pos === false) pos = 0;
    		if (pos === true) pos = 1;
    		var w = this.jEl.width();
    		var l = pos * (w / this.steps) + 3;
    		if (pos === 0) {
    			this.jEl.removeClass("step1");
    			l += this._spaceCursor;
    		} else {
    			this.jEl.addClass("step1");
    		}
    		// if (pos===0) this.jEl.css('background-color',this.colorLeft) ;
    		// else this.jEl.css('background-color',this.colorRight) ;
    		this.jElCursor.transition({ left: l });
    	}
    	setValue(val) {
    		if (val === "") return;
    		// console.log("this.name,val",this.name,val);
    		this.previousValue = this.value;
    		this.value = val;
    		this.setPosition(val);
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		if (this.steps == 2) {
    			if (this.value == 1) return true;
    			return false;
    		}
    		return this.value;
    	}
    };

    /**
     * Hidden input form
     * @class
     * @extends M_.Form.Input
      */
    M_.Form.Hidden = class extends M_.Form.Input {
    	setValue(val) {
    		if (_.isPlainObject(val)) val = val[this.name];
    		super.setValue(val);
    	}
    };

    /**
     * A multi is a combobox + a list of values
     * @class
      * @extends M_.Form.Input
     * @property {type} inputType
     * @property {type} confirmDelete
     * @property {type} confirmDeleteMessage
     * @property {type} value
     * @property {type} onClickBtAdd
     */
    M_.Form.Multi = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			inputType: "none",
    			confirmDelete: false,
    			confirmDeleteMessage: "Etes-vous certain de vouloir supprimer ce mot clé ?",
    			value: [],
    			onClickBtAdd: null,
    			chooseValues: null,
    			chooseValuesAreNumbers: true
    		};
    		opts._idBtAddKeyword = M_.Utils.id();
    		if (opts.label) opts.label += " <span id='" + opts._idBtAddKeyword + "' class='fa fa-plus faa-pulse animated-hover'>";
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    		if (this.value.length) this.setValue(this.value);
    		$("#" + this._idBtAddKeyword).click(evt => {
    			evt.stopPropagation();
    			if (this.onClickBtAdd) this.onClickBtAdd(this, this.value);
    			else if (this.chooseValues) this.showChooseValues();
    		});
    	}
    	showChooseValues() {
    		let html = "";
    		_.each(this.chooseValues, c => {
    			let idTemp = M_.Utils.id();
    			let checked = "";
    			if (_.indexOf(this.value, c.key) >= 0) checked = "checked";
    			html += `<div class="M_FormMultiItem"><label for="${idTemp}"><input type='checkbox' id='${idTemp}' name='${idTemp}' data-id="${c.key}" ${checked}>${c.val}</label></div>`;
    		});
    		this.dd = new M_.Dropdown({
    			autoShow: true,
    			alignTo: $("#" + this._idBtAddKeyword),
    			html: html
    		});
    		this.dd.show();
    		this.dd.jEl.find(".M_FormMultiItem input").change(evt => {
    			let ids = [];
    			this.dd.jEl.find(".M_FormMultiItem input:checked").each((ind, el) => {
    				let id = $(el).attr("data-id");
    				let v2 = id;
    				if (!isNaN(id)) v2 = id * 1;
    				ids.push(v2);
    			});
    			this.setValue(ids);
    		});
    	}
    	/**
    	 * @return {type}
    	 */
    	drawContainer() {
    		// log("this.value",this.value,this.name);
    		this.jEl.empty();
    		_.each(this.value, val => {
    			let valtxt = "",
    				valid = "";
    			if (this.chooseValues) {
    				let v2 = val;
    				if (!isNaN(val)) v2 = val * 1;
    				let v = _.find(this.chooseValues, { key: v2 });
    				if (v) {
    					valid = v.key;
    					valtxt = v.val;
    				} else return;
    			} else {
    				valid = valtxt = val;
    			}
    			let html = `<div class="M_ComboboxMultiItem selected" data-kw-id="${valid}">${valtxt} <span class="fa fa-trash faa-pulse animated-hover"></span></div>`;
    			let jEl = $(html);
    			this.jEl.append(jEl);
    			jEl.find(".fa-trash").click(evt => {
    				if (this.confirmDelete) {
    					M_.Dialog.confirm("Confirmation effacement", this.confirmDeleteMessage, () => {
    						this.removeValue(
    							$(evt.target)
    								.parent()
    								.attr("data-kw-id")
    						);
    					});
    				} else {
    					this.removeValue(
    						$(evt.target)
    							.parent()
    							.attr("data-kw-id")
    					);
    				}
    			});
    		});
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	removeValue(valtoremove) {
    		// _.pull(this.value, val);
    		let oks = [];
    		_.each(this.value, val => {
    			if (val != valtoremove) oks.push(val);
    		});
    		this.value = oks;
    		this.drawContainer();
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		this.previousValue = this.value;
    		if (val === "") val = [];
    		this.value = val;
    		this.drawContainer();
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		return this.value;
    	}
    	/**
    	 * @param {type}
    	 */
    	addValue(val) {
    		this.value.push(val);
    	}
    };

    /**
     * Display stars to rate
     * @class
      * @extends M_.Form.Input
     * @property {type} inputType
     * @property {type} value
     * @property {type} nbStars
     */
    M_.Form.Rating = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			inputType: "none",
    			value: -1,
    			nbStars: 5
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    		this.jEl.addClass("M_FormRate");
    		for (var i = 0; i < this.nbStars; i++) {
    			var el = $("<div class='M_FormRateItem'>" + i + "</div>");
    			this.jEl.append(el);
    			el.mouseenter({ note: i }, evt => {
    				if (this._toExitStar) window.clearTimeout(this._toExitStar);
    				// log("mouseover", evt.data.note, $(evt.target))
    				// $(evt.target).addClass('over') ;
    				for (var i = 0; i < this.nbStars; i++) {
    					if (i <= evt.data.note * 1) this.jEl.find(".M_FormRateItem:nth-child(" + (i + 1) + ")").addClass("over");
    					else this.jEl.find(".M_FormRateItem:nth-child(" + (i + 1) + ")").removeClass("over");
    				}
    			});
    			el.mouseleave({ note: i }, evt => {
    				this._toExitStar = window.setTimeout(() => {
    					this._toExitStar = null;
    					this.setValue(this.value);
    				}, 300);
    			});
    			el.click({ note: i }, evt => {
    				this.setValue(evt.data.note * 1);
    			});
    		}
    		this.drawContainer();
    	}
    	/**
    	 * @return {type}
    	 */
    	drawContainer() {
    		// this.jEl.empty() ;
    		// this.jEl.removeClass('M_FormRate') ;
    		for (var i = 0; i < this.nbStars; i++) {
    			if (i <= this.value * 1) this.jEl.find(".M_FormRateItem:nth-child(" + (i + 1) + ")").addClass("over");
    			else this.jEl.find(".M_FormRateItem:nth-child(" + (i + 1) + ")").removeClass("over");
    		}
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	removeValue(val) {
    		_.pull(this.value, val);
    		this.drawContainer();
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		this.previousValue = this.value;
    		if (val === "") val = 0;
    		this.value = val;
    		this.drawContainer();
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		return this.value;
    	}
    	/**
    	 * @param {type}
    	 */
    	addValue(val) {
    		this.value.push(val);
    	}
    };

    /**
     * Input file form
     * @class
      * @extends M_.Form.Input
     * @property {type} placeholder
     * @property {type} inputType
     * @property {type} regxValidChar
     */
    M_.Form.File = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			placeholder: "",
    			inputType: "file",
    			txtChooseFile: "Choisir un fichier",
    			multiple: false,
    			regxValidChar: null
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		this._labelButton = $(this.jEl.get(0).nextElementSibling);

    		this.jEl.on("change", evt => {
    			let fileName = "";
    			if (this.jEl.files && this.jEl.files.length > 1)
    				fileName = (this.jEl.getAttribute("data-multiple-caption") || "").replace("{count}", this.jEl.files.length);
    			else fileName = evt.target.value.split("\\").pop();
    			if (fileName) this._labelButton.html(fileName);
    			else this._labelButton.html(this.txtChooseFile);

    			this.trigger("change", this, evt);
    		});
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		this.value = this.jEl.val();
    		// if (this.jEl.get(0).files.length>0) return this.jEl.get(0).files[0] ;
    		return this.value;
    		// return this.value ;
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		this.previousValue = this.value;
    		this.value = val;
    	}
    	setLabelButton(txt) {
    		this._labelButton.html(txt);
    	}
    	reset() {
    		this._labelButton.html(this.txtChooseFile);
    	}
    	// reset() {
    	// 	if (!this.jEl) return ;
    	// 	var par = this.jEl.parent() ;
    	// 	this.jEl.remove() ;
    	// 	// this.container.empty() ;
    	//
    	// 	var tabindex = "" ;
    	// 	var readOnly = "" ;
    	// 	if (this.tabindex) tabindex = 'tabindex="'+this.tabindex+'"' ;
    	// 	if (!this.editable) readOnly = "readonly" ;
    	// 	var multiple = "" ;
    	// 	if (this.multiple) multiple = "multiple" ;
    	// 	this.jEl = $(`<input ${tabindex} ${readOnly} id="${this.id}" type="${this.inputType}" style="${this.styleInput}" ${multiple} class="M_Input ${this.clsInput}" name="${this.name}" value="" placeholder="${this.placeholder}">`) ;
    	// 	par.append(this.jEl) ;
    	// 	this.jEl.on('change', (evt)=> {
    	// 		this.trigger("change", this, evt) ;
    	// 	}) ;
    	// }
    };

    /**
     * Input text form
     * @class
      * @extends M_.Form.Input
     * @property {type} placeholder
     * @property {type} inputType
     * @property {type} regxValidChar
     */
    M_.Form.Text = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			placeholder: "",
    			inputType: "text",
    			regxValidChar: null,
    			selectOnFocus: false
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		// this.jEl.keypress((evt)=> {
    		// 	// log("evt.which",evt.which)
    		// 	if (this.jEl.hasClass('M_editablePlaceholder')) {
    		// 		this.jEl.removeClass('M_editablePlaceholder') ;
    		// 		this.jEl.empty() ;
    		// 	}
    		// 	if(evt.which == 13) {
    		// 		evt.preventDefault() ;
    		// 		this.form.edit(false) ;
    		// 		this.jEl.blur() ;
    		// 	}
    		// }) ;
    		// this.jEl.keydown((evt)=> {
    		// 	// log("keydownevt.which",evt.which)
    		// }) ;
    		this.jEl.keyup(evt => {
    			if (evt.which == 13) {
    				if (this.onEnterInput(evt) !== false) {
    					this.onKeyup(evt);
    				}
    			} else this.onKeyup(evt);
    		});
    		this.jEl.keypress(evt => {
    			this.onKeyPress(evt);
    		});
    		if (this.selectOnFocus) {
    			this.jEl.focus(evt => {
    				evt.preventDefault();
    				this.jEl.select();
    			});
    		}
    	}
    	onEnterInput(evt) {}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	onKeyup(evt) {
    		// this.value = this.jEl.val() ;
    		this.trigger("keyup", this, evt);
    		this.trigger("update", this, this.getValue());
    		this.valid();
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	onKeyPress(evt) {
    		this.trigger("keypress", this, evt);
    		if (evt.which == 13) return true;
    		if (evt.which == 9) return true;
    		if (evt.which == 8) return true;
    		if (!this.validChar(String.fromCharCode(evt.which))) {
    			evt.preventDefault();
    			evt.stopPropagation();
    			evt.stopImmediatePropagation();
    			// this.informValid(2);
    			return false;
    		}
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	validChar(txt) {
    		if (this.regxValidChar === null) return true;
    		var reg = new RegExp(this.regxValidChar);
    		var ok = reg.test(txt);
    		return ok;
    	}
    };

    M_.Form.Search = class extends M_.Form.Text {
    	constructor(opts) {
    		var defaults = {
    			inputType: "search",
    			incremental: true
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		if (M_.Utils.isEventSupported("search")) {
    			this.jEl.on("search", evt => {
    				this.trigger("search", this, evt);
    			});
    		} else {
    			this.jEl.on("keyup", evt => {
    				this.trigger("search", this, evt);
    			});
    		}
    	}
    };

    /**
     * Input textarea form
     * @class
      * @extends M_.Form.Text
     * @property {type} inputType
     * @property {type} acceptTabulation
     * @property {type} rows
     */
    M_.Form.Textarea = class extends M_.Form.Text {
    	constructor(opts) {
    		var defaults = {
    			inputType: "textarea",
    			acceptTabulation: false,
    			rows: 4
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		if (this.acceptTabulation) {
    			this.jEl.on("keydown", e => {
    				var keyCode = e.keyCode || e.which;
    				if (keyCode == 9) {
    					e.preventDefault();
    					var start = this.jEl.get(0).selectionStart;
    					var end = this.jEl.get(0).selectionEnd;
    					this.jEl.val(this.jEl.val().substring(0, start) + "\t" + this.jEl.val().substring(end));
    					this.jEl.get(0).selectionStart = this.jEl.get(0).selectionEnd = start + 1;
    				}
    			});
    		}
    	}
    };

    M_.Form.Password = class extends M_.Form.Text {
    	constructor(opts) {
    		var defaults = {
    			inputType: "password",
    			checkstrength: true
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		if (this.checkstrength) {
    			this.jElPass = $("<div class='M_DivPassword'>OK</div>");
    			this.jElPass.hide();
    			this.container.append(this.jElPass);
    			this.container.css("position", "relative");
    			this.jEl.on("keyup", e => {
    				var pos = this.jEl.offset();
    				this.jElPass.show();
    				var score = this.checkPassword();
    				this.jElPass.removeClass("bg_col3 bg_col5 bg_col2");
    				if (score < 2) {
    					this.jElPass.addClass("bg_col3").html("Mauvais");
    				} else if (score < 4) {
    					this.jElPass.addClass("bg_col5").html("Moyen");
    				} else {
    					this.jElPass.addClass("bg_col2").html("Bon");
    				}
    				this.jElPass
    					.offset({
    						top: pos.top,
    						left: pos.left + this.jEl.outerWidth() - this.jElPass.outerWidth()
    					})
    					.outerHeight(this.jEl.outerHeight());
    			});
    		}
    	}
    	checkPassword() {
    		var v = this.jEl.val();
    		var err = [];
    		var score = 0;
    		if (v.length < this.minLength) {
    			err.push(this.minLength + " char minimum");
    		} else score++;
    		var re = /[0-9]/;
    		if (!re.test(v)) {
    			err.push("password must contain at least one number (0-9)!");
    		} else score++;
    		re = /[a-z]/;
    		if (!re.test(v)) {
    			err.push("password must contain at least one lowercase letter (a-z)!");
    		} else score++;
    		re = /[A-Z]/;
    		if (!re.test(v)) {
    			err.push("password must contain at least one uppercase letter (A-Z)!");
    		} else score++;
    		return score;
    	}
    };

    /**
     * A wysiwyg editor
     * @class
      * @extends M_.Form.Textarea
     * @property {type} heightEditor
     */
    M_.Form.TextEditor = class extends M_.Form.Textarea {
    	constructor(opts) {
    		var defaults = {
    			heightEditor: 200
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		super.create();

    		// var h = this.jEl.height() ;
    		this.jEl.hide();
    		var idTemp1 = M_.Utils.id();
    		var idTemp2 = M_.Utils.id();
    		this.jEl.after("<div id='" + idTemp1 + "'></div><div><div class='M_FormEditor-Content' id='" + idTemp2 + "'></div></div>");
    		$("#" + idTemp2).height(this.heightEditor);
    		this.editor = new M_.Editor({
    			buttonsContainer: $("#" + idTemp1),
    			container: $("#" + idTemp2)
    		});
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		this.previousValue = this.value;
    		this.value = val;
    		this.jEl.val(val);
    		this.editor.container.html(val);
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		var val = this.editor.container.html();
    		this.jEl.val(val);
    		return val;
    	}
    	/**
    	 * @return {type}
    	 */
    	getEditor() {
    		return this.editor;
    	}
    };

    /**
     * A radio buttons group
     * @class
      * @extends M_.Form.Input
     * @property {type} inputType
     * @property {type} radioPosition
     * @property {type} items
     */
    M_.Form.RadioGroup = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			inputType: "radiogroup",
    			radioPosition: "inline", // inline | col
    			items: []
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		// var id1 = M_.Utils.id(),
    		// 	id2 = M_.Utils.id() ;

    		if (M_.Utils.isEmpty(this.id)) this.id = M_.Utils.id();
    		var v = this.value;
    		if ($.type(v) !== "string") v = "";
    		if ((this.labelPosition == "left" || this.labelPosition == "right") && this.label !== "") {
    			this.clsLabel += " M_LabelLeft";
    			this.clsInput += " M_InputLeft";
    		}
    		if (
    			this.labelWidth > 0 &&
    			this.label !== "" &&
    			(this.labelPosition == "left" || this.labelPosition == "right") &&
    			this.inputType != "checkbox"
    		) {
    			if (this.labelWidth <= 1) {
    				this.styleLabel += " width:" + this.labelWidth * 100 + "%;";
    				this.styleInput += " width:" + (100 - this.labelWidth * 100) + "%;";
    			} else {
    				this.styleLabel += " width:" + this.labelWidth + "px;";
    				this.styleInput += " width:calc(100% - " + this.labelWidth + "px);";
    			}
    		}

    		var html = "";
    		var readOnly = "";
    		var forattr = "";
    		if (this.labelFocusInput) forattr = 'for="' + this.id + '"';
    		if (!this.editable) readOnly = "readonly";

    		// var html = `<div class='M_HorizontalForm'></div></div>` ;

    		html += `<div style="${this.styleGroup}" class="M_FormGroup ${this.clsGroup}">`;
    		if (this.label !== "" && (this.labelPosition == "left" || this.labelPosition == "top"))
    			html += `<label style="${this.styleLabel}" class="${this.clsLabel}" ${forattr}>${this.label}</label>`;
    		html += `<div id="${this.id}" type="${this.inputType}" style="${this.styleInput}" class="M_Input ${this.clsInput}">`;
    		var nameTemp = M_.Utils.id();
    		_.each(this.items, item => {
    			let idTemp = M_.Utils.id();
    			html += `<div class="M_RadioGroupItem ${this
    				.radioPosition}"><input type='radio' name="${nameTemp}" id="${idTemp}" value="${item.key}" class="M_InputRight"/><label for="${idTemp}" class="M_LabelRight">${item.val}</label></div>`;
    		});
    		html += `</div></div>`;

    		this.container.append(html);
    		this.jEl = this.container.find(".M_HorizontalForm");

    		this.container.find("input").click(evt => {
    			this.trigger("change", this, this.getValue(), evt);
    		});
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		this.previousValue = this.value;
    		this.value = val;
    		this.container.find("input").each((ind, el) => {
    			if (el.value == this.value) $(el).prop("checked", true);
    			else $(el).prop("checked", false);
    		});
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		var v = this.container.find("input:checked").val();
    		return v;
    	}
    };

    /**
     * Input checkbox form
     * @class
      * @extends M_.Form.Input
     * @property {type} inputType
     * @property {type} labelWidth
     * @property {type} labelFocusInput
     * @property {type} labelPosition
     */
    M_.Form.Checkbox = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			inputType: "checkbox",
    			labelWidth: 100,
    			labelFocusInput: true,
    			labelPosition: "right",
    			threeStates: false,
    			indeterminate: false,
    			previousValue: false
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);

    		if (this.threeStates) {
    			var v = 0;
    			if (this.value) v = 1;
    			if (this.indeterminate) v = 2;
    			this.jEl.data("checked", v);
    			if (v === 2) this.jEl.prop("indeterminate", true);
    		}
    		this.jEl.change(evt => {
    			if (this.indeterminate) {
    				switch (this.jEl.data("checked")) {
    					// unchecked, going indeterminate
    					case 0:
    						this.jEl.data("checked", 1);
    						this.jEl.prop("indeterminate", true);
    						break;
    					// indeterminate, going checked
    					case 1:
    						this.jEl.data("checked", 2);
    						this.jEl.prop("indeterminate", false);
    						this.jEl.prop("checked", true);
    						break;
    					// checked, going unchecked
    					default:
    						this.jEl.data("checked", 0);
    						this.jEl.prop("indeterminate", false);
    						this.jEl.prop("checked", false);
    				}
    			}
    			this.trigger("change", this, this.getValue(), evt);
    		});
    		if (this.indeterminate) this.jEl.prop("indeterminate", true);
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		// console.log("val", val);
    		if (val === "" || val === "false" || val === "0" || val === 0) val = false;
    		if (val === "true" || val === "1" || val === 1) val = true;
    		this.previousValue = this.value;
    		this.value = val;
    		if (val) this.jEl.prop("checked", true);
    		else this.jEl.prop("checked", false);
    		if (this.threeStates) {
    			var v = 0;
    			if (val) v = 1;
    			if (val == 2) v = 2;
    			this.jEl.data("checked", v);
    			if (v === 2) this.jEl.prop("indeterminate", true);
    			else this.jEl.prop("indeterminate", false);
    		}
    	}
    	imcheckbox() {
    		return "ok";
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		if (this.threeStates) {
    			if (this.jEl.prop("indeterminate")) return 2;
    		}
    		return this.jEl.is(":checked");
    	}
    };

    /**
     * Input number form ; only number are accepted
     * @class
      * @extends M_.Form.Text
     * @property {type} decimalLength
     * @property {type} decimalSeparator
     * @property {type} decimalForced
     * @property {type} allowNegative
     * @property {type} valueMax
     * @property {type} valueMin
     * @property {type} startEmpty
     * @property {type} allowComparison allow signs : > or < or = or >= or <=
     */
    M_.Form.Number = class extends M_.Form.Text {
    	constructor(opts) {
    		var defaults = {
    			// inputType: 'number',
    			decimalLength: 0,
    			decimalSeparator: ",",
    			decimalForced: false,
    			allowNegative: true,
    			valueMax: null,
    			valueMin: null,
    			startEmpty: false,
    			allowComparison: false
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    	// onKeyup(evt) {}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	validChar(txt) {
    		var re = "\\d";
    		if (this.allowNegative) re += "|\\-";
    		if (this.decimalLength > 0) re += "|\\" + this.decimalSeparator;
    		if (this.allowComparison) re += "|>|<|=";
    		this.regxValidChar = re;
    		var ok = super.validChar(txt);
    		if (this.maxLength && (this.jEl.val() + "").length >= this.maxLength) ok = false;
    		return ok;
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(v) {
    		// log("setValue",v)
    		if (this.startEmpty && (v === "" || v === null || typeof v === "undefined")) return this.jEl.val("");
    		v = v * 1;
    		this.value = v;
    		var v2 = v;
    		v2 = (v2 + "").replace(".", this.decimalSeparator);
    		if (this.decimalForced) {
    			let dec = v % 1,
    				nbDec = Math.pow(10, this.decimalLength),
    				dec2 = Math.floor(dec * nbDec);
    			dec2 = M_.Utils.str_pad(dec2, this.decimalLength, "0", -1);
    			// log("dec",dec, nbDec, dec2)
    			v2 = Math.floor(v) + this.decimalSeparator + dec2;
    		}
    		this.jEl.val(v2);
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		if (this.startEmpty && this.jEl.val() === "") return null;
    		var v = (this.jEl.val() + "").replace(this.decimalSeparator, ".");
    		this.value = v * 1;
    		if (this.allowComparison) this.value = v;
    		return this.value;
    	}
    };

    /**
     * Input price form ; only number are accepted
     * @class
      * @extends M_.Form.Number
     * @property {type} decimalLength
     * @property {type} decimalSeparator
     * @property {type} decimalForced
     * @property {type} allowNegative
     */
    M_.Form.Price = class extends M_.Form.Number {
    	constructor(opts) {
    		var defaults = {
    			decimalLength: 2,
    			decimalSeparator: ",",
    			decimalForced: true,
    			allowNegative: true
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    };

    /**
     * Input color form ; TO implement !!!!
     * @class
      * @extends M_.Form.Input
     * @property {type} inputType
     */
    M_.Form.Color = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			inputType: "color"
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    };

    /**
     * A base class to add a picker to input text form
     * @class
      * @extends M_.Form.Text
     * @property {type} hidePicker
     * @property {type} showDropdownOnFocus
     * @property {type} icon
     * @property {type} containerDropdown
     */
    M_.Form.Picker = class extends M_.Form.Text {
    	constructor(opts) {
    		var defaults = {
    			alwaysDropdownBelow: false,
    			hasDropdown: true,
    			hidePicker: false,
    			showDropdownOnFocus: true,
    			icon: "fa fa-caret-down",
    			containerDropdown: null,
    			_clsMore: "M_Combobox",
    			stylePicker: "",
    			dropdownOpts: {},
    			dropdown: null
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		if (!optsTemp.containerDropdown) optsTemp.containerDropdown = optsTemp.container;
    		optsTemp.containerDropdown = "body";
    		// log("optsTemp.containerDropdown",optsTemp.containerDropdown,optsTemp.container)

    		super(optsTemp);

    		if (this.showDropdownOnFocus && this.hasDropdown) {
    			this._sameEventEventClick = false;
    			this.jEl.on("click", evt => {
    				// focus
    				// console.log("this._sameEventEventClick", this._sameEventEventClick);
    				if (!this._sameEventEventClick) this.showDropdown(evt);
    				this._sameEventEventClick = true;
    				M_.Utils.delay(
    					() => {
    						this._sameEventEventClick = false;
    					},
    					200,
    					"_sameEventEventClick"
    				);
    			});
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	createDropdown() {
    		if (this.dropdown) return;
    		if (!this.hasDropdown) return;
    		// console.log("createDropdown");
    		var optsTemp = $.extend(
    			{},
    			{
    				alignTo: this.jEl,
    				autoShow: false,
    				destroyOnHide: true,
    				container: this.containerDropdown,
    				alwaysDropdownBelow: this.alwaysDropdownBelow,
    				listeners: [
    					[
    						"hide",
    						() => {
    							this.dropdown = null;
    						}
    					]
    				]
    			},
    			this.dropdownOpts
    		);
    		this.dropdown = new M_.Dropdown(optsTemp);
    	}
    	/**
    	 * @return {Boolean}
    	 */
    	isDropdownVisible() {
    		if (!this.dropdown) return false;
    		return this.dropdown.isVisible();
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	showDropdown(evt) {
    		// log("showDropdown")
    		M_.Help.hideMHelp();
    		if (!this.hasDropdown) return;
    		evt.stopPropagation();
    		if (!this.dropdown) this.createDropdown();
    		this.dropdown.show();
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	clickPicker(evt) {
    		this.showDropdown(evt);
    		this.trigger("clickpicker", this, evt);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		super.create();

    		if (!this.hidePicker) {
    			var moreCls = "",
    				moreStyle = "";
    			if ((this.labelPosition == "left" || this.labelPosition == "right") && this.label !== "") {
    				moreCls += " M_InputLeft";
    				moreStyle += " width:calc(100% - " + this.labelWidth + "px);";
    			}
    			this.jEl.outerWidth("100%");
    			this.jEl.removeClass("M_InputLeft");
    			this.jEl.wrap("<div class='M_FormInputGroup " + moreCls + "' style='" + moreStyle + "'>");
    			var htmlCaret = `<div class="M_FormCaret ${this.icon}" style='${this.stylePicker}'></div>`;
    			this.caret = $(htmlCaret);
    			this.container.find(".M_FormInputGroup").append(this.caret);
    			this.caret.click(evt => {
    				this.clickPicker(evt);
    			});
    		}
    	}
    };

    /**
     * Input date form ; display a date picker and a month view
     * @class
      * @extends M_.Form.Picker
     * @property {type} icon
     * @property {type} dateFormat
     * @property {type} dateFormatInput
     */
    M_.Form.Date = class extends M_.Form.Picker {
    	constructor(opts) {
    		var defaults = {
    			icon: "fa fa-calendar",
    			dateFormat: "DD/MM/YYYY",
    			dateFormatInput: "YYYY-MM-DD",
    			noDays: false,
    			showWeekNumber: false,
    			selectWeek: false,
    			disabledDates: null
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		// if (!optsTemp.value) optsTemp.value = moment() ;
    		super(optsTemp);

    		// this.jEl.click((evt)=> {
    		// 	evt.stopPropagation() ;
    		// 	// if (!this.dropdown) this.createDropdown() ;
    		// 	// this.dropdown.show() ;
    		// })
    		// this.jEl.on('M_DropdownShow', (evt)=> {
    		// 	if (!this.dropdown) this.createDropdown() ;
    		// 	this.dropdown.show() ;
    		// }) ;
    	}
    	/**
    	 * @return {type}
    	 */
    	createDropdown() {
    		this.dropdown = new M_.Dropdown({
    			destroyOnHide: true,
    			alignTo: this.jEl,
    			container: this.containerDropdown,
    			autoSize: false,
    			alwaysDropdownBelow: this.alwaysDropdownBelow,
    			listeners: [
    				[
    					"destroy",
    					() => {
    						this.dropdown = null;
    						this.calendar.destroy();
    					}
    				]
    			]
    		});
    		this.calendar = new M_.CalendarMonth({
    			controller: this,
    			container: this.dropdown.jEl,
    			noDays: this.noDays,
    			showWeekNumber: this.showWeekNumber,
    			selectWeek: this.selectWeek,
    			disabledDates: this.disabledDates,
    			listeners: [
    				[
    					"selected",
    					(cal, date) => {
    						// log("date",cal, date)
    						// this.container.html(date.format('DD/MM/YYYY')) ;
    						this.setValue(date);
    						this.trigger("change", this, date);
    						this.dropdown.hide();
    					}
    				],
    				[
    					"viewedChanged",
    					() => {
    						this.dropdown.realign();
    					}
    				]
    			]
    		});
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	showDropdown(evt) {
    		super.showDropdown(evt);
    		if (moment.isMoment(this.value) && this.calendar) {
    			this.calendar.setDateViewed(this.value);
    			this.calendar.setDateSelected(this.value);
    		}
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	onKeyup(evt) {
    		super.onKeyup(evt);
    		if (this.isDropdownVisible()) {
    			var m = moment(this.jEl.val(), this.dateFormat);
    			if (m.isValid()) {
    				this.value = m;
    				if (this.calendar) {
    					this.calendar.setDateViewed(this.value);
    					this.calendar.setDateSelected(this.value);
    				}
    			}
    		}
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		if (val === "" || !val || val == "0000-00-00" || val == "0000-00-00 00:00:00") val = "";
    		else if (moment.isMoment(val)) {
    			if (!val.isValid()) val = "";
    			// else val = val.format('YYYY-MM-DD') ;
    		} else val = moment(val, this.dateFormatInput);
    		this.value = val;
    		// if (val!=='') log("format",val,val.format(this.dateFormat))
    		if (val !== "") {
    			this.jEl.val(val.format(this.dateFormat));
    		} else this.jEl.val(val);
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		var v = this.jEl.val();
    		if (v !== "") {
    			v = moment(v, this.dateFormat).startOf("day");
    			if (!serialized) return v;
    			v = v.format("YYYY-MM-DD");
    		}
    		return v;
    	}
    };

    M_.Form.DateWeek = class extends M_.Form.Date {
    	constructor(opts) {
    		var defaults = {
    			// icon: 'fa fa-clock-o',
    			dateFormat: "YYYY-\\SWW",
    			dateFormatInput: "YYYY-MM-DD",
    			showWeekNumber: true,
    			selectWeek: true
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);
    	}
    };

    /**
     * Input hour form ; display a date picker and a hour view
     * @class
      * @extends M_.Form.Date
     * @property {type} icon
     * @property {type} dateFormat
     * @property {type} dateFormatInput
     * @property {type} incrementHour
     * @property {type} incrementMinute
     * @property {type} incrementSecond
     */
    M_.Form.Hour = class extends M_.Form.Date {
    	constructor(opts) {
    		var defaults = {
    			icon: "fa fa-clock-o",
    			dateFormat: "HH:mm",
    			dateFormatInput: "HH:mm",
    			incrementHour: 1,
    			incrementMinute: 10,
    			incrementSecond: 5
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);
    	}
    	/**
    	 * @return {type}
    	 */
    	createDropdown() {
    		// log("createDropdown")
    		var html = `
    		<div class="">
    			<div class="M_Col6 M_FormHour_hour_up"><span class="fa fa-chevron-up"></span></div>
    			<div class="M_Col6 M_FormHour_minute_up"><span class="fa fa-chevron-up"></span></div>
    			<div class="M_Col6 M_FormHour_second_up"><span class="fa fa-chevron-up"></span></div>
    			<div class="M_Clear"></div>

    			<div class="M_Col6"><input type="text" class="form-control M_FormHour_hour" placeholder="" maxlength="2"></div>
    			<div class="M_Col6"><input type="text" class="form-control M_FormHour_minute" placeholder="" maxlength="2"></div>
    			<div class="M_Col6"><input type="text" class="form-control M_FormHour_second" placeholder="" maxlength="2"></div>
    			<div class="M_Clear"></div>

    			<div class="M_Col6 M_FormHour_hour_down"><span class="fa fa-chevron-down"></span></div>
    			<div class="M_Col6 M_FormHour_minute_down"><span class="fa fa-chevron-down"></span></div>
    			<div class="M_Col6 M_FormHour_second_down"><span class="fa fa-chevron-down"></span></div>
    			<div class="M_Clear"></div>
    			<div>
    		</div>
    		`;
    		this.dropdown = new M_.Dropdown({
    			destroyOnHide: true,
    			alignTo: this.jEl,
    			autoSize: false,
    			html: html,
    			listeners: [
    				[
    					"destroy",
    					() => {
    						this.dropdown = null;
    					}
    				]
    			]
    		});
    		this.jElDropdown = this.dropdown.jEl;
    		this.value = moment();
    		// log("this.value",this.value)
    		if (this.jEl.val().length > 0) this.value = moment(this.jEl.val(), this.dateFormat);
    		this.setToCurrent();
    		if (this.dateFormat.indexOf("s") < 0) {
    			this.jElDropdown.find(".M_FormHour_second_up, .M_FormHour_second_down").hide();
    			this.jElDropdown
    				.find(".M_FormHour_second")
    				.parent()
    				.hide();
    			this.jElDropdown
    				.find(".col-lg-4")
    				.removeClass("col-lg-4")
    				.addClass("col-lg-6");
    			this.jElDropdown.find(".dropdown-menu").addClass("short");
    		}
    		this.jElDropdown.find(".M_FormHour_hour_up").off("click");
    		this.jElDropdown.find(".M_FormHour_hour_up").on(
    			"click",
    			$.proxy(function(evt) {
    				// log("this.value",this.value)
    				var m = this.value.hours() + this.incrementHour;
    				m = Math.floor(m / this.incrementHour) * this.incrementHour;
    				this.value.hours(m);
    				this.setToCurrent();
    				this.setValue(this.value);
    			}, this)
    		);
    		this.jElDropdown.find(".M_FormHour_minute_up").off("click");
    		this.jElDropdown.find(".M_FormHour_minute_up").on(
    			"click",
    			$.proxy(function(evt) {
    				var m = this.value.minutes() + this.incrementMinute;
    				m = Math.floor(m / this.incrementMinute) * this.incrementMinute;
    				this.value.minutes(m);
    				this.setToCurrent();
    				this.setValue(this.value);
    			}, this)
    		);
    		this.jElDropdown.find(".M_FormHour_second_up").off("click");
    		this.jElDropdown.find(".M_FormHour_second_up").on(
    			"click",
    			$.proxy(function(evt) {
    				var m = this.value.seconds() + this.incrementSecond;
    				m = Math.floor(m / this.incrementSecond) * this.incrementSecond;
    				this.value.seconds(m);
    				this.setToCurrent();
    				this.setValue(this.value);
    			}, this)
    		);
    		this.jElDropdown.find(".M_FormHour_hour_down").off("click");
    		this.jElDropdown.find(".M_FormHour_hour_down").on(
    			"click",
    			$.proxy(function(evt) {
    				var m = this.value.hours() - this.incrementHour;
    				m = Math.floor(m / this.incrementHour) * this.incrementHour;
    				this.value.hours(m);
    				this.setToCurrent();
    				this.setValue(this.value);
    			}, this)
    		);
    		this.jElDropdown.find(".M_FormHour_minute_down").off("click");
    		this.jElDropdown.find(".M_FormHour_minute_down").on(
    			"click",
    			$.proxy(function(evt) {
    				var m = this.value.minutes() - this.incrementMinute;
    				m = Math.floor(m / this.incrementMinute) * this.incrementMinute;
    				this.value.minutes(m);
    				this.setToCurrent();
    				this.setValue(this.value);
    			}, this)
    		);
    		this.jElDropdown.find(".M_FormHour_second_down").off("click");
    		this.jElDropdown.find(".M_FormHour_second_down").on(
    			"click",
    			$.proxy(function(evt) {
    				var m = this.value.seconds() - this.incrementSecond;
    				m = Math.floor(m / this.incrementSecond) * this.incrementSecond;
    				this.value.seconds(m);
    				this.setToCurrent();
    				this.setValue(this.value);
    			}, this)
    		);
    		this.jElDropdown.find(".dropdown-menu").show();
    	}
    	/**
    	 * To document
    	 */
    	setToCurrent() {
    		this.jElDropdown.find(".M_FormHour_hour").val(this.value.format("HH"));
    		this.jElDropdown.find(".M_FormHour_minute").val(this.value.format("mm"));
    		this.jElDropdown.find(".M_FormHour_second").val(this.value.format("ss"));
    	}
    	getValue() {
    		var v = this.jEl.val();
    		var t = v.split(":");
    		var m = moment()
    			.hours(t[0])
    			.minutes(t[1])
    			.seconds(0);
    		// console.log("v",v,m,t);
    		return m;
    	}
    	setValue(val) {
    		var val2;
    		// console.log("val",val,moment().utcOffset());
    		if (!val) val = "00:00";
    		if (typeof val == "string") {
    			var v = "";
    			if (val.length > 11) v = val.substring(11, 16);
    			else v = val;
    			var t = v.split(":");
    			val2 = moment()
    				.hours(t[0])
    				.minutes(t[1])
    				.seconds(0); //.add(moment().utcOffset(),'minutes')
    		} else val2 = moment(val); //.add(moment().utcOffset(),'minutes')
    		this.jEl.val(val2.format(this.dateFormat));
    		this.value = val2;
    	}
    };

    /**
     * Input datehour form ; display 2 pickers for date and hour
     * @class
      * @extends M_.Form.Input
     * @property {type} configDateDef
     * @property {type} configHourDef
     */
    M_.Form.DateHour = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			configDateDef: {
    				label: "",
    				// labelPosition: 'left',
    				// labelWidth: 3,
    				name: "",
    				containerWidth: "64%",
    				required: true
    			},
    			configHourDef: {
    				label: "",
    				name: "",
    				containerWidth: "36%",
    				incrementMinute: 15,
    				required: false
    			}
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);

    		// if ((this.labelPosition=='left' || this.labelPosition=='right') && this.labelWidth===0) this.labelWidth = 0.3 ;
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		var id1 = M_.Utils.id(),
    			id2 = M_.Utils.id();

    		if (M_.Utils.isEmpty(this.id)) this.id = M_.Utils.id();
    		var v = this.value;
    		if ($.type(v) !== "string") v = "";
    		if ((this.labelPosition == "left" || this.labelPosition == "right") && this.label !== "") {
    			this.clsLabel += " M_LabelLeft";
    			this.clsInput += " M_InputLeft";
    		}
    		if (
    			this.labelWidth > 0 &&
    			this.label !== "" &&
    			(this.labelPosition == "left" || this.labelPosition == "right") &&
    			this.inputType != "checkbox"
    		) {
    			if (this.labelWidth <= 1) {
    				this.styleLabel += " width:" + this.labelWidth * 100 + "%;";
    				this.styleInput += " width:" + (100 - this.labelWidth * 100) + "%;";
    			} else {
    				this.styleLabel += " width:" + this.labelWidth + "px;";
    				this.styleInput += " width:calc(100% - " + this.labelWidth + "px);";
    			}
    		}

    		var html = "";
    		var readOnly = "";
    		var forattr = "";
    		if (this.labelFocusInput) forattr = 'for="' + this.id + '"';
    		if (!this.editable) readOnly = "readonly";

    		// var html = `<div class='M_HorizontalForm'></div></div>` ;

    		html += `<div style="${this.styleGroup}" class="M_FormGroup ${this.clsGroup}">`;
    		if (this.label !== "" && (this.labelPosition == "left" || this.labelPosition == "top"))
    			html += `<label style="${this.styleLabel}" class="${this.clsLabel}" ${forattr}>${this.label}</label>`;
    		html += `<div id="${this.id}" type="${this.inputType}" style="${this.styleInput}" class="M_Input ${this
    			.clsInput}"><div id='${id1}' class='M_FloatLeft'></div><div id='${id2}' class='M_FloatLeft'></div>`;
    		html += `</div>`;

    		this.container.append(html);
    		this.jEl = this.container.find(".M_HorizontalForm");

    		var configDate = $.extend({}, this.configDateDef, this.configDate);
    		var configHour = $.extend({}, this.configHourDef, this.configHour);
    		configDate.container = $("#" + id1);
    		configHour.container = $("#" + id2);
    		$("#" + id1).width(configDate.containerWidth);
    		$("#" + id2).width(configHour.containerWidth);
    		this.formDate = new M_.Form.Date(configDate);
    		this.formHour = new M_.Form.Hour(configHour);
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		this.formDate.setValue(val);
    		this.formHour.setValue(val);
    	}
    	/**
    	 * @param  {Boolean}
    	 * @return {type}
    	 */
    	getValue(serialized = false) {
    		var d1 = this.formDate.getValue();
    		if (d1 !== "") {
    			var d = moment(d1);
    			var d2 = this.formHour.getValue();
    			// console.log("d2",d2,d2.hours(),d2.minutes(),d2.seconds(),d2.format('HH:mm'),d2.utcOffset());
    			if (d2) {
    				// d2.local() ;
    				// log(".utcOffset()",d2.utcOffset())
    				d.hours(d2.hours());
    				d.minutes(d2.minutes()); //+d2.utcOffset()
    				d.seconds(d2.seconds());
    			}
    			return d;
    		}
    		return d1;
    	}
    };

    /**
     * Input combobox form ; display a comobobox
     * @class
      * @extends M_.Form.Picker
     * @property {type} store
     * @property {type} modelKey
     * @property {type} modelValue
     * @property {type} mode
     * @property {type} editable
     * @property {type} useRawValue
     * @property {type} useZeroIfEmpty
     */
    M_.Form.Combobox = class extends M_.Form.Picker {
    	constructor(opts) {
    		var defaults = {
    			store: null,
    			modelKey: "key",
    			modelValue: "val",
    			mode: "local",
    			editable: true,
    			useRawValue: false,
    			useZeroIfEmpty: false
    			// inputType: 'text',
    			// _currentVal: ''
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);

    		if (this.store.url === "") this.mode = "local";
    		else this.mode = "remote";

    		if (this.mode == "remote") {
    			this.store.addListener("load", (store, models) => {
    				// log("models", models, this.modelValue)
    				this.createDropdown();
    				this._createItemsFromStore();
    				this.dropdown.show();
    			});
    		}

    		// this.jEl.on('M_DropdownShow', (evt)=> {
    		// 	if (!this.dropdown) this.createDropdown() ;
    		// 	this.showDropdown() ;
    		// }) ;
    		if (!this.editable) {
    			this.jEl.css("cursor", "pointer");
    			this.jEl.on("click", evt => {
    				this.showDropdown(evt);
    			});
    		}
    	}
    	_createItemsFromStore() {
    		// log("_createItemsFromStore",this.store)
    		var items = [];
    		this.store.each(model => {
    			// var val = "" ;
    			// if (typeof this.modelValue === 'function') val = this.modelValue(model) ;
    			// else val = model.get(this.modelValue) ;
    			items.push({
    				text: this._getVal(model),
    				click: $.proxy(this.clickItemDropdown, this, model.get(this.modelKey))
    			});
    		});
    		// if (!this.dropdown)
    		this.createDropdown();
    		this.dropdown.setItems(items);
    	}
    	/**
    	 * @return {type}
    	 */
    	create() {
    		super.create();
    		// this.caret = $('<span class="fa fa-caret-down M_caret M_showonedit"></span>') ;
    		// this.container.find('.M_editable').after(this.caret);
    		// this.caret.click($.proxy(this.showDropdown, this)) ;
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	showDropdown(evt) {
    		M_.Help.hideMHelp();
    		if (evt) evt.stopPropagation();
    		if (this.disabled) return;
    		// if (!this.dropdown)

    		if (this.mode == "local") {
    			this.createDropdown();
    			this._createItemsFromStore();
    			this.dropdown.show();
    		} else if (this.store.load()) {
    			// this.createDropdown() ;
    		}
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	clickItemDropdown(val) {
    		this.setValue(val);
    		let model = this.store.getRow(val);
    		if (model) {
    			this.trigger("itemclick", this, model);
    		} else {
    			this.trigger("itemclick", this, val);
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	getRawValue() {
    		// return this.container.find('M_editable').text() ;
    		return this.jEl.val();
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		if (this.useRawValue) return this.getRawValue();
    		if (this.getRawValue() === "") this.value = "";
    		if (this.value === "" && this.useZeroIfEmpty) return 0;
    		return this.value;
    	}
    	/**
    	 * @return {type}
    	 */
    	getValueAndRaw() {
    		return { key: this.value, val: this.getRawValue() };
    	}
    	_getVal(model) {
    		var val = "";
    		if (typeof this.modelValue === "function") val = this.modelValue(model);
    		else val = model.get(this.modelValue);
    		return val;
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		// log("setValue",this.name, val)
    		if (val === null) val = "";
    		if (typeof val === "object") {
    			let model = new this.store.model({ row: val });
    			// log("setValue créé",model)
    			this.setValueAndRawValue(val[this.modelKey], this._getVal(model));
    			this.trigger("update", this, model);
    		} else {
    			this.value = val;
    			let model = this.store.getRow(val);
    			if (model) {
    				this.setValueAndRawValue(val, this._getVal(model));
    				this.trigger("update", this, model);
    			} else {
    				this.setRawValue(val);
    				this.trigger("update", this, val);
    			}
    		}
    	}
    	/**
    	 * @param {type}
    	 * @param {type}
    	 */
    	setValueAndRawValue(val, rawVal) {
    		this.value = val;
    		this.setRawValue(rawVal);
    	}
    	/**
    	 * @param {type}
    	 */
    	setRawValue(val) {
    		var v = val;
    		v = M_.Utils.strip_tags(v);
    		v = M_.Utils.html_entity_decode(v);
    		v = M_.Utils.trim(v);
    		this.jEl.val(v);
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	onKeyup(evt) {
    		// log("this.keyField",this.keyField)
    		var v = this.getRawValue();
    		if (this.mode == "local") {
    			this.showDropdown();
    			var items = [];
    			this.store.each(model => {
    				// log("model",model)
    				var val = this._getVal(model).toLowerCase();
    				if (val.indexOf(v.toLowerCase()) >= 0) {
    					items.push({
    						text: this._getVal(model),
    						click: $.proxy(this.clickItemDropdown, this, model.get(this.modelKey))
    					});
    				}
    			});
    			this.dropdown.setItems(items);
    		} else {
    			this.store.load({ query: v.toLowerCase() });
    		}
    		this.trigger("keyup", this, evt);
    	}
    };

    /**
     * Input combobox form ; display a comobobox + a list of values
     * @class
      * @extends M_.Form.Combobox
     */
    M_.Form.ComboboxMulti = class extends M_.Form.Combobox {
    	constructor(opts) {
    		var defaults = {
    			allowCreate: true
    		};
    		opts = opts ? opts : {};
    		var optsTemp = $.extend({}, defaults, opts);
    		super(optsTemp);

    		this.jElForMulti = $("<div></div>");
    		this.container.append(this.jElForMulti);
    		this.container.append("<div class='M_Clear'></div>");
    	}
    	onEnterInput() {
    		if (this.allowCreate) {
    			this.addItemMulti("", this.getRawValue());
    			this.setValueAndRawValue("", "");
    		}
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	clickItemDropdown(val) {
    		var model = this.store.getRow(val);
    		var realval = this._getVal(model);
    		// super.clickItemDropdown(val);
    		// this.setValueAndRawValue('', '') ;
    		// this.addItemMulti(val, realval) ;
    		// this.setValue(val) ;
    		// let model = this.store.getRow(val) ;
    		// if (model) {
    		// 	this.trigger('itemclick', this, model) ;
    		// } else {
    		// 	this.trigger('itemclick', this, val) ;
    		// }
    		this.setRawValue("");
    		this.addItemMulti(val, realval);
    	}
    	/**
    	 * @param {type}
    	 * @param {type}
    	 */
    	addItemMulti(val, realval, deletable) {
    		var idTemp = M_.Utils.id();
    		var html =
    			"<div class='M_ComboboxMultiItem selected' data-comboboxmultiitem=\"" + val + '" data-comboboxmultiitemval="' + realval + '">' + realval;
    		if (deletable !== false) html += "&nbsp;<span class='fa fa-trash faa-pulse animated-hover' id='" + idTemp + "'></span>";
    		html += " </div>";
    		this.jElForMulti.append(html);
    		if (deletable !== false) {
    			$("#" + idTemp).click(evt => {
    				$(evt.target)
    					.closest(".M_ComboboxMultiItem")
    					.remove();
    				this.trigger("update", this, val);
    			});
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		var res = [];
    		var me = this;
    		this.jElForMulti.find(".M_ComboboxMultiItem").each(function() {
    			var obj = {};
    			obj[me.modelKey] = $(this).attr("data-comboboxmultiitem");
    			obj[me.modelValue] = $(this).attr("data-comboboxmultiitemval");
    			res.push(obj);
    		});
    		return res;
    	}
    	setValue(val) {
    		this.reset();
    		_.each(val, v => {
    			this.addItemMulti(v[this.modelKey], v[this.modelValue], v.deletable);
    		});
    	}
    	/**
    	 * @return {type}
    	 */
    	reset() {
    		if (this.jElForMulti) this.jElForMulti.empty();
    	}
    	/**
    	 * @return {type}
    	 */
    	getValueAndRaw() {
    		var res = [];
    		this.jElForMulti.find(".M_ComboboxMultiItem").each(function() {
    			res.push({
    				val: $(this).attr("data-comboboxmultiitemval"),
    				key: $(this).attr("data-comboboxmultiitem")
    			});
    		});
    		return res;
    	}
    };

    /**
     * A simple div to use in forms (no label)
     * @class
      * @property {type} html
     */
    M_.Form.Div = class {
    	constructor(opts) {
    		var defaults = {
    			html: ""
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);
    		this.container.append(this.html);
    	}
    };

    /**
     * A simple div with label
     * @class
      * @extends M_.Form.Input
     * @property {type} inputType
     */
    M_.Form.Display = class extends M_.Form.Input {
    	constructor(opts) {
    		var defaults = {
    			inputType: "none"
    		};
    		opts = opts ? opts : {};
    		opts = $.extend({}, defaults, opts);
    		super(opts);
    	}
    	/**
    	 * @param {type}
    	 */
    	setValue(val) {
    		// if (val=="") val=[] ;
    		this.value = val;
    		this.jEl.html(val);
    	}
    	/**
    	 * @return {type}
    	 */
    	getValue() {
    		return this.value;
    	}
    };

    /**
     * A wysiwyg editor to use directly or in a textarea
     * @class
      * @property {type} buttonsContainer
     * @property {type} buttons
     */
    M_.Editor = class {
    	constructor(opts) {
    		var defaults = {
    			buttonsContainer: null,
    			buttonsToDisplay: [
    				"formatBlock",
    				"bold",
    				"italic",
    				"justifyLeft",
    				"justifyCenter",
    				"justifyRight",
    				"justifyFull",
    				"list",
    				"removeFormat",
    				"|",
    				"createLink",
    				"table",
    				"image",
    				"templates",
    				"|",
    				"source"
    			],
    			buttons: [
    				[
    					"formatBlock",
    					"Fomater paragraphe",
    					"fa-font",
    					"",
    					"",
    					[
    						["formatH1", "H1", "", "execFormatBlock", "H1"],
    						["formatH2", "H2", "", "execFormatBlock", "H2"],
    						["formatH3", "H3", "", "execFormatBlock", "H3"],
    						["formatH4", "H4", "", "execFormatBlock", "H4"],
    						["formatH5", "H5", "", "execFormatBlock", "H5"],
    						["formatP", "P", "", "execFormatBlock", "P"],
    						["formatDIV", "DIV", "", "execFormatBlock", "DIV"]
    					]
    				],
    				["bold", "Gras", "fa-bold", "execCommand", "bold"],
    				["italic", "Italique", "fa-italic", "execCommand", "italic"],
    				["justifyLeft", "Justifier à gauche", "fa-align-left", "execJustify", "Left"],
    				["justifyCenter", "Justifier au centre", "fa-align-center", "execJustify", "Center"],
    				["justifyRight", "Justifier à droite", "fa-align-right", "execJustify", "Right"],
    				["justifyFull", "Justifier à gauche et à droite", "fa-align-justify", "execJustify", "Full"],
    				[
    					"list",
    					"Liste numéroté ou avec puces",
    					"fa-list-ol",
    					"execCommand",
    					"",
    					[
    						["insertorderedlist", "Liste numéroté", "fa-list-ol", "execCommand", "insertorderedlist"],
    						["insertunorderedlist", "Liste avec puces", "fa-list-ul", "execCommand", "insertunorderedlist"],
    						["outdent", "Outdent", "fa-outdent", "execCommand", "outdent"],
    						["indent", "Indent", "fa-indent", "execCommand", "indent"]
    					]
    				],
    				["removeFormat", "Effacer", "fa-eraser", "execCommand", "removeFormat"],
    				"|",
    				[
    					"createLink",
    					"Lien",
    					"fa-link",
    					"",
    					"",
    					[
    						["insertorderedlist", "Créer un lien", "fa-link", "execLink", ""],
    						["insertunorderedlist", "Enlever le lien", "fa-unlink", "execCommand", "unlink"]
    					]
    				],
    				[
    					"table",
    					"Outils tableau",
    					"fa-table",
    					"",
    					"",
    					[
    						["insertTable", "Insérer un tableau", "", "execInsertTable"],
    						["insertTableColumn", "Nouvelle colonne", "", "execInsertTableColumn"],
    						["insertTableRow", "Nouvelle rangée", "", "execInsertTableRow"],
    						["editTable", "Edition tableau", "", "execEditTable"],
    						["editCell", "Edition céllule", "", "execEditCell"]
    					]
    				],
    				[
    					"image",
    					"Images",
    					"fa-picture-o",
    					"",
    					"",
    					[
    						["newImage", "Nouvelle image", "fa-picture-o", "execNewImage", ""],
    						["infoImage", "Information sur l'image", "fa-edit", "execImageInfo", ""],
    						["imagesLibrary", "Bibliothèque d'images", "fa-windows", "execImageLibrary", ""],
    						["imagesReplace", "Remplacer image", "fa-magic", "execImageReplace", ""]
    					]
    				],
    				["templates", "Templates", "fa-file", "", ""],
    				"|",
    				["source", "Code source", "fa-terminal", "execSource"]
    			]
    		};
    		opts = opts ? opts : {};
    		$.extend(this, defaults, opts);

    		this.init();
    	}
    	/**
    	 */
    	init() {
    		this.createButtons();

    		if (this.container) {
    			this.setContainer(this.container);
    			this.path = $("<div class='M_FormEditor-Path'>&nbsp;</div>");
    			this.container.after(this.path);
    		}
    		document.execCommand("styleWithCSS", false, false);
    		document.execCommand("enableObjectResizing", false, false);
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	execJustify(what) {
    		document.execCommand("justify" + what, false, null);
    		this.caretMove();
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	execCommand(comm) {
    		document.execCommand(comm, false, null);
    		this.caretMove();
    	}
    	/**
    	 * @return {type}
    	 */
    	execInsertTable() {
    		document.execCommand(
    			"insertHTML",
    			false,
    			"<table><tr><td>Cell 1.1</td><td>Cell 1.2</td></tr><tr><td>Cell 2.1</td><td>Cell 2.2</td></tr></table>"
    		);
    		this.caretMove();
    	}
    	/**
    	 * @param  {type}
    	 * @return {type}
    	 */
    	execFormatBlock(block) {
    		document.execCommand("formatBlock", false, block.toLowerCase());
    		this.caretMove();
    	}
    	/**
    	 * @return {type}
    	 */
    	execInsertTableColumn() {
    		var td = $(document.getSelection().getRangeAt(0).commonAncestorContainer).closest("td");
    		if (td.length && td.closest("#" + this.container.get(0).id).length) {
    			var n = td
    				.closest("tr")
    				.find("td")
    				.index(td);
    			td
    				.closest("table")
    				.find("tr")
    				.each(function() {
    					$(this)
    						.find("td")
    						.eq(n)
    						.after("<td>Nouvelle cellule</td>");
    				});
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	execInsertTableRow() {
    		var tr = $(document.getSelection().getRangeAt(0).commonAncestorContainer).closest("tr");
    		if (tr.length && tr.closest("#" + this.container.get(0).id).length) {
    			// var n = tr.closest("table").find("tr").index(tr) ;
    			var countTds = tr.find("td").length;
    			var newTr = $("<tr></tr>");
    			for (var i = 0; i < countTds; i++) {
    				newTr.append("<td>Nouvelle cellule</td>");
    			}
    			tr.after(newTr);
    		}
    	}
    	/**
    	 * @return {type}
    	 */
    	execLink() {
    		if (!this.winLink) {
    			this.winLink = new class extends M_.Window {
    				constructor() {
    					super({
    						modal: true,
    						width: 400
    					});
    				}
    				create() {
    					var idBtSave = M_.Utils.id(),
    						idBtCancel = M_.Utils.id(),
    						idLink = M_.Utils.id(),
    						idTarget = M_.Utils.id();
    					this.html = `<div class="M_WindowContent">
    						<div class="M_WindowHeader">
    							<h1>Edition <b>d'un lien</b></h1>
    						</div>
    						<div class="M_WindowBody">
    							<div id="${idLink}"></div>
    							<div id="${idTarget}"></div>
    						</div>
    						<div class="M_WindowFooter">
    							<div class="M_FloatRight">
    								<button id="${idBtSave}" type="button" class="M_Button primary">Enregistrer</button>
    							</div>
    							<div class="M_FloatLeft">
    								<button id="${idBtCancel}" type="button" class="M_Button">Annuler</button>
    							</div>
    						</div>
    					</div>`;
    					super.create();

    					this.form = new M_.Form.Form({
    						items: [
    							{
    								type: M_.Form.Text,
    								name: "link",
    								placeholder: "Lien",
    								label: "Lien",
    								labelPosition: "top",
    								container: $("#" + idLink)
    							},
    							{
    								type: M_.Form.Combobox,
    								name: "target",
    								allowEmpty: true,
    								placeholder: "",
    								label: "Cible",
    								labelPosition: "top",
    								container: $("#" + idTarget),
    								store: new M_.Store({
    									controller: this,
    									model: M_.ModelKeyVal,
    									rows: [{ key: "", val: "Aucun" }, { key: "_blank", val: "_blank" }, { key: "_self", val: "_self" }]
    								})
    							}
    						]
    					});
    					$("#" + idBtSave).click(() => {
    						M_.Utils.restoreSelection();
    						document.execCommand("createLink", false, this.form.find("link").getValue());
    						if (this.form.find("target").getValue() !== "")
    							$(document.getSelection().getRangeAt(0).commonAncestorContainer)
    								.closest("a")
    								.attr("target", this.form.find("target").getValue());
    						this.hide();
    					});
    					$("#" + idBtCancel).click(() => {
    						M_.Utils.restoreSelection();
    						this.hide();
    					});
    				}
    			}();
    		}
    		this.winLink.show();
    	}
    	/**
    	 * @return {type}
    	 */
    	execSource() {
    		if (!this.winSource) {
    			this.winSource = new class extends M_.Window {
    				constructor(containerEditor) {
    					super({
    						modal: true,
    						width: 800
    					});
    					this.containerEditor = containerEditor;
    				}
    				create() {
    					var idBtSave = M_.Utils.id(),
    						idBtCancel = M_.Utils.id(),
    						idCode = M_.Utils.id();
    					this.html = `<div class="M_WindowContent">
    						<div class="M_WindowHeader">
    							<h1>Edition <b>du code source</b></h1>
    						</div>
    						<div class="M_WindowBody">
    							<div id="${idCode}"></div>
    						</div>
    						<div class="M_WindowFooter">
    							<div class="M_FloatRight">
    								<button id="${idBtSave}" type="button" class="M_Button primary">Enregistrer</button>
    							</div>
    							<div class="M_FloatLeft">
    								<button id="${idBtCancel}" type="button" class="M_Button">Annuler</button>
    							</div>
    						</div>
    					</div>`;
    					super.create();

    					this.form = new M_.Form.Form({
    						items: [
    							{
    								type: M_.Form.Textarea,
    								name: "htmlcode",
    								acceptTabulation: true,
    								rows: 30,
    								container: $("#" + idCode)
    							}
    						]
    					});
    					$("#" + idBtSave).click(() => {
    						var html = this.form.find("htmlcode").getValue();
    						this.containerEditor.html(html);
    						this.hide();
    					});
    					$("#" + idBtCancel).click(() => {
    						M_.Utils.restoreSelection();
    						this.hide();
    					});
    				}
    			}(this.container);
    		}
    		var html = this.container.html();
    		this.winSource.form.find("htmlcode").setValue(html);
    		this.winSource.show();
    	}
    	/**
    	 * @return {type}
    	 */
    	execEditTable() {
    		var table = $(document.getSelection().getRangeAt(0).commonAncestorContainer).closest("table");
    		if (!table.length || !table.closest("#" + this.container.get(0).id).length) return;
    		if (!this.winEditTable) {
    			this.winEditTable = new class extends M_.Window {
    				constructor(containerEditor) {
    					super({
    						modal: true,
    						width: 800,
    						containerEditor: containerEditor
    					});
    				}
    				create() {
    					var idBtSave = M_.Utils.id(),
    						idBtCancel = M_.Utils.id(),
    						idwidth = M_.Utils.id(),
    						idcellspacing = M_.Utils.id(),
    						idcellpadding = M_.Utils.id(),
    						idborder = M_.Utils.id(),
    						idstyle = M_.Utils.id(),
    						idid = M_.Utils.id(),
    						idalign = M_.Utils.id();
    					this.html = `<div class="M_WindowContent">
    						<div class="M_WindowHeader">
    							<h1>Edition <b>d'un tableau</b></h1>
    						</div>
    						<div class="M_WindowBody">
    							<div id="${idwidth}"></div>
    							<div id="${idcellspacing}"></div>
    							<div id="${idcellpadding}"></div>
    							<div id="${idborder}"></div>
    							<div id="${idstyle}"></div>
    							<div id="${idid}"></div>
    							<div id="${idalign}"></div>
    						</div>
    						<div class="M_WindowFooter">
    							<div class="M_FloatRight">
    								<button id="${idBtSave}" type="button" class="M_Button primary">Enregistrer</button>
    							</div>
    							<div class="M_FloatLeft">
    								<button id="${idBtCancel}" type="button" class="M_Button">Annuler</button>
    							</div>
    						</div>
    					</div>`;
    					super.create();

    					this.form = new M_.Form.Form({
    						itemsDefaults: {
    							labelPosition: "left",
    							labelWidth: 250
    						},
    						items: [
    							{
    								type: M_.Form.Text,
    								label: "Largeur (px ou %)",
    								name: "width",
    								container: $("#" + idwidth)
    							},
    							{
    								type: M_.Form.Number,
    								label: "Espacement des cellules",
    								name: "cellpadding",
    								container: $("#" + idcellpadding)
    							},
    							{
    								type: M_.Form.Number,
    								label: "Marge des cellules",
    								name: "cellspacing",
    								container: $("#" + idcellspacing)
    							},
    							{
    								type: M_.Form.Number,
    								label: "Taille de la bordure",
    								name: "border",
    								container: $("#" + idborder)
    							},
    							{
    								type: M_.Form.Text,
    								label: "Style CSS",
    								name: "style",
    								container: $("#" + idstyle)
    							},
    							{
    								type: M_.Form.Text,
    								label: "ID",
    								name: "id",
    								container: $("#" + idid)
    							},
    							{
    								type: M_.Form.Combobox,
    								name: "align",
    								allowEmpty: true,
    								placeholder: "",
    								label: "Alignement",
    								container: $("#" + idalign),
    								store: new M_.Store({
    									controller: this,
    									model: M_.ModelKeyVal,
    									rows: [
    										{ key: "", val: "Aucun" },
    										{ key: "left", val: "Gauche" },
    										{ key: "center", val: "Centré" },
    										{ key: "right", val: "Droite" }
    									]
    								})
    							}
    						]
    					});
    					$("#" + idBtSave).click(() => {
    						this.currentTable.css({ width: this.form.find("width").getValue() });
    						this.currentTable.attr("cellpadding", this.form.find("cellpadding").getValue());
    						this.currentTable.attr("cellspacing", this.form.find("cellspacing").getValue());
    						this.currentTable.attr("border", this.form.find("border").getValue());
    						this.currentTable.attr("align", this.form.find("align").getValue());
    						this.hide();
    					});
    					$("#" + idBtCancel).click(() => {
    						M_.Utils.restoreSelection();
    						this.hide();
    					});
    				}
    				setCurrentTable(currentTable) {
    					this.currentTable = currentTable;
    					var sEl = this.currentTable.get(0).style;
    					if (sEl.width !== "") this.form.find("width").setValue(this.currentTable.get(0).style.width);
    					else this.form.find("width").setValue("");
    					if (this.currentTable.attr("cellpadding")) this.form.find("cellpadding").setValue(this.currentTable.attr("cellpadding"));
    					else this.form.find("cellpadding").setValue("");
    					if (this.currentTable.attr("cellspacing")) this.form.find("cellspacing").setValue(this.currentTable.attr("cellspacing"));
    					else this.form.find("cellspacing").setValue("");
    					if (this.currentTable.attr("border")) this.form.find("border").setValue(this.currentTable.attr("border"));
    					else this.form.find("border").setValue("");
    					if (this.currentTable.attr("align")) this.form.find("align").setValue(this.currentTable.attr("align"));
    					else this.form.find("align").setValue("");
    				}
    			}(this.container);
    		}
    		this.winEditTable.setCurrentTable(table);
    		this.winEditTable.show();
    	}
    	/**
    	 * @return {type}
    	 */
    	execEditCell() {
    		var td = $(document.getSelection().getRangeAt(0).commonAncestorContainer).closest("td");
    		if (!td.length || !td.closest("#" + this.container.get(0).id).length) return;
    		if (!this.winEditCell) {
    			this.winEditCell = new class extends M_.Window {
    				constructor(containerEditor) {
    					super({
    						modal: true,
    						width: 800,
    						containerEditor: containerEditor
    					});
    				}
    				create() {
    					var idBtSave = M_.Utils.id(),
    						idBtCancel = M_.Utils.id(),
    						idwidth = M_.Utils.id(),
    						idalign = M_.Utils.id(),
    						idvalign = M_.Utils.id(),
    						idcolor = M_.Utils.id();
    					this.html = `<div class="M_WindowContent">
    						<div class="M_WindowHeader">
    							<h1>Edition <b>d'une cellule</b></h1>
    						</div>
    						<div class="M_WindowBody">
    							<div id="${idwidth}"></div>
    							<div id="${idalign}"></div>
    							<div id="${idvalign}"></div>
    							<div id="${idcolor}"></div>
    						</div>
    						<div class="M_WindowFooter">
    							<div class="M_FloatRight">
    								<button id="${idBtSave}" type="button" class="M_Button primary">Enregistrer</button>
    							</div>
    							<div class="M_FloatLeft">
    								<button id="${idBtCancel}" type="button" class="M_Button">Annuler</button>
    							</div>
    						</div>
    					</div>`;
    					super.create();

    					this.form = new M_.Form.Form({
    						itemsDefaults: {
    							labelPosition: "left",
    							labelWidth: 250
    						},
    						items: [
    							{
    								type: M_.Form.Text,
    								label: "Largeur (px ou %)",
    								name: "width",
    								container: $("#" + idwidth)
    							},
    							{
    								type: M_.Form.Combobox,
    								name: "align",
    								allowEmpty: true,
    								placeholder: "",
    								label: "Alignement",
    								container: $("#" + idalign),
    								store: new M_.Store({
    									controller: this,
    									model: M_.ModelKeyVal,
    									rows: [
    										{ key: "", val: "Aucun" },
    										{ key: "left", val: "Gauche" },
    										{ key: "center", val: "Centré" },
    										{ key: "right", val: "Droite" }
    									]
    								})
    							},
    							{
    								type: M_.Form.Combobox,
    								name: "valign",
    								allowEmpty: true,
    								placeholder: "",
    								label: "Alignement vertical",
    								container: $("#" + idvalign),
    								store: new M_.Store({
    									controller: this,
    									model: M_.ModelKeyVal,
    									rows: [
    										{ key: "", val: "Aucun" },
    										{ key: "top", val: "Haut" },
    										{ key: "middle", val: "Milieu" },
    										{ key: "bottom", val: "Bas" }
    									]
    								})
    							},
    							{
    								type: M_.Form.Text,
    								label: "Couleur",
    								name: "color",
    								container: $("#" + idwidth)
    							}
    						]
    					});
    					$("#" + idBtSave).click(() => {
    						this.currentCell.css({ width: this.form.find("width").getValue() });
    						this.currentCell.attr("align", this.form.find("align").getValue());
    						this.currentCell.attr("valign", this.form.find("valign").getValue());
    						this.currentCell.css({
    							backgroundColor: this.form.find("color").getValue()
    						});
    						this.hide();
    					});
    					$("#" + idBtCancel).click(() => {
    						M_.Utils.restoreSelection();
    						this.hide();
    					});
    				}
    				setCurrentCell(currentCell) {
    					this.currentCell = currentCell;
    					var sEl = this.currentCell.get(0).style;
    					if (sEl.width !== "") this.form.find("width").setValue(this.currentCell.css("width"));
    					else this.form.find("width").setValue("");
    					if (this.currentCell.attr("align")) this.form.find("align").setValue(this.currentCell.attr("cellpadding"));
    					else this.form.find("align").setValue("");
    					if (this.currentCell.attr("valign")) this.form.find("valign").setValue(this.currentCell.attr("valign"));
    					else this.form.find("valign").setValue("");
    					if (sEl.backgroundColor !== "") this.form.find("color").setValue(this.currentCell.css("backgroundColor"));
    					else this.form.find("color").setValue("");
    				}
    			}(this.container);
    		}
    		this.winEditCell.setCurrentCell(td);
    		this.winEditCell.show();
    	}
    	/**
    	 * @return {type}
    	 */
    	execNewImage() {
    		if (!this.winNewImage) {
    			this.winNewImage = new class extends M_.Window {
    				constructor(containerEditor) {
    					super({
    						modal: true,
    						width: 800,
    						tabFilesToSend: []
    					});
    					this.containerEditor = containerEditor;
    				}
    				create() {
    					var idBtSave = M_.Utils.id(),
    						idBtCancel = M_.Utils.id(),
    						idCode = M_.Utils.id();
    					this.html = `<div class="M_WindowContent">
    						<div class="M_WindowHeader">
    							<h1>Envoyer <b>des images</b></h1>
    						</div>
    						<div class="M_WindowBody">
    							<div id="${idCode}"></div>
    							<div class="m_dropherenewimage" contenteditable><h2>Déposer ici un (ou des) fichier(s) de votre bureau</h2></div>
    							<h2 style="margin-top:15px;">Ou choisissez les fichiers à envoyer</h2>
    							<input type="file" class="m_fileinput" multiple />
    							<div class="M_Clear"></div>
    							<output class="m_fileoutput"></output>
    							<div class="M_Clear"></div>
    						</div>
    						<div class="M_WindowFooter">
    							<div class="M_FloatRight">
    								<button id="${idBtSave}" type="button" class="M_Button primary">Enregistrer</button>
    							</div>
    							<div class="M_FloatLeft">
    								<button id="${idBtCancel}" type="button" class="M_Button">Annuler</button>
    							</div>
    						</div>
    					</div>`;
    					super.create();

    					this.jEl.find(".m_dropherenewimage").on(
    						"dragover",
    						$.proxy(function(e) {
    							this.jEl.find(".m_dropherenewimage").css("background-color", "green");
    						}, this)
    					);
    					this.jEl.find(".m_dropherenewimage").on(
    						"dragleave",
    						$.proxy(function(e) {
    							this.jEl.find(".m_dropherenewimage").css("background-color", "white");
    						}, this)
    					);
    					this.jEl.find(".m_dropherenewimage").on(
    						"drop",
    						$.proxy(function(e) {
    							this.jEl.find(".m_dropherenewimage").css("background-color", "white");
    							e.preventDefault();
    							var files = e.originalEvent.dataTransfer.files;
    							if (files.length === 0) return;
    							for (var i = 0; i < files.length; i++) {
    								var f = files[i];
    								this.createThumbnail(f);
    							}
    						}, this)
    					);

    					$("#" + idBtSave).click(() => {
    						var formData = new FormData();
    						for (var i = 0; i < this.tabFilesToSend.length; i++) {
    							formData.append("file" + i, this.tabFilesToSend[i]);
    						}
    						var xhr = new XMLHttpRequest();
    						xhr.open("POST", "/upload");
    						var me = this;
    						xhr.onload = function() {
    							//console.log(xhr.responseText);
    							var jsonResponse = JSON.parse(xhr.responseText);
    							if (me.callbackSave) {
    								var img = null;
    								if (jsonResponse.tabRes.length > 0) img = jsonResponse.tabRes[0];
    								me.callbackSave(img);
    							} else {
    								for (var i = 0; i < jsonResponse.tabRes.length; i++) {
    									document.execCommand("insertImage", false, jsonResponse.tabRes[i]);
    								}
    							}
    							me.hide();
    							//document.execCommand("insertImage", false, jsonResponse.url) ;
    							//me.caretMove() ;
    						};
    						xhr.upload.onprogress = function(e) {
    							if (e.lengthComputable) {
    								// var complete = (e.loaded / e.total * 100 | 0);
    								//Mlog("complete",complete)
    								//updates a <progress> tag to show upload progress
    								//$('progress').val(complete);
    							}
    						};
    						xhr.send(formData);
    					});
    					$("#" + idBtCancel).click(() => {
    						M_.Utils.restoreSelection();
    						this.hide();
    					});
    				}
    				resetFileInput() {
    					var control = this.jEl.find(".m_fileinput");
    					this.jEl.find(".m_fileinput").replaceWith((control = control.clone(true)));
    					this.jEl.find(".m_fileinput").on(
    						"change",
    						$.proxy(function(e) {
    							var files = e.target.files;
    							if (files.length === 0) return;
    							for (var i = 0; i < files.length; i++) {
    								var f = files[i];
    								this.createThumbnail(f);
    							}
    						}, this)
    					);
    				}
    				createThumbnail(f) {
    					this.tabFilesToSend.push(f);
    					var list = this.jEl.find(".m_fileoutput");
    					var reader = new FileReader();
    					var me = this;
    					reader.onload = (function(theFile) {
    						return function(e) {
    							list.append(
    								['<img style="margin:0 15px 15px 0" src="', e.target.result, '" title="', theFile.name, '" width="100" />'].join("")
    							);
    							me.center();
    						};
    					})(f);
    					reader.readAsDataURL(f);
    				}
    			}(this.container);
    		}
    		this.winNewImage.resetFileInput();
    		this.winNewImage.show();
    	}
    	/**
    	 * @return {type}
    	 */
    	caretMove() {
    		if (document.getSelection().rangeCount > 0) {
    			var sel = $(document.getSelection().getRangeAt(0).commonAncestorContainer);
    			var html = "";
    			var jEls = sel.parentsUntil("#" + this.container.get(0).id);
    			if (!sel.hasClass("M_FormEditor-Content")) {
    				var tabTemp = [];
    				jEls.each(function(ind, el) {
    					if (el.tagName) tabTemp.push(el.tagName);
    				});
    				tabTemp.reverse();
    				tabTemp.push("text");
    				html = tabTemp.join(" &gt; ");
    			}
    			//html += sel.get(0).tagName ;
    			html += "&nbsp;";
    			this.path.html(html);
    		}

    		if (document.queryCommandValue("bold") == "true") this.buttonsContainer.find('button[m_id="bold"]').addClass("active");
    		else this.buttonsContainer.find('button[m_id="bold"]').removeClass("active");
    		if (document.queryCommandValue("italic") == "true") this.buttonsContainer.find('button[m_id="italic"]').addClass("active");
    		else this.buttonsContainer.find('button[m_id="italic"]').removeClass("active");
    		if (document.queryCommandValue("justifyLeft") == "true") this.buttonsContainer.find('button[m_id="justifyLeft"]').addClass("active");
    		else this.buttonsContainer.find('button[m_id="justifyLeft"]').removeClass("active");
    		if (document.queryCommandValue("justifyRight") == "true") this.buttonsContainer.find('button[m_id="justifyRight"]').addClass("active");
    		else this.buttonsContainer.find('button[m_id="justifyRight"]').removeClass("active");
    		if (document.queryCommandValue("justifyCenter") == "true") this.buttonsContainer.find('button[m_id="justifyCenter"]').addClass("active");
    		else this.buttonsContainer.find('button[m_id="justifyCenter"]').removeClass("active");
    		if (document.queryCommandValue("justifyFull") == "true") this.buttonsContainer.find('button[m_id="justifyFull"]').addClass("active");
    		else this.buttonsContainer.find('button[m_id="justifyFull"]').removeClass("active");
    	}
    	/**
    	 * @return {type}
    	 */
    	createButtons() {
    		var toolbar = $("<div>", { class: "M_Editor", role: "toolbar" });
    		this.buttonsContainer.append(toolbar);
    		// var currentGroup = $("<div>", {'class':'btn-group'}) ;
    		// toolbar.append(currentGroup) ;
    		for (var i = 0; i < this.buttons.length; i++) {
    			let bt = this.buttons[i];
    			if (_.indexOf(this.buttonsToDisplay, bt[0]) < 0) continue;
    			if (bt[0] === "|") {
    				// currentGroup = $("<div>", {'class':'btn-group'}) ;
    				// toolbar.append(currentGroup) ;
    			} else {
    				let caret = "";
    				if (bt[5]) caret = "<span class='fa fa-caret-down'></span>";
    				let button = $("<button m_id='" + bt[0] + "'><i class='fa " + bt[2] + "'></i> " + caret + "</button>");
    				toolbar.append(button);
    				if (bt[5]) {
    					button.click({ items: bt[5] }, evt => {
    						evt.stopPropagation();
    						M_.Utils.saveSelection();
    						var items = [];
    						_.each(evt.data.items, item => {
    							let nameTemp = item[1];
    							if (item[2] && item[2] !== "") nameTemp = "<i class='fa " + item[2] + "'></i>&nbsp;" + nameTemp;
    							items.push({
    								text: nameTemp,
    								click: evt => {
    									// evt.stopPropagation() ;
    									// evt.preventDefault()
    									M_.Utils.restoreSelection();
    									this[item[3]](item[4]);
    								}
    							});
    						});
    						var dropdown = new M_.Dropdown({
    							destroyOnHide: true,
    							alignTo: $(evt.target).closest("button"),
    							items: items
    						});
    						dropdown.show();
    					});
    				} else {
    					button.click({ items: bt[5] }, evt => {
    						this[bt[3]](bt[4]);
    					});
    				}
    			}
    		}
    	}
    	/**
    	 * @param {type}
    	 */
    	setContainer(container) {
    		this.container = container;
    		this.container.on(
    			"click",
    			$.proxy(function(e) {
    				this.container.prop("contenteditable", true);
    				this.container.addClass("isEditing");
    				if (!this.isStartedDrag) {
    					$("#" + this.container.get(0).id + " .M_todelete").remove();
    					this.imgDraggable = null;
    				}
    				this.caretMove();
    			}, this)
    		);
    		this.container.on(
    			"keyup",
    			$.proxy(function(e) {
    				this.caretMove();
    			}, this)
    		);
    		this.container.on(
    			"keydown",
    			$.proxy(function(e) {
    				this.caretMove();
    			}, this)
    		);
    		this.container.on(
    			"dragstart",
    			$.proxy(function(e) {
    				$("#" + this.container.get(0).id + " .M_todelete").remove();
    			}, this)
    		);
    		$(document).on(
    			"dblclick",
    			"#" + this.container.get(0).id + " img",
    			$.proxy(function(e) {
    				this.execImageInfo();
    			}, this)
    		);
    		$(document).on(
    			"drop",
    			"#" + this.container.get(0).id + " img",
    			$.proxy(function(e) {
    				log("drop on image", e);
    			}, this)
    		);
    		$(document).click(
    			$.proxy(function(e) {
    				if ($(e.target).closest("#" + this.container.get(0).id + ", #" + this.buttonsContainer.get(0).id).length === 0) {
    					$("#" + this.container.get(0).id + " .M_todelete").remove();
    					//this.container.prop("contenteditable", false) ;
    					this.container.removeClass("isEditing");
    				}
    			}, this)
    		);
    		this.container.on(
    			"drop",
    			$.proxy(function(e) {
    				var files = e.originalEvent.dataTransfer.files;
    				if (files.length === 0) return;

    				e.preventDefault();
    				if (!this.container.prop("contenteditable")) {
    					M_.Dialog.alert("Information", "Merci de cliquer d'abord l'endroit où vous souhaitez insérer l'image");
    					return;
    				}
    				this.container.prop("contenteditable", true);
    				var formData = new FormData();
    				formData.append("file0", files[0]);
    				var xhr = new XMLHttpRequest();
    				xhr.open("POST", "/upload");
    				var me = this;
    				xhr.onload = function() {
    					//console.log(xhr.responseText);
    					var jsonResponse = JSON.parse(xhr.responseText);
    					document.execCommand("insertImage", false, jsonResponse.tabRes[0]);
    					me.caretMove();
    				};
    				xhr.upload.onprogress = function(e) {
    					if (e.lengthComputable) {
    						// var complete = (e.loaded / e.total * 100 | 0);
    						//updates a <progress> tag to show upload progress
    						//$('progress').val(complete);
    					}
    				};
    				xhr.send(formData);
    			}, this)
    		);
    	}
    };

    export var M_ = M_;

    $.postJSON = function(url, data, callback) {
    	return $.ajax({
    		type: "POST",
    		url: url,
    		contentType: "application/json",
    		data: JSON.stringify(data),
    		dataType: "json",
    		success: callback
    	});
    };

    window.log = function() {
    	if (window.console) {
    		console.log.apply(window.console, arguments);
    	}
    };

    // (function(factory) {
    // 	if (typeof define === 'function' && define.amd) {
    // 		define([ 'jquery' ], factory);
    // 	}
    // 	else {
    // 		factory(jQuery);
    // 	}
    // })(function($) {

    // ;;

    // var defaults = {
    // } ;

    // var md = $.m_draggable = { version: "0.1.2" };

    // // classic jQuery plugin ; I don't comment
    // $.fn.m_draggable = function(options) {
    // 	var args = Array.prototype.slice.call(arguments, 1);
    // 	var res = this;
    // 	this.each(function(i, _element) {
    // 		var element = $(_element);
    // 		var draggable = element.data('m_draggable');
    // 		var singleRes;
    // 		if (typeof options === 'string') {
    // 			if (draggable && $.isFunction(draggable[options])) {
    // 				singleRes = draggable[options].apply(draggable, args);
    // 				if (!i) res = singleRes;
    // 				if (options === 'destroy') element.removeData('m_draggable');
    // 			}
    // 		} else if (!draggable) { // don't initialize twice
    // 			draggable = new m_draggable(element, options);
    // 			element.data('m_draggable', draggable);
    // 			draggable.reload();
    // 		}
    // 	});
    // 	return res;
    // };

    // // More interesting...
    // function m_draggable(element, instanceOptions) {
    // 	var me = this;

    // 	var opts = $.extend(true, {}, defaults, instanceOptions) ;

    // 	// Exports
    // 	// me.render = render ;
    // 	me.sayHello = sayHello ;
    // 	me.reload = reload ;
    // 	me.opts = opts ;

    // 	// local variables

    // 	// for debug
    // 	function log() {
    // 		if (window.console) console.log.apply(window.console, arguments) ;
    // 	}

    // 	function reload() {
    // // 		jElHandle.mousedown({el:el}, $.proxy(function(evt) {
    // // 			this.dragEl = evt.data.el ;
    // // 			this.dragStart = true ;
    // // 			this.dragDiff = {top: evt.pageY-this.dragEl.offset().top, left: evt.pageX-this.dragEl.offset().left}
    // // 		}, this)) ;
    // // 		$(document).mouseup({el:el}, $.proxy(function() {
    // // 			this.dragStart = false ;
    // // 		}, this)) ;
    // // 		$(document).mousemove({el:el}, $.proxy(function(evt) {
    // // 			if (this.dragStart && this.dragEl == evt.data.el) {
    // // 				var top = evt.pageY ;
    // // 				var left = evt.pageX ;
    // // 				this.dragEl.offset({top: top-this.dragDiff.top, left: left-this.dragDiff.left}) ;
    // // 			}
    // // 		}, this)) ;
    // 	}

    // 	// don't create / re-create dom, resize all components
    // 	function redraw() {
    // 		render() ;
    // 	}
    // 	function sayHello() {
    // 		log("hello") ;
    // 	}

    // }

    // // mg._startDrag = false ;

    // ;;

    // });

    // M_.Draggable = class {
    // 	constructor(el, jElHandle=null, jElHelper=null) {
    // 		//tabDraggable.push({el:el, jElHandle:jElHandle}) ;
    // 		jElHandle.mousedown({el:el}, $.proxy(function(evt) {
    // 			this.dragEl = evt.data.el ;
    // 			this.dragStart = true ;
    // 			this.dragDiff = {top: evt.pageY-this.dragEl.offset().top, left: evt.pageX-this.dragEl.offset().left}
    // 		}, this)) ;
    // 		$(document).mouseup({el:el}, $.proxy(function() {
    // 			this.dragStart = false ;
    // 		}, this)) ;
    // 		$(document).mousemove({el:el}, $.proxy(function(evt) {
    // 			if (this.dragStart && this.dragEl == evt.data.el) {
    // 				var top = evt.pageY ;
    // 				var left = evt.pageX ;
    // 				this.dragEl.offset({top: top-this.dragDiff.top, left: left-this.dragDiff.left}) ;
    // 			}
    // 		}, this)) ;
    // 	}

    // changer update en change pour les événements combobox

    //// voir http://jsfiddle.net/jhfrench/GpdgF/ pour un treeview
    //// mettre un appel bouton "téléphonique reçu" accessible sur toutes les pages
    //// excellent : http://dropthebit.com/demos/fancy_input/fancyInput.html
    /// pour du scroll : http://johnpolacek.github.io/superscrollorama/
    /// peut servir, d'autres trucs bien sur ce site : http://tympanus.net/codrops/2012/01/24/arctext-js-curving-text-with-css3-and-jquery/
    /// à utiliser sur un site web : http://www.seanmccambridge.com/tubular/
    /// excellent : http://tutorialzine.com/2011/09/jquery-pointpoint-plugin/
    /// peut-être pas mal, pour rafraichir les listes : http://usehook.com/
    // à étudier : http://www.chartjs.org/docs/#getting-started-creating-a-chart
    // à copier ??? http://paulkinzett.github.io/toolbar/

    /// http://codecanyon.net/item/fun-captcha-jquery-plugin/6705437?ref=yuzuruishikawa

    // Mac OS X: http://blog.coolaj86.com/articles/how-to-create-an-osx-pkg-installer.html
    // Ubuntu Linux: http://blog.coolaj86.com/articles/how-to-create-a-debian-installer.html
    // Microsoft Windows: http://blog.coolaj86.com/articles/how-to-create-an-innosetup-installer.html

    // à annalyser : http://www.corm.fr/demonstration/
    // à analyser également (permet de faire des applications locales en nodejs) : https://github.com/rogerwang/node-webkit

    // l'utiliser pour la création du PDF ? http://www.jqueryrain.com/?l1iRfaTH
    /// ...pas mal du tout, car çà permet de laisser en colonne !!!!