src/pyams_skin/resources/js/myams.js
changeset 0 bb4aabe07487
child 4 d2b6936e607b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_skin/resources/js/myams.js	Thu Feb 19 10:59:00 2015 +0100
@@ -0,0 +1,3666 @@
+/*
+ * MyAMS
+ * « My Application Management Skin »
+ *
+ * $Tag$
+ * A bootstrap based application/administration skin
+ *
+ * Custom administration and application skin tools
+ * Released under Zope Public License ZPL 1.1
+ * ©2014 Thierry Florac <tflorac@ulthar.net>
+ */
+
+(function($) {
+
+	/**
+	 * String prototype extensions
+	 */
+	String.prototype.startsWith = function(str) {
+		var slen = this.length;
+		var dlen = str.length;
+		if (slen < dlen) {
+			return false;
+		}
+		return (this.substr(0,dlen) == str);
+	};
+
+	String.prototype.endsWith = function(str) {
+		var slen = this.length;
+		var dlen = str.length;
+		if (slen < dlen) {
+			return false;
+		}
+		return (this.substr(slen-dlen) == str);
+	};
+
+
+	/**
+	 * Array prototype extensions
+	 */
+	if (!Array.prototype.indexOf) {
+		Array.prototype.indexOf = function(elt /*, from*/) {
+			var len = this.length;
+
+			var from = Number(arguments[1]) || 0;
+			from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+			if (from < 0)
+				from += len;
+
+			for (; from < len; from++) {
+				if (from in this &&
+					this[from] === elt)
+					return from;
+			}
+			return -1;
+		};
+	}
+
+
+	/**
+	 * JQuery 'econtains' expression
+	 * Case insensitive contains expression
+	 */
+	$.expr[":"].econtains = function(obj, index, meta /*, stack*/) {
+		return (obj.textContent || obj.innerText || $(obj).text() || "").toLowerCase() == meta[3].toLowerCase();
+	};
+
+
+	/**
+	 * JQuery 'withtext' expression
+	 * Case sensitive exact search expression
+	 */
+	$.expr[":"].withtext = function(obj, index, meta /*, stack*/) {
+		return (obj.textContent || obj.innerText || $(obj).text() || "") == meta[3];
+	};
+
+
+	/**
+	 * JQuery filter on parents class
+	 */
+	$.expr[':'].parents = function(obj, index, meta /*, stack*/) {
+		return $(obj).parents(meta[3]).length > 0;
+	};
+
+
+	/**
+	 * JQuery 'scrollbarWidth' function
+	 * Get width of vertical scrollbar
+	 */
+	if ($.scrollbarWidth === undefined) {
+		$.scrollbarWidth = function() {
+			var parent = $('<div style="width:50px;height:50px;overflow:auto"><div/></div>').appendTo('body');
+			var child = parent.children();
+			var width = child.innerWidth() - child.height(99).innerWidth();
+			parent.remove();
+			return width;
+		};
+	}
+
+
+	/**
+	 * MyAMS JQuery extensions
+	 */
+	$.fn.extend({
+
+		/*
+		 * Check if current object is empty or not
+		 */
+		exists: function() {
+			return $(this).length > 0;
+		},
+
+		/*
+		 * CSS style function
+		 * Code from Aram Kocharyan on stackoverflow.com
+		 */
+		style: function(styleName, value, priority) {
+			// DOM node
+			var node = this.get(0);
+			// Ensure we have a DOM node
+			if (typeof node == 'undefined') {
+				return;
+			}
+			// CSSStyleDeclaration
+			var style = this.get(0).style;
+			// Getter/Setter
+			if (typeof styleName != 'undefined') {
+				if (typeof value != 'undefined') {
+					// Set style property
+					priority = typeof priority != 'undefined' ? priority : '';
+					style.setProperty(styleName, value, priority);
+					return this;
+				} else {
+					// Get style property
+					return style.getPropertyValue(styleName);
+				}
+			} else {
+				// Get CSSStyleDeclaration
+				return style;
+			}
+		},
+
+		/*
+		 * Remove CSS classes starting with a given prefix
+		 */
+		removeClassPrefix: function (prefix) {
+			this.each(function (i, it) {
+				var classes = it.className.split(" ").map(function(item) {
+					return item.startsWith(prefix) ? "" : item
+				});
+				it.className = $.trim(classes.join(" "))
+			});
+			return this;
+		},
+
+		/*
+		 * Main menus manager
+		 */
+		myams_menu: function(options) {
+			// Extend our default options with those provided
+			var defaults = {
+				accordion : 'true',
+				speed : 200,
+				closedSign : '<em class="fa fa-angle-down"></em>',
+				openedSign : '<em class="fa fa-angle-up"></em>'
+			};
+			var settings = $.extend({}, defaults, options);
+
+			// Assign current element to variable, in this case is UL element
+			var menu = $(this);
+
+			// Add a mark [+] to a multilevel menu
+			menu.find("LI").each(function() {
+				var menu_item = $(this);
+				if (menu_item.find("UL").size() > 0) {
+
+					// add the multilevel sign next to the link
+					menu_item.find("A:first")
+							 .append("<b class='collapse-sign'>" + settings.closedSign + "</b>");
+
+					// avoid jumping to the top of the page when the href is an #
+					var first_link = menu_item.find("A:first");
+					if (first_link.attr('href') == "#") {
+						first_link.click(function() {
+							return false;
+						});
+					}
+				}
+			});
+
+			// Open active level
+			menu.find("LI.active").each(function() {
+				var active_parent = $(this).parents('UL');
+				var active_item = active_parent.parent('LI');
+				active_parent.slideDown(settings.speed);
+				active_item.find("b:first").html(settings.openedSign);
+				active_item.addClass("open")
+			});
+
+			menu.find("LI A").on('click', function() {
+				var link = $(this);
+				var parent_ul = link.parent().find("UL");
+				if (parent_ul.size() != 0) {
+					if (settings.accordion) {
+						// Do nothing when the list is open
+						if (!parent_ul.is(':visible')) {
+							var parents = link.parent().parents("UL");
+							var visible = menu.find("UL:visible");
+							visible.each(function(visibleIndex) {
+								var close = true;
+								parents.each(function(parentIndex) {
+									if (parents[parentIndex] == visible[visibleIndex]) {
+										close = false;
+										return false;
+									}
+								});
+								if (close) {
+									if (parent_ul != visible[visibleIndex]) {
+										$(visible[visibleIndex]).slideUp(settings.speed, function() {
+											link.parent("LI")
+												.find("b:first")
+												.html(settings.closedSign);
+											link.parent("LI")
+												.removeClass("open");
+										});
+									}
+								}
+							});
+						}
+					}
+					var first_ul = link.parent().find("UL:first");
+					if (!link.attr('href').replace(/^#/,'') &&
+						first_ul.is(":visible") &&
+						!first_ul.hasClass("active")) {
+						first_ul.slideUp(settings.speed, function() {
+							link.parent("LI")
+								.removeClass("open")
+								.find("B:first")
+								.delay(settings.speed)
+								.html(settings.closedSign);
+						});
+					} else /*if (link.attr('href') != location.hash)*/ {
+						first_ul.slideDown(settings.speed, function() {
+							link.parent("LI")
+								.addClass("open")
+								.find("B:first")
+								.delay(settings.speed)
+								.html(settings.openedSign);
+						});
+					}
+				}
+			});
+		}
+	});
+
+
+	/**
+	 * UTF-8 encoding class
+	 * Mainly used by IE...
+	 */
+	$.UTF8 = {
+
+		// public method for url encoding
+		encode : function (string) {
+			string = string.replace(/\r\n/g,"\n");
+			var utftext = "";
+
+			for (var n = 0; n < string.length; n++) {
+
+				var c = string.charCodeAt(n);
+
+				if (c < 128) {
+					utftext += String.fromCharCode(c);
+				}
+				else if((c > 127) && (c < 2048)) {
+					utftext += String.fromCharCode((c >> 6) | 192);
+					utftext += String.fromCharCode((c & 63) | 128);
+				}
+				else {
+					utftext += String.fromCharCode((c >> 12) | 224);
+					utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+					utftext += String.fromCharCode((c & 63) | 128);
+				}
+			}
+			return utftext;
+		},
+
+		// public method for url decoding
+		decode : function (utftext) {
+			var string = "";
+			var i = 0,
+				c = 0,
+				c2 = 0,
+				c3 = 0;
+
+			while ( i < utftext.length ) {
+
+				c = utftext.charCodeAt(i);
+
+				if (c < 128) {
+					string += String.fromCharCode(c);
+					i++;
+				}
+				else if((c > 191) && (c < 224)) {
+					c2 = utftext.charCodeAt(i+1);
+					string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
+					i += 2;
+				}
+				else {
+					c2 = utftext.charCodeAt(i+1);
+					c3 = utftext.charCodeAt(i+2);
+					string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+					i += 3;
+				}
+			}
+			return string;
+		}
+	}; /** $.UTF8 */
+
+
+	/**
+	 * MyAMS extensions to JQuery
+	 */
+	if (window.MyAMS === undefined) {
+		window.MyAMS = {
+			devmode: true,
+			throttle_delay: 350,
+			menu_speed: 235,
+			navbar_height: 49,
+			ajax_nav: true,
+			enable_widgets: true,
+			enable_mobile: false,
+			enable_fastclick: false,
+			warn_on_form_change: false,
+			ismobile: (/iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent.toLowerCase()))
+		};
+	}
+	var ams = MyAMS;
+
+	/**
+	 * Get MyAMS base URL
+	 * Copyright Andrew Davy: https://forrst.com/posts/Get_the_URL_of_the_current_javascript_file-Dst
+	 */
+	MyAMS.baseURL = (function () {
+		var script = $('script[src$="/myams.js"], script[src$="/myams.min.js"]');
+		var src = script.attr("src");
+		ams.devmode = !src.endsWith('.min.js');
+		return src.substring(0, src.lastIndexOf('/') + 1);
+	})();
+
+
+	/**
+	 * Extract parameter value from given query string
+	 */
+	MyAMS.getQueryVar = function(src, varName) {
+		// Check src
+		if (src.indexOf('?') < 0)
+			return false;
+		if (!src.endsWith('&'))
+			src += '&';
+		// Dynamic replacement RegExp
+		var regex = new RegExp('.*?[&\\?]' + varName + '=(.*?)&.*');
+		// Apply RegExp to the query string
+		var val = src.replace(regex, "$1");
+		// If the string is the same, we didn't find a match - return false
+		return val == src ? false : val;
+	};
+
+
+	/**
+	 * Color conversion function
+	 */
+	MyAMS.rgb2hex = function(color) {
+		return "#" + $.map(color.match(/\b(\d+)\b/g), function(digit) {
+			return ('0' + parseInt(digit).toString(16)).slice(-2)
+		}).join('');
+	};
+
+
+	/**
+	 * Generate a random ID
+	 *
+	 * @param length
+	 */
+	MyAMS.generateId = function() {
+		function s4() {
+			return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+		}
+		return s4() + s4() + s4() + s4();
+	};
+
+
+	/**
+	 * Get and execute a function given by name
+	 * Small piece of code by Jason Bunting
+	 */
+	MyAMS.getFunctionByName = function(functionName, context) {
+		if (functionName === undefined)
+			return undefined;
+		else if (typeof(functionName) == 'function')
+			return functionName;
+		var namespaces = functionName.split(".");
+		var func = namespaces.pop();
+		context = (context === undefined || context === null) ? window : context;
+		for (var i=0; i < namespaces.length; i++) {
+			try {
+				context = context[namespaces[i]];
+			} catch (e) {
+				return undefined;
+			}
+		}
+		try {
+			return context[func];
+		} catch (e) {
+			return undefined;
+		}
+	};
+
+	MyAMS.executeFunctionByName = function(functionName, context /*, args */) {
+		var func = ams.getFunctionByName(functionName, window);
+		if (typeof(func) == 'function') {
+			var args = Array.prototype.slice.call(arguments, 2);
+			return func.apply(context, args);
+		}
+	};
+
+
+	/**
+	 * Get script or CSS file using browser cache
+	 * Script or CSS URLs can include variable names, given between braces, as in
+	 * {MyAMS.baseURL}
+	 */
+	MyAMS.getSource = function(url) {
+		return url.replace(/{[^{}]*}/g, function(match) {
+			return ams.getFunctionByName(match.substr(1, match.length-2));
+		});
+	};
+
+	MyAMS.getScript = function(url, callback, options) {
+		var defaults = {
+			dataType: 'script',
+			url: ams.getSource(url),
+			success: callback,
+			error: ams.error.show,
+			cache: true,
+			async: true
+		};
+		var settings = $.extend({}, defaults, options);
+		return $.ajax(settings);
+	};
+
+	MyAMS.getCSS = function(url, id) {
+		var head = $('HEAD');
+		var css = $('link[data-ams-id="' + id + '"]', head);
+		if (css.length == 0) {
+			$('<link />').attr({rel: 'stylesheet',
+								type: 'text/css',
+								href: ams.getSource(url),
+								'data-ams-id': id})
+						 .appendTo(head);
+		}
+	};
+
+
+	/**
+	 * Events management
+	 */
+	MyAMS.event = {
+
+		stop: function(event) {
+			if (!event) {
+				event = window.event;
+			}
+			if (event) {
+				if (event.stopPropagation) {
+					event.stopPropagation();
+					event.preventDefault();
+				} else {
+					event.cancelBubble = true;
+					event.returnValue = false;
+				}
+			}
+		}
+	};
+
+
+	/**
+	 * Browser testing functions; mostly for IE...
+	 */
+	MyAMS.browser = {
+
+		getInternetExplorerVersion: function() {
+			var rv = -1;
+			if (navigator.appName == "Microsoft Internet Explorer") {
+				var ua = navigator.userAgent;
+				var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
+				if (re.exec(ua) != null)
+					rv = parseFloat(RegExp.$1);
+			}
+			return rv;
+		},
+
+		checkVersion: function() {
+			var msg = "You're not using Windows Internet Explorer.";
+			var ver = this.getInternetExplorerVersion();
+			if (ver > -1) {
+				if (ver >= 8)
+					msg = "You're using a recent copy of Windows Internet Explorer.";
+				else
+					msg = "You should upgrade your copy of Windows Internet Explorer.";
+			}
+			alert(msg);
+		},
+
+		isIE8orlower: function() {
+			var msg = "0";
+			var ver = this.getInternetExplorerVersion();
+			if (ver > -1) {
+				if (ver >= 9)
+					msg = 0;
+				else
+					msg = 1;
+			}
+			return msg;
+		}
+	};
+
+
+	/**
+	 * Errors management features
+	 */
+	MyAMS.error = {
+
+		/**
+		 * Default JQuery AJAX error handler
+		 */
+		ajax: function(event, request /*, settings*/) {
+			if (request.statusText == 'OK')
+				return;
+			var response = ams.ajax.getResponse(request);
+			if (response.content_type == 'json') {
+				ams.ajax.handleJSON(response.data);
+			} else {
+				var title = event.statusText || event.type;
+				var message = request.responseText;
+				ams.skin.messageBox('error', {
+					title: ams.i18n.ERROR_OCCURED,
+					content: '<h4>' + title + '</h4><p>' + message + '</p>',
+					icon: 'fa fa-warning animated shake',
+					timeout: 10000
+				});
+			}
+			if (window.console) {
+				console.error(event);
+				console.debug(request);
+			}
+		},
+
+		/**
+		 * Show AJAX error
+		 */
+		show: function(request, status, error) {
+			if (!error)
+				return;
+			var response = ams.ajax.getResponse(request);
+			if (response.content_type == 'json') {
+				ams.ajax.handleJSON(response.data);
+			} else {
+				ams.skin.messageBox('error', {
+					title: ams.i18n.ERRORS_OCCURED,
+					content: '<h4>' + status + '</h4><p>' + error + '</p>',
+					icon: "fa fa-warning animated shake",
+					timeout: 10000
+				});
+			}
+			if (window.console) {
+				console.error(error);
+				console.debug(request);
+			}
+		}
+	};
+
+
+	/**
+	 * AJAX helper functions
+	 */
+	MyAMS.ajax = {
+
+		/**
+		 * Check for given feature and download script if necessary
+		 *
+		 * @checker: pointer to a javascript object which will be downloaded in undefined
+		 * @source: URL of a javascript file containing requested feature
+		 * @callback: pointer to a function which will be called after the script is downloaded. The first
+		 *   argument of this callback is a boolean value indicating if the script was just downloaded (true)
+		 *   or if the requested object was already loaded (false)
+		 * @options: callback options
+		 */
+		check: function(checker, source, callback, options) {
+			if (typeof(callback) == 'object') {
+				options = callback;
+				callback = undefined;
+			}
+			var defaults = {
+				async: typeof(callback) == 'function'
+			};
+			var settings = $.extend({}, defaults, options);
+			if (checker === undefined) {
+				ams.getScript(source, function() {
+					if (typeof(callback) == 'function')
+						callback(true, options);
+				}, settings);
+			} else {
+				if (typeof(callback) == 'function')
+					callback(false, options);
+			}
+		},
+
+		/**
+		 * Get address relative to current page
+		 */
+		getAddr: function(addr) {
+			var href = addr || $('HTML HEAD BASE').attr('href') || window.location.href;
+			return href.substr(0, href.lastIndexOf("/") + 1);
+		},
+
+		/**
+		 * Handle AJAX upload and download progress
+		 *
+		 * @param event: the source event
+		 * @param request: the request
+		 * @param options: AJAX options
+		 */
+		progress: function(event) {
+			if (!event.lengthComputable)
+				return;
+			if (event.loaded >= event.total)
+				return;
+			console.log(parseInt((event.loaded / event.total * 100), 10) + "%");
+		},
+
+		/**
+		 * Post data to given URL
+		 */
+		post: function(url, data, options, callback) {
+			if (url.startsWith(window.location.protocol))
+				var addr = url;
+			else
+				addr = this.getAddr() + url;
+			if (typeof(options) == 'function') {
+				callback = options;
+				options = {}
+			} else if (!options) {
+				options = {};
+			}
+			if (typeof(callback) == 'undefined')
+				callback = options.callback;
+			if (typeof(callback) == 'string')
+				callback = ams.getFunctionByName(callback);
+			delete options.callback;
+
+			var result = undefined;
+			var defaults = {
+				url: addr,
+				type: 'post',
+				cache: false,
+				async: typeof(callback) == 'function',
+				data: $.param(data, true),
+				dataType: 'json',
+				success: callback || function(data /*, status*/) {
+					result = data.result;
+				},
+				error: ams.error.show
+			};
+			var settings = $.extend({}, defaults, options);
+			$.ajax(settings);
+			return result;
+		},
+
+		/**
+		 * Extract data type and result from response
+		 */
+		getResponse: function(request) {
+			var content_type = request.getResponseHeader('content-type'),
+				data_type,
+				result;
+			if (content_type) {
+				// Got server response
+				if (content_type.startsWith('application/javascript')) {
+					data_type = 'script';
+					result = request.responseText;
+				} else if (content_type.startsWith('text/html')) {
+					data_type = 'html';
+					result = request.responseText;
+				} else if (content_type.startsWith('text/xml')) {
+					data_type = 'xml';
+					result = request.responseText;
+				} else {
+					result = request.responseJSON;
+					if (result)
+						data_type = 'json';
+					else {
+						try {
+							result = JSON.parse(request.responseText);
+							data_type = 'json';
+						} catch (e) {
+							result = request.responseText;
+							data_type = 'text';
+						}
+					}
+				}
+			} else {
+				// Probably no response from server...
+				data_type = 'json';
+				result = {
+					status: 'alert',
+					alert: {
+						title: ams.i18n.ERROR_OCCURED,
+						content: ams.i18n.NO_SERVER_RESPONSE
+					}
+				};
+			}
+			return {content_type: data_type,
+					data: result};
+		},
+
+		/**
+		 * Handle server response in JSON format
+		 *
+		 * Result is made of several JSON attributes:
+		 *  - status: error, success, callback, callbacks, reload or redirect
+		 *  - close_form: boolean indicating if current modal should be closed
+		 *  - location: target URL for reload or redirect status
+		 *  - target: target container's selector for loaded content ('#content' by default)
+		 *  - content: available for any status producing output content:
+		 *        {target: target container's selector (source form by default)
+		 *         html: HTML result}
+		 *  - message: available for any status producing output message:
+		 *        {target: target message container's selector
+		 *         status: message status
+		 *         header: message header
+		 *         subtitle: message subtitle,
+		 *         body: message body}
+		 *
+		 * For errors data structure, please see MyAMS.form.showErrors function
+		 */
+		handleJSON: function(result, form, target) {
+			var status = result.status;
+			var url;
+			switch (status) {
+				case 'alert':
+					alert(result.alert.title + '\n\n' + result.alert.content);
+					break;
+				case 'error':
+					ams.form.showErrors(form, result);
+					break;
+				case 'info':
+				case 'success':
+					if (result.close_form != false)
+						ams.dialog.close(form);
+					break;
+				case 'message':
+				case 'messagebox':
+					break;
+				case 'notify':
+				case 'callback':
+				case 'callbacks':
+					if (result.close_form != false)
+						ams.dialog.close(form);
+					break;
+				case 'modal':
+					ams.dialog.open(result.location);
+					break;
+				case 'reload':
+					if (result.close_form != false)
+						ams.dialog.close(form);
+					url = result.location || window.location.hash;
+					if (url.startsWith('#'))
+						url = url.substr(1);
+					ams.skin.loadURL(url, result.target || target || '#content');
+					break;
+				case 'redirect':
+					if (result.close_form == true)
+						ams.dialog.close(form);
+					url = result.location || window.location.href;
+					if (result.window) {
+						window.open(url, result.window, result.options);
+					} else {
+						window.location.href = url;
+					}
+					break;
+				default:
+					console.log("Unhandled status: " + status);
+					break;
+			}
+			if (result.content) {
+				var content = result.content;
+				var container = $(content.target || target || form || '#content');
+				container.html(content.html);
+				ams.initContent(container);
+			}
+			if (result.message) {
+				var message = result.message;
+				if (typeof(message) == 'string') {
+					if ((status == 'info') || (status == 'success'))
+						ams.skin.smallBox(status,
+										  {title: message,
+										   icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
+										   timeout: 3000});
+					else
+						ams.skin.alert($(form || '#content'), status, message);
+				} else
+					ams.skin.alert($(message.target || target || form || '#content'),
+								   message.status || 'success',
+								   message.header,
+								   message.body,
+								   message.subtitle);
+			}
+			if (result.smallbox) {
+				ams.skin.smallBox(result.smallbox_status || status,
+								  {title: result.smallbox,
+								   icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
+								   timeout: 3000});
+			}
+			if (result.messagebox) {
+				message = result.messagebox;
+				if (typeof(message) == 'string')
+					ams.skin.messageBox('info',
+										{title: ams.i18n.ERROR_OCCURED,
+										 content: message,
+										 timeout: 10000});
+				else {
+					var message_status = message.status || 'info';
+					if (message_status == 'error' && form && target)
+						ams.executeFunctionByName(form.data('ams-form-submit-error') || 'MyAMS.form.finalizeSubmitOnError', form, target);
+					ams.skin.messageBox(message_status,
+										{title: message.title || ams.i18n.ERROR_OCCURED,
+										 content: message.content,
+										 icon: message.icon,
+										 number: message.number,
+										 timeout: message.timeout == null ? undefined : (message.timeout || 10000)});
+				}
+			}
+			if (result.event)
+				form.trigger(result.event, result.event_options);
+			if (result.callback)
+				ams.executeFunctionByName(result.callback, form, result.options);
+			if (result.callbacks) {
+				for (var index in result.callbacks) {
+					if (!$.isNumeric(index))
+						continue;
+					var callback = result.callbacks[index];
+					ams.executeFunctionByName(callback, form, callback.options);
+				}
+			}
+		}
+	};
+
+
+	/**
+	 * JSON-RPC helper functions
+	 */
+	MyAMS.jsonrpc = {
+
+		/**
+		 * Get address relative to current page
+		 */
+		getAddr: function(addr) {
+			var href = addr || $('HTML HEAD BASE').attr('href') || window.location.href;
+			var target = href.replace(/\+\+skin\+\+\w+\//, '');
+			return target.substr(0, target.lastIndexOf("/") + 1);
+		},
+
+		/**
+		 * Execute JSON-RPC request on given method
+		 *
+		 * Query can be given as a simple "query" string or as an object containing all query parameters.
+		 * Parameters:
+		 *  - @query: query string (posted as "query" parameter) or object containing all parameters
+		 *  - @method: name of JSON-RPC procedure to call
+		 *  - @options: additional JSON-RPC procedure parameters
+		 *  - @callback: name of a callback which will be called on server response
+		 */
+		query: function(query, method, options, callback) {
+			ams.ajax.check($.jsonRpc,
+						   ams.baseURL + 'ext/jquery-jsonrpc' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+								var result;
+								if (typeof(options) == 'function') {
+									callback = options;
+									options = {};
+								}
+								else if (!options)
+									options = {};
+								if (typeof(callback) == 'undefined')
+									callback = options.callback;
+								if (typeof(callback) == 'string')
+									callback = ams.getFunctionByName(callback);
+								delete options.callback;
+
+								var params = {};
+								if (typeof(query) == 'string')
+									params['query'] = query;
+								else if (typeof(query) == 'object')
+									$.extend(params, query);
+								$.extend(params, options);
+
+								var settings = {
+									url: ams.jsonrpc.getAddr(options.url),
+									type: 'post',
+									cache: false,
+									method: method,
+									params: params,
+									async: typeof(callback) == 'function',
+									success: callback || function(data /*, status*/) {
+										result = data.result;
+									},
+									error: ams.error.show
+								};
+								$.jsonRpc(settings);
+								return result;
+						   });
+		},
+
+		/**
+		 * Execute given JSON-RPC post on given method
+		 *
+		 * Parameters:
+		 *  - @method: name of JSON-RPC procedure to call
+		 *  - @options: additional JSON-RPC procedure parameters
+		 *  - @callback: name of a callback which will be called on server response
+		 */
+		post: function(method, data, options, callback) {
+			ams.ajax.check($.jsonRpc,
+						   ams.baseURL + 'ext/jquery-jsonrpc' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+								var result;
+								if (typeof(options) == 'function') {
+									callback = options;
+									options = {};
+								}
+								else if (!options)
+									options = {};
+								if (typeof(callback) == 'undefined')
+									callback = options.callback;
+								if (typeof(callback) == 'string')
+									callback = ams.getFunctionByName(callback);
+								delete options.callback;
+
+								var defaults = {
+									url: ams.jsonrpc.getAddr(options.url),
+									type: 'post',
+									cache: false,
+									method: method,
+									params: data,
+									async: typeof(callback) == 'function',
+									success: callback || function(data /*, status*/) {
+										result = data.result;
+									},
+									error: ams.error.show
+								};
+								var settings = $.extend({}, defaults, options);
+								$.jsonRpc(settings);
+								return result;
+						   });
+		}
+	};
+
+
+	/**
+	 * Forms helper functions
+	 */
+	MyAMS.form = {
+
+		/**
+		 * Init forms to activate form change listeners
+		 *
+		 * @param element: the parent element
+		 */
+		init: function(element) {
+			// Activate form changes if required
+			if (ams.warn_on_form_change)
+				var forms = $('FORM[data-ams-warn-on-change!="false"]', element);
+			else
+				forms = $('FORM[data-ams-warn-on-change="true"]', element);
+			forms.each(function() {
+				var form = $(this);
+				$('INPUT[type="text"], ' +
+				  'INPUT[type="checkbox"], ' +
+				  'INPUT[type="radio"], ' +
+				  'SELECT, ' +
+				  'TEXTAREA, ' +
+				  '[data-ams-changed-event]', form).each(function() {
+						var source = $(this);
+						if (source.data('ams-ignore-change') !== true) {
+							var event = source.data('ams-changed-event') || 'change';
+							source.on(event, function () {
+								$(this).parents('FORM').attr('data-ams-form-changed', true);
+							});
+						}
+				});
+				form.on('reset', function() {
+					$(this).removeAttr('data-ams-form-changed');
+				});
+			});
+		},
+
+		/**
+		 * Check for modified forms before exiting
+		 */
+		checkBeforeUnload: function() {
+			var forms = $('FORM[data-ams-form-changed="true"]');
+			if (forms.exists()) {
+				return ams.i18n.FORM_CHANGED_WARNING;
+			}
+		},
+
+		/**
+		 * Check for modified forms before loading new inner content
+		 */
+		confirmChangedForm: function(element, callback) {
+			if (typeof(element) == 'function') {
+				callback = element;
+				element = undefined;
+			}
+			var forms = $('FORM[data-ams-form-changed="true"]', element);
+			if (forms.exists()) {
+				ams.skin.bigBox({
+					title: ams.i18n.WARNING,
+					content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; ' + ams.i18n.FORM_CHANGED_WARNING,
+					buttons: ams.i18n.BTN_OK_CANCEL
+				}, function(button) {
+					if (button == ams.i18n.BTN_OK)
+						callback.call(element);
+				});
+			} else {
+				callback.call(element);
+			}
+		},
+
+		/**
+		 * Submit given form
+		 */
+		submit: function(form, handler, submit_options) {
+			// Check params
+			form = $(form);
+			if (!form.exists())
+				return false;
+			if (typeof(handler) == 'object') {
+				submit_options = handler;
+				handler = undefined;
+			}
+			// Prevent multiple submits of the same form
+			if (form.data('submitted')) {
+				if (!form.data('ams-form-hide-submitted')) {
+					ams.skin.messageBox('warning', {
+						title: ams.i18n.WAIT,
+						content: ams.i18n.FORM_SUBMITTED,
+						icon: 'fa fa-save shake animated',
+						timeout: form.data('ams-form-alert-timeout') || 5000
+					});
+				}
+				return false;
+			}
+			// Check submit validators
+			if (!ams.form._checkSubmitValidators(form))
+				return false;
+			// Remove remaining status messages
+			$('.alert, SPAN.state-error', form).remove();
+			$('.state-error', form).removeClassPrefix('state-');
+			// Check submit button
+			var button = $(form.data('ams-submit-button'));
+			if (button)
+				button.button('loading');
+			ams.ajax.check($.fn.ajaxSubmit,
+						   ams.baseURL + 'ext/jquery-form-3.49' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+
+								function _submitAjaxForm(form, options) {
+
+									var button;
+									var data = form.data();
+									var form_options = data.amsFormOptions;
+
+									if (submit_options)
+										var form_data_callback = submit_options.formDataInitCallback;
+									if (form_data_callback)
+										delete submit_options.formDataInitCallback;
+									else
+										form_data_callback = data.amsFormDataInitCallback;
+									if (form_data_callback) {
+										var veto = {};
+										if (typeof(form_data_callback) == 'function')
+											var form_data = form_data_callback.call(form, veto);
+										else
+											form_data = ams.executeFunctionByName(form_data_callback, form, veto);
+										if (veto.veto) {
+											button = form.data('ams-submit-button');
+											if (button)
+												button.button('reset');
+											ams.form.finalizeSubmitFooter.call(form);
+											return false;
+										}
+									} else {
+										form_data = data.amsFormData || {};
+									}
+
+									button = $(form.data('ams-submit-button'));
+									var buttonHandler,
+										buttonTarget;
+									if (button) {
+										buttonHandler = button.data('ams-form-handler');
+										buttonTarget = button.data('ams-form-submit-target');
+									}
+
+									var form_handler = handler || buttonHandler || data.amsFormHandler || '';
+									if (form_handler.startsWith(window.location.protocol)) {
+										var url = form_handler;
+									} else {
+										var action = form.attr('action').replace(/#/, '');
+										if (action.startsWith(window.location.protocol))
+											url = action;
+										else
+											url = ams.ajax.getAddr() + action;
+										url += form_handler;
+									}
+
+									var target = null;
+									if (data.amsFormInitSubmitTarget) {
+										target = $(buttonTarget || data.amsFormSubmitTarget || '#content');
+										ams.executeFunctionByName(data.amsFormInitSubmit || 'MyAMS.form.initSubmit', form, target);
+									} else if (!data.amsFormHideSubmitFooter)
+										ams.executeFunctionByName(data.amsFormInitSubmit || 'MyAMS.form.initSubmitFooter', form);
+
+									var hasUpload = typeof(options.uuid) != 'undefined';
+									if (hasUpload) {
+										if (url.indexOf('X-Progress-ID') < 0)
+											url += "?X-Progress-ID=" + options.uuid;
+										delete options.uuid;
+									}
+
+									var defaults = {
+										url: url,
+										type: 'post',
+										cache: false,
+										data: form_data,
+										dataType: data.amsFormDatatype,
+										beforeSerialize: function(/*form, options*/) {
+											if (typeof(tinyMCE) != 'undefined')
+												tinyMCE.triggerSave();
+										},
+										beforeSubmit: function(data, form /*, options*/) {
+											form.data('submitted', true);
+										},
+										error: function(request, status, error, form) {
+											if (target)
+												ams.executeFunctionByName(data.amsFormSubmitError || 'MyAMS.form.finalizeSubmitOnError', form, target);
+											if (form.is(':visible')) {
+												var button = form.data('ams-submit-button');
+												if (button)
+													button.button('reset');
+												ams.form.finalizeSubmitFooter.call(form);
+											}
+											form.data('submitted', false);
+											form.removeData('ams-submit-button');
+										},
+										success: function(result, status, request, form) {
+											var callback;
+											var button = form.data('ams-submit-button');
+											if (button)
+												callback = button.data('ams-form-submit-callback');
+											if (!callback)
+												callback = ams.getFunctionByName(data.amsFormSubmitCallback) || ams.form._submitCallback;
+											callback.call(form, result, status, request, form);
+											if (form.is(':visible') && button)
+												button.button('reset');
+											form.data('submitted', false);
+											form.removeData('ams-submit-button');
+											form.removeAttr('data-ams-form-changed');
+										},
+										iframe: hasUpload
+									}
+									var settings = $.extend({}, defaults, options, form_options, submit_options);
+									$(form).ajaxSubmit(settings);
+								}
+
+								var hasUpload = $('INPUT[type="file"]', form).length > 0;
+								if (hasUpload) {
+									// JQuery-progressbar plug-in must be loaded synchronously!!
+									// Otherwise, hidden input fields created by jquery-validate plug-in
+									// and matching named buttons will be deleted (on first form submit)
+									// before JQuery-form plug-in can get them when submitting the form...
+									ams.ajax.check($.progressBar,
+												   ams.baseURL + 'ext/jquery-progressbar' + (ams.devmode ? '.js' : '.min.js'));
+									var settings = $.extend({}, {
+										uuid: $.progressBar.submit(form)
+									});
+									_submitAjaxForm(form, settings);
+								} else
+									_submitAjaxForm(form, {});
+						   });
+			return false;
+		},
+
+		/**
+		 * Initialize AJAX submit call
+		 *
+		 * @param this: the submitted form
+		 * @param target: the form submit container target
+		 * @param message: the optional message
+		 */
+		initSubmit: function(target, message) {
+			var form = $(this);
+			var spin = '<i class="fa fa-3x fa-gear fa-spin"></i>';
+			if (!message)
+				message = form.data('ams-form-submit-message');
+			if (message)
+				spin += '<strong>' + message + '</strong>';
+			$(target).html('<div class="row margin-20"><div class="text-center">' + spin + '</div></div>');
+			$(target).parents('.hidden').removeClass('hidden');
+		},
+
+		/**
+		 * Finalize AJAX submit call
+		 *
+		 * @param target: the form submit container target
+		 */
+		finalizeSubmitOnError: function(target) {
+			$('i', target).removeClass('fa-spin')
+						  .removeClass('fa-gear')
+						  .addClass('fa-ambulance');
+		},
+
+		/**
+		 * Initialize AJAX submit call in form footer
+		 *
+		 * @param this: the submitted form
+		 * @param message: the optional submit message
+		 */
+		initSubmitFooter: function(message) {
+			var form = $(this);
+			var spin = '<i class="fa fa-3x fa-gear fa-spin"></i>';
+			if (!message)
+				message = $(this).data('ams-form-submit-message');
+			if (message)
+				spin += '<strong class="submit-message align-top padding-left-10 margin-top-10">' + message + '</strong>';
+			var footer = $('footer', form);
+			$('button', footer).hide();
+			footer.append('<div class="row"><div class="text-center">' + spin + '</div></div>');
+		},
+
+		/**
+		 * Finalize AJAX submit call
+		 *
+		 * @param this: the submitted form
+		 * @param target: the form submit container target
+		 */
+		finalizeSubmitFooter: function(/*target*/) {
+			var form = $(this);
+			var footer = $('footer', form);
+			if (footer) {
+				$('.row', footer).remove();
+				$('button', footer).show();
+			}
+		},
+
+		/**
+		 * Handle AJAX submit results
+		 *
+		 * Submit results are auto-detected via response content type, except when this content type
+		 * is specified into form's data attributes.
+		 * Submit response can be of several content types:
+		 * - html or text: the response is directly included into a "target" container (#content by default)
+		 * - json: a "status" attribute indicates how the request was handled and how the response should be
+		 *   treated:
+		 *     - error: indicates that an error occured; other response attributes indicate error messages
+		 *     - success: basic success, no other action is requested
+		 *     - callback: only call given function to handle the result
+		 *     - callbacks: only call given set of functions to handle the result
+		 *     - reload: page's body should be reloaded from a given URL
+		 *     - redirect: redirect browser to given URL
+		 *   Each JSON response can also specify an HTML content, a message and a callback (
+		 */
+		_submitCallback: function(result, status, request, form) {
+
+			if (form.is(':visible')) {
+				ams.form.finalizeSubmitFooter.call(form);
+				var button = form.data('ams-submit-button');
+				if (button)
+					button.button('reset');
+			}
+			var data = form.data();
+			if (data.amsFormDatatype)
+				var data_type = data.amsFormDatatype;
+			else {
+				var request_data = ams.ajax.getResponse(request);
+				data_type = request_data.content_type;
+				result = request_data.data;
+			}
+
+			if (button)
+				var target = $(button.amsFormSubmitTarget || data.amsFormSubmitTarget || '#content');
+			else
+				target = $(data.amsFormSubmitTarget || '#content');
+
+			switch (data_type) {
+				case 'json':
+					ams.ajax.handleJSON(result, form, target);
+					break;
+				case 'script':
+					break;
+				case 'xml':
+					break;
+				case 'html':
+				case 'text':
+				default:
+					if (button && (button.data('ams-keep-modal') !== true))
+						ams.dialog.close(form);
+					if (!target.exists())
+						target = $('body');
+					target.parents('.hidden').removeClass('hidden');
+					$('.alert', target.parents('.alerts-container')).remove();
+					target.css({opacity: '0.0'})
+						  .html(result)
+						  .delay(50)
+						  .animate({opacity: '1.0'}, 300);
+					ams.initContent(target);
+			}
+			var callback = request.getResponseHeader('X-AMS-Callback');
+			if (callback) {
+				var options = request.getResponseHeader('X-AMS-Callback-Options');
+				ams.executeFunctionByName(callback, form, options === undefined ? {} : JSON.parse(options), request);
+			}
+		},
+
+		/**
+		 * Get list of custom validators called before submit
+		 */
+		_getSubmitValidators: function(form) {
+			var validators = new Array();
+			var form_validator = form.data('ams-form-validator');
+			if (form_validator)
+				validators.push([form, form_validator]);
+			$('[data-ams-form-validator]', form).each(function() {
+				var source = $(this);
+				validators.push([source, source.data('ams-form-validator')]);
+			});
+			return validators;
+		},
+
+		/**
+		 * Call list of custom validators before submit
+		 *
+		 * Each validator can return:
+		 *  - a boolean 'false' value to just specify that an error occured
+		 *  - a string value containing an error message
+		 *  - an array containing a list of string error messages
+		 * Any other value (undefined, null, True...) will lead to a successful submit.
+		 */
+		_checkSubmitValidators: function(form) {
+			var validators = ams.form._getSubmitValidators(form);
+			if (!validators.length)
+				return true;
+			var output = new Array();
+			var result = true;
+			for (var index in validators) {
+				if (!$.isNumeric(index))  // IE check !
+					continue;
+				var validator = validators[index];
+				var source = validator[0];
+				var handler = validator[1];
+				var validator_result = ams.executeFunctionByName(handler, form, source);
+				if (validator_result === false)
+					result = false;
+				else if (typeof(validator_result) == 'string')
+					output.push(validator_result);
+				else if (result.length && (result.length > 0))
+					output = output.concat(result);
+			}
+			if (output.length > 0) {
+				var header = output.length == 1 ? ams.i18n.ERROR_OCCURED : ams.i18n.ERRORS_OCCURED;
+				ams.skin.alert(form, 'danger', header, output);
+				return false;
+			} else
+				return result;
+		},
+
+		/**
+		 * Display JSON errors
+		 * JSON errors should be defined in an object as is:
+		 * {status: 'error',
+		 *  error_message: "Main error message",
+		 *  messages: ["Message 1", "Message 2",...]
+		 *  widgets: [{label: "First widget name",
+		 *             name: "field-name-1",
+		 *             message: "Error message"},
+		 *            {label: "Second widget name",
+		 *             name: "field-name-2",
+		 *             message: "Second error message"},...]}
+		 */
+		showErrors: function(form, errors) {
+			if (typeof(errors) == 'string') {
+				ams.skin.alert(form, 'error', ams.i18n.ERROR_OCCURED, errors)
+			} else if (errors instanceof Array) {
+				var header = errors.length == 1 ? ams.i18n.ERROR_OCCURED : ams.i18n.ERRORS_OCCURED;
+				ams.skin.alert(form, 'error', header, errors);
+			} else {
+				header = errors.widgets && (errors.widgets.length > 1) ? ams.i18n.ERRORS_OCCURED : ams.i18n.ERROR_OCCURED;
+				var message = new Array();
+				var index;
+				for (index in errors.messages) {
+					if (!$.isNumeric(index))
+						continue;
+					message.push(errors.messages[index].message || errors.messages[index]);
+				}
+				for (index in errors.widgets) {
+					if (!$.isNumeric(index))
+						continue;
+					var widget = errors.widgets[index];
+					$('[name="' + widget.name + '"]', form).parent('label')
+														   .removeClassPrefix('state-')
+														   .addClass('state-error')
+														   .after('<span for="name" class="state-error">' + widget.message + '</span>');
+					if (widget.label) {
+						message.push(widget.label + ' : ' + widget.message);
+					}
+				}
+				ams.skin.alert(form, 'error', header, message, errors.error_message);
+			}
+		}
+	};
+
+
+	/**
+	 * Modal dialogs helper functions
+	 */
+	MyAMS.dialog = {
+
+		/**
+		 * Modal dialog opener
+		 */
+		open: function(source, options) {
+			ams.ajax.check($.fn.modalmanager,
+						   ams.baseURL + 'ext/bootstrap-modalmanager' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+								ams.ajax.check($.fn.modal.defaults,
+											   ams.baseURL + 'ext/bootstrap-modal' + (ams.devmode ? '.js' : '.min.js'),
+								function(first_load) {
+									if (first_load) {
+										$(document).off('click.modal');
+										$.fn.modal.defaults.spinner = $.fn.modalmanager.defaults.spinner =
+											'<div class="loading-spinner" style="width: 200px; margin-left: -100px;">' +
+												'<div class="progress progress-striped active">' +
+													'<div class="progress-bar" style="width: 100%;"></div>' +
+												'</div>' +
+											'</div>';
+									}
+									if (typeof(source) == 'string') {
+										var source_data = {};
+										var url = source;
+									} else {
+										source_data = source.data();
+										url = source.attr('href') || source_data.amsUrl;
+										var url_getter = ams.getFunctionByName(url);
+										if (typeof(url_getter) == 'function') {
+											url = url_getter.call(source);
+										}
+									}
+									if (!url)
+										return;
+									$('body').modalmanager('loading');
+									if (url.indexOf('#') == 0) {
+										// Inner hidden modal dialog
+										$(url).modal('show');
+									} else {
+										// Remote URL modal dialog
+										$.ajax({
+											url: url,
+											type: 'get',
+											cache: source_data.amsAllowCache === undefined ? false : source_data.amsAllowCache,
+											data: options,
+											success: function(data, status, request) {
+												$('body').modalmanager('removeLoading');
+												var request_data = ams.ajax.getResponse(request);
+												var data_type = request_data.content_type;
+												var result = request_data.data;
+												switch (data_type) {
+													case 'json':
+														ams.ajax.handleJSON(result, $($(source).data('ams-json-target') || '#content'));
+														break;
+													case 'script':
+														break;
+													case 'xml':
+														break;
+													case 'html':
+													case 'text':
+													default:
+														var content = $(result);
+														var dialog = $('.modal-dialog', content.wrap('<div></div>').parent());
+														var dialog_data = dialog.data();
+														var data_options = {
+															overflow: dialog_data.amsModalOverflow || '.modal-viewport',
+															maxHeight: dialog_data.amsModalMaxHeight === undefined
+																	? function() {
+																		return $(window).height() -
+																					$('.modal-header', content).outerHeight(true) -
+																					$('footer', content).outerHeight(true) - 85;
+																	}
+																	: ams.getFunctionByName(dialog_data.amsModalMaxHeight)
+														};
+														var settings = $.extend({}, data_options, dialog_data.amsModalOptions);
+														settings = ams.executeFunctionByName(dialog_data.amsModalInitCallback, dialog, settings) || settings;
+														$('<div>').addClass('modal fade')
+																  .append(content)
+																  .modal(settings);
+														ams.initContent(content);
+												}
+											}
+										});
+									}
+								});
+						   });
+		},
+
+		/**
+		 * Modals shown callback
+		 * This callback is used to initialize modal's viewport size
+		 */
+		shown: function(e) {
+
+			function resetViewport(ev) {
+				var top = $('.scrollmarker.top', viewport);
+				var top_position = viewport.scrollTop();
+				if (top_position > 0)
+					top.show();
+				else
+					top.hide();
+				var bottom = $('.scrollmarker.bottom', viewport);
+				if (maxHeight + top_position >= viewport.get(0).scrollHeight)
+					bottom.hide();
+				else
+					bottom.show();
+			}
+
+			var modal = e.target;
+			var viewport = $('.modal-viewport', modal);
+			if (viewport.length == 0)
+				return;
+			var maxHeight = parseInt(viewport.css('max-height'));
+			var barWidth = $.scrollbarWidth();
+			if (viewport.height() == maxHeight) {
+				$('<div></div>').addClass('scrollmarker')
+								.addClass('top')
+								.css('top', 0)
+								.css('width', viewport.width() - barWidth)
+								.hide()
+								.appendTo(viewport);
+				$('<div></div>').addClass('scrollmarker')
+								.addClass('bottom')
+								.css('top', maxHeight - 20)
+								.css('width', viewport.width() - barWidth)
+								.appendTo(viewport);
+				viewport.scroll(resetViewport);
+				viewport.off('resize')
+						.on('resize', resetViewport);
+			} else {
+				$('.scrollmarker', viewport).remove();
+			}
+		},
+
+		/**
+		 * Close modal dialog associated with given context
+		 */
+		close: function(context) {
+			var modal = context.parents('.modal').data('modal');
+			if (modal) {
+				var manager = $('body').data('modalmanager');
+				if (manager && (manager.getOpenModals().indexOf(modal) >= 0))
+					modal.hide();
+			}
+		}
+	};
+
+
+	/**
+	 * Plug-ins helpers functions
+	 *
+	 * These helpers functions are used by several JQuery plug-in extensions.
+	 * They have been extracted from these extensions management code to reuse them more easily into
+	 * application specific callbacks.
+	 */
+	MyAMS.helpers = {
+
+		/** Clear Select2 slection */
+		select2ClearSelection: function() {
+			var source = $(this);
+			var label = source.parents('label');
+			var target = source.data('ams-select2-target');
+			$('INPUT[name="' + target + '"]', label).data('select2').val('');
+		},
+
+		/** Select2 selection formatter */
+		select2FormatSelection: function(object, container) {
+			if (object instanceof Array) {
+				$(object).each(function() {
+					if (typeof(this) == 'object')
+						container.append(this.text);
+					else
+						container.append(this);
+				});
+			} else {
+				if (typeof(object) == 'object')
+					container.append(object.text);
+				else
+					container.append(object);
+			}
+		},
+
+		/** Select2 query results callback */
+		select2QueryUrlResultsCallback: function(data, page, context) {
+			switch (data.status) {
+				case 'error':
+					ams.skin.messageBox('error', {
+						title: ams.i18n.ERROR_OCCURED,
+						content: '<h4>' + data.error_message + '</h4>',
+						icon: "fa fa-warning animated shake",
+						timeout: 10000
+					});
+					break;
+				case 'modal':
+					$(this).data('select2').dropdown.hide();
+					ams.dialog.open(result.location);
+					break;
+				default:
+					return {
+						results: data.results || data,
+						more: data.has_more || false,
+						context: data.context
+					};
+			}
+		},
+
+		/** Select2 JSON-RPC success callback */
+		select2QueryMethodSuccessCallback: function(data, status, options) {
+			var result = data.result;
+			if (typeof(result) == 'string') {
+				try {
+					result = JSON.parse(result);
+				} catch (e) {}
+			}
+			switch (result.status) {
+				case 'error':
+					ams.skin.messageBox('error', {
+						title: ams.i18n.ERROR_OCCURED,
+						content: '<h4>' + result.error_message + '</h4>',
+						icon: "fa fa-warning animated shake",
+						timeout: 10000
+					});
+					break;
+				case 'modal':
+					$(this).data('select2').dropdown.hide();
+					ams.dialog.open(result.location);
+					break;
+				default:
+					options.callback({
+						results: result.results || result,
+						more: result.has_more || false,
+						context: result.context
+					});
+			}
+		}
+	};
+
+
+	/**
+	 * Plug-ins management features
+	 *
+	 * Only basic JQuery, Bootstrap and MyAMS javascript extensions are typically loaded from main page.
+	 * Other JQuery plug-ins may be loaded dynamically.
+	 * Several JQuery extension plug-ins are already included and pre-configured by MyAMS. Other external
+	 * plug-ins can be defined and loaded dynamically using simple "data" attributes.
+	 *
+	 * WARNING: any plug-in implicated into a form submit process (like JQuery-form or JQuery-progressbar)
+	 * must be loaded in a synchronous way. Otherwise, if you use named buttons to submit your forms,
+	 * dynamic hidden input fields created by JQuery-validate plug-in will be removed from the form
+	 * before the form is submitted!
+	 */
+	MyAMS.plugins = {
+
+		/**
+		 * Initialize list of content plug-ins
+		 */
+		init: function(element) {
+
+			// Initialize custom data attributes
+			ams.plugins.initData(element);
+
+			// Check for disabled plug-ins
+			var disabled = new Array();
+			$('[data-ams-plugins-disabled]', element).each(function() {
+				var plugins = $(this).data('ams-plugins-disabled').split(/\s+/);
+				for (var name in plugins) {
+					disabled.push(plugins[name]);
+				}
+			});
+
+			// Run already enabled plug-ins
+			for (var index in ams.plugins.enabled) {
+				if (disabled.indexOf(index) >= 0)
+					continue;
+				var plugin = ams.plugins.enabled[index];
+				if (typeof(plugin) == 'function')
+					plugin(element);
+			}
+
+			// Load, run and register new plug-ins
+			var name;
+			$('[data-ams-plugins]', element).each(function() {
+				var source = $(this);
+				var plugins = {}
+				if (typeof(source.data('ams-plugins')) === 'string') {
+					var names = source.data('ams-plugins').split(/\s+/);
+					for (var index in names) {
+						name = names[index];
+						var plugin_options = {
+							src: source.data('ams-plugin-' + name + '-src'),
+							css: source.data('ams-plugin-' + name + '-css'),
+							callback: source.data('ams-plugin-' + name + '-callback'),
+							register: source.data('ams-plugin-' + name + '-register'),
+							async: source.data('ams-plugin-' + name + '-async')
+						}
+						plugins[name] = plugin_options;
+					}
+				} else {
+					plugins = source.data('ams-plugins');
+				}
+				for (name in plugins) {
+					if (ams.plugins.enabled[name] === undefined) {
+						var plugin = plugins[name];
+						ams.getScript(plugin.src, function() {
+							var callback = plugin.callback;
+							if (callback) {
+								var called = ams.getFunctionByName(callback);
+								if (typeof(called) == 'function')
+									called.apply(source);
+								if (plugin.register !== false)
+									ams.plugins.enabled[name] = called;
+							} else {
+								if (plugin.register !== false)
+									ams.plugins.enabled[name] = null;
+							}
+							var css = plugin['css'];
+							if (css) {
+								ams.getCSS(css, name + '_css');
+							}
+						}, {
+							async: plugin.async === undefined ? true : plugin.async
+						});
+					}
+				}
+			});
+		},
+
+		/**
+		 * Data initializer
+		 * This plug-in converts a single JSON "data-ams-data" attribute into a set of several equivalent "data-" attributes.
+		 * This way of defining data attributes can be used with HTML templates engines which don't allow you
+		 * to create dynamic attributes easily.
+		 */
+		initData: function(element) {
+			$('[data-ams-data]', element).each(function() {
+				var handler = $(this);
+				var data = handler.data('ams-data');
+				for (var index in data) {
+					handler.attr('data-' + index, data[index]);
+				}
+			});
+		},
+
+		/**
+		 * Map of enabled plug-ins
+		 * This map can be extended by external plug-ins.
+		 *
+		 * Standard MyAMS plug-ins management method generally includes:
+		 * - applying a class matching plug-in name on a set of HTML entities to apply the plug-in
+		 * - defining a set of data-attributes on each of these entities to customize the plug-in
+		 * For each standard plug-in, you can also provide an options object (to define plug-in options not handled
+		 * by default MyAMS initialization engine) and an initialization callback (to define these options dynamically).
+		 * Another callback can also be provided to be called after plug-in initialization.
+		 */
+		enabled: {
+
+			/**
+			 * Label hints
+			 */
+			hint: function(element) {
+				var hints = $('.hint:not(:parents(.nohints))', element);
+				if (hints.length > 0)
+					ams.ajax.check($.fn.tipsy,
+								   ams.baseURL + 'ext/jquery-tipsy' + (ams.devmode ? '.js' : '.min.js'),
+								   function() {
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-tipsy' + (ams.devmode ? '.css' : '.min.css'),
+												  'jquery-tipsy');
+										hints.each(function() {
+											var hint = $(this);
+											var data = hint.data();
+											var data_options = {
+												html: data.amsHintHtml,
+												title: ams.getFunctionByName(data.amsHintTitleGetter) || function() {
+													var hint = $(this);
+													return hint.attr('original-title') ||
+														   hint.attr(data.amsHintTitleAttr || 'title') ||
+														   (data.amsHintHtml ? hint.html() : hint.text());
+												},
+												opacity: data.amsHintOpacity,
+												gravity: data.amsHintGravity || 'sw',
+												offset: data.amsHintOffset || 0
+											};
+											var settings = $.extend({}, data_options, data.amsHintOptions);
+											settings = ams.executeFunctionByName(data.amsHintInitCallback, hint, settings) || settings;
+											var plugin = hint.tipsy(settings);
+											ams.executeFunctionByName(data.amsHintAfterInitCallback, hint, plugin, settings);
+										});
+								   });
+			},
+
+			/**
+			 * Fieldset legend switcher
+			 */
+			switcher: function(element) {
+				$('LEGEND.switcher', element).each(function() {
+					var legend = $(this);
+					var fieldset = legend.parent('fieldset');
+					var data = legend.data();
+					if (!data.amsSwitcher) {
+						$('<i class="fa fa-fw"></i>')
+							.prependTo($(this))
+							.addClass(data.amsSwitcherState == 'open' ?
+									  (data.amsSwitcherMinusClass || 'fa-minus') :
+									  (data.amsSwitcherPlusClass || 'fa-plus'));
+						legend.on('click', function(e) {
+							e.preventDefault();
+							var veto = {};
+							legend.trigger('ams.switcher.before-switch', [legend, veto]);
+							if (veto.veto)
+								return;
+							if (fieldset.hasClass('switched')) {
+								fieldset.removeClass('switched');
+								$('.fa', legend).removeClass(data.amsSwitcherPlusClass || 'fa-plus')
+												.addClass(data.amsSwitcherMinusClass || 'fa-minus');
+								legend.trigger('ams.switcher.opened', [legend]);
+								var id = legend.attr('id');
+								if (id) {
+									$('legend.switcher[data-ams-switcher-sync="'+id+'"]', fieldset).each(function() {
+										var switcher = $(this);
+										if (switcher.parents('fieldset').hasClass('switched'))
+											switcher.click();
+									});
+								}
+							} else {
+								fieldset.addClass('switched');
+								$('.fa', legend).removeClass(data.amsSwitcherMinusClass || 'fa-minus')
+												.addClass(data.amsSwitcherPlusClass || 'fa-plus');
+								legend.trigger('ams.switcher.closed', [legend]);
+							}
+						});
+						if (data.amsSwitcherState != 'open')
+							fieldset.addClass('switched');
+						legend.data('ams-switcher', 'on');
+					}
+				});
+			},
+
+			/**
+			 * Fieldset legend checker
+			 */
+			checker: function(element) {
+				$('LEGEND.checker', element).each(function() {
+					var legend = $(this);
+					var fieldset = legend.parent('fieldset');
+					var data = legend.data();
+					if (!data.amsChecker) {
+						var checker = $('<label class="checkbox"></label>');
+						var fieldname = data.amsCheckerFieldname || ('checker_'+ams.generateId());
+						var prefix = data.amsCheckerHiddenPrefix;
+						var hidden = null;
+						var checkedValue = data.amsCheckerHiddenValueOn || 'true';
+						var uncheckedValue = data.amsCheckerHiddenValueOff || 'false';
+						if (prefix) {
+							hidden = $('<input type="hidden">').attr('name', prefix + fieldname)
+															   .val(data.amsCheckerState == 'on' ? checkedValue : uncheckedValue)
+															   .prependTo(legend);
+						}
+						var input = $('<input type="checkbox">').attr('name', fieldname)
+																.attr('id', fieldname.replace(/\./, '_'))
+																.data('ams-checker-hidden-input', hidden)
+																.data('ams-checker-init', true)
+																.val(true)
+																.attr('checked', data.amsCheckerState == 'on' ? 'checked' : null);
+						if (data.amsCheckerReadonly) {
+							input.attr('disabled', 'disabled');
+						} else {
+							input.on('change', function(e) {
+								e.preventDefault();
+								var veto = {};
+								legend.trigger('ams.checker.before-switch', [legend, veto]);
+								if (veto.veto)
+									return;
+								var isChecked = $(this).is(':checked');
+								ams.executeFunctionByName(data.amsCheckerChangeHandler, legend, isChecked);
+								if (!data.amsCheckerCancelDefault) {
+									var hidden = input.data('ams-checker-hidden-input');
+									if (isChecked) {
+										if (data.amsCheckerMode == 'disable')
+											fieldset.removeAttr('disabled');
+										else
+											fieldset.removeClass('switched');
+										if (hidden)
+											hidden.val(checkedValue);
+										$('[data-required]', fieldset).attr('required', 'required');
+										legend.trigger('ams.checker.opened', [legend]);
+									} else {
+										if (data.amsCheckerMode == 'disable')
+											fieldset.attr('disabled', 'disabled');
+										else
+											fieldset.addClass('switched');
+										if (hidden)
+											hidden.val(uncheckedValue);
+										$('[data-required]', fieldset).removeAttr('required');
+										legend.trigger('ams.checker.closed', [legend]);
+									}
+								}
+							});
+						}
+						input.appendTo(checker);
+						$('.legend', legend).attr('for', input.attr('id'));
+						checker.append('<i></i>')
+							   .prependTo(legend);
+						var required = $('[required]', fieldset);
+						required.attr('data-required', true);
+						if (data.amsCheckerState == 'on') {
+							input.attr('checked', true);
+						} else {
+							if (data.amsCheckerMode == 'disable')
+								fieldset.attr('disabled', 'disabled');
+							else
+								fieldset.addClass('switched');
+							required.removeAttr('required');
+						}
+						legend.data('ams-checker', 'on');
+					}
+				});
+			},
+
+			/**
+			 * Sliders
+			 */
+			slider: function(element) {
+				var sliders = $('.slider', element);
+				if (sliders.length > 0) {
+					ams.ajax.check($.fn.slider,
+								   ams.baseURL + 'ext/bootstrap-slider.min.js',
+								   function() {
+										sliders.each(function() {
+											var slider = $(this);
+											var data = slider.data();
+											var data_options = {};
+											var settings = $.extend({}, data_options, slider.data.amsSliderOptions);
+											settings = ams.executeFunctionByName(data.amsSliderInitCallback, slider, settings) || settings;
+											var plugin = slider.slider(settings);
+											ams.executeFunctionByName(data.amsSliderAfterInitCallback, slider, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * Select2 plug-in
+			 */
+			select2: function(element) {
+				var selects = $('.select2', element);
+				if (selects.length > 0) {
+					ams.ajax.check($.fn.select2,
+								   ams.baseURL + 'ext/jquery-select2-3.5.2' + (ams.devmode ? '.js' : '.min.js'),
+								   function() {
+										selects.each(function() {
+											var select = $(this);
+											var data = select.data();
+											var data_options = {
+												placeholder: data.amsSelect2Placeholder,
+												multiple: data.amsSelect2Multiple,
+												minimumInputLength: data.amsSelect2MinimumInputLength || 0,
+												maximumSelectionSize: data.amsSelect2MaximumSelectionSize,
+												openOnEnter: data.amsSelect2EnterOpen === undefined ? true : data.amsSelect2EnterOpen,
+												allowClear: data.amsSelect2AllowClear === undefined ? true : data.amsSelect2AllowClear,
+												width: data.amsSelect2Width || '100%',
+												initSelection: ams.getFunctionByName(data.amsSelect2InitSelection),
+												formatSelection: data.amsSelect2FormatSelection === undefined
+																	? ams.helpers.select2FormatSelection
+																	: ams.getFunctionByName(data.amsSelect2FormatSelection),
+												formatResult: ams.getFunctionByName(data.amsSelect2FormatResult),
+												formatMatches: data.amsSelect2FormatMatches === undefined
+																	? function(matches) {
+																		if (matches == 1)
+																			return ams.i18n.SELECT2_MATCH;
+																		else
+																			return matches + ams.i18n.SELECT2_MATCHES;
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatMatches),
+												formatNoMatches: data.amsSelect2FormatResult === undefined
+																	? function(term) {
+																		return ams.i18n.SELECT2_NOMATCHES;
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatResult),
+												formatInputTooShort: data.amsSelect2FormatInputTooShort === undefined
+																	? function(input, min) {
+																		var n = min - input.length;
+																		return ams.i18n.SELECT2_INPUT_TOOSHORT
+																						.replace(/\{0\}/, n)
+																						.replace(/\{1\}/, n == 1 ? "" : ams.i18n.SELECT2_PLURAL);
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatInputTooShort),
+												formatInputTooLong: data.amsSelect2FormatInputTooLong === undefined
+																	? function(input, max) {
+																		var n = input.length - max;
+																		return ams.i18n.SELECT2_INPUT_TOOLONG
+																						.replace(/\{0\}/, n)
+																						.replace(/\{1\}/, n == 1 ? "" : ams.i18n.SELECT2_PLURAL);
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatInputTooLong),
+												formatSelectionTooBig: data.amsSelect2FormatSelectionTooBig === undefined
+																	? function(limit) {
+																		return ams.i18n.SELECT2_SELECTION_TOOBIG
+																						.replace(/\{0\}/, limit)
+																						.replace(/\{1\}/, limit == 1 ? "" : ams.i18n.SELECT2_PLURAL);
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatSelectionTooBig),
+												formatLoadMore: data.amsSelect2FormatLoadMore === undefined
+																	? function (pageNumber) {
+																		return ams.i18n.SELECT2_LOADMORE;
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatLoadMore),
+												formatSearching: data.amsSelect2FormatSearching === undefined
+																	? function() {
+																		return ams.i18n.SELECT2_SEARCHING;
+																	}
+																	: ams.getFunctionByName(data.amsSelect2FormatSearching),
+												separator: data.amsSelect2Separator || ',',
+												tokenSeparators: data.amsSelect2TokensSeparators || [','],
+												tokenizer: ams.getFunctionByName(data.amsSelect2Tokenizer)
+											};
+
+											switch (select.context.type) {
+												case 'text':
+												case 'hidden':
+													if (!data_options.initSelection) {
+														var values_data = select.data('ams-select2-values');
+														if (values_data) {
+															data_options.initSelection = function(element, callback) {
+																var data = [];
+																$(element.val().split(data_options.separator)).each(function() {
+																	data.push({id: this,
+																			   text: values_data[this] || this});
+																});
+																callback(data);
+															}
+														}
+													}
+													break;
+												default:
+													break;
+											}
+
+											if (data.amsSelect2Query) {
+												// Custom query method
+												data_options.query = ams.getFunctionByName(data.amsSelect2Query);
+												data_options.minimumInputLength = data.amsSelect2MinimumInputLength || 1;
+											} else if (data.amsSelect2QueryUrl) {
+												// AJAX query
+												data_options.ajax = {
+													url: data.amsSelect2QueryUrl,
+													quietMillis: data.amsSelect2QuietMillis || 200,
+													type: data.amsSelect2QueryType || 'POST',
+													dataType: data.amsSelect2QueryDatatype || 'json',
+													data: function(term, page, context) {
+														var options = {};
+														options[data.amsSelect2QueryParamName || 'query'] = term;
+														options[data.amsSelect2PageParamName || 'page'] = page;
+														options[data.amsSelect2ContextParamName || 'context'] = context;
+														return $.extend({}, options, data.amsSelect2QueryOptions);
+													},
+													results: ams.helpers.select2QueryUrlResultsCallback
+												};
+												data_options.minimumInputLength = data.amsSelect2MinimumInputLength || 1;
+											} else if (data.amsSelect2QueryMethod) {
+												// JSON-RPC query
+												data_options.query = function(options) {
+													var settings = {
+														url: data.amsSelect2MethodTarget || ams.jsonrpc.getAddr(),
+														type: data.amsSelect2MethodType || 'POST',
+														cache: false,
+														method: data.amsSelect2QueryMethod,
+														params: data.amsSelect2QueryParams || {},
+														success: function(data, status) {
+															return ams.helpers.select2QueryMethodSuccessCallback.call(select, data, status, options);
+														},
+														error: ams.error.show
+													};
+													settings.params[data.amsSelect2QueryParamName || 'query'] = options.term;
+													settings.params[data.amsSelect2PageParamName || 'page'] = options.page;
+													settings.params[data.amsSelect2ContextParamName || 'context'] = options.context;
+													settings = $.extend({}, settings, data.amsSelect2QueryOptions);
+													settings = ams.executeFunctionByName(data.amsSelect2QueryInitCallback, select, settings) || settings;
+													ams.ajax.check($.jsonRpc,
+																   ams.baseURL + 'ext/jquery-jsonrpc' + (ams.devmode ? '.js' : '.min.js'),
+																   function() {
+																		$.jsonRpc(settings);
+																   });
+												};
+												data_options.minimumInputLength = data.amsSelect2MinimumInputLength || 1;
+											} else if (data.amsSelect2Tags) {
+												// Tags mode
+												data_options.tags = data.amsSelect2Tags;
+											} else if (data.amsSelect2Data) {
+												// Provided data mode
+												data_options.data = data.amsSelect2Data;
+											}
+
+											if (data.amsSelect2EnableFreeTags) {
+												data_options.createSearchChoice = function(term) {
+													return {id: term,
+															text: (data.amsSelect2FreeTagsPrefix || ams.i18n.SELECT2_FREETAG_PREFIX) + term};
+												};
+											}
+
+											var settings = $.extend({}, data_options, data.amsSelect2Options);
+											settings = ams.executeFunctionByName(data.amsSelect2InitCallback, select, settings) || settings;
+											var plugin = select.select2(settings);
+											ams.executeFunctionByName(data.amsSelect2AfterInitCallback, select, plugin, settings);
+
+											select.on('change', function() {
+												var validator = $(select.get(0).form).data('validator');
+												if (validator !== undefined)
+													$(select).valid();
+											});
+										});
+								   });
+				}
+			},
+
+			/**
+			 * Edit mask plug-in
+			 */
+			maskedit: function(element) {
+				var masks = $('[data-mask]', element);
+				if (masks.length > 0) {
+					ams.ajax.check($.fn.mask,
+								   ams.baseURL + 'ext/jquery-maskedinput-1.3.1.min.js',
+								   function() {
+										masks.each(function() {
+											var mask = $(this);
+											var data = mask.data();
+											var data_options = {
+												placeholder: data.amsMaskeditPlaceholder || 'X'
+											};
+											var settings = $.extend({}, data_options, data.amsMaskeditOptions);
+											settings = ams.executeFunctionByName(data.amsMaskeditInitCallback, mask, settings) || settings;
+											var plugin = mask.mask(mask.attr('data-mask'), settings);
+											ams.executeFunctionByName(data.amsMaskeditAfterInitCallback, mask, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * JQuery-UI date picker
+			 */
+			datepicker: function(element) {
+				var datepickers = $('.datepicker', element);
+				if (datepickers.length > 0) {
+					datepickers.each(function() {
+						var picker = $(this);
+						var data = picker.data();
+						var data_options = {
+							dateFormat: data.amsDatepickerFormat || 'dd/mm/yy',
+							prevText: '<i class="fa fa-chevron-left"></i>',
+							nextText: '<i class="fa fa-chevron-right"></i>',
+							changeMonth: data.amsDatepickerChangeMonth,
+							changeYear: data.amsDatepickerChangeYear,
+							showButtonPanel: !data.amsDatepickerHidePanel
+						};
+						var settings = $.extend({}, data_options, data.amsDatepickerOptions);
+						settings = ams.executeFunctionByName(data.amsDatepickerInitCallback, picker, settings) || settings;
+						var plugin = picker.datepicker(settings);
+						ams.executeFunctionByName(data.amsDatepickerAfterInitCallback, picker, plugin, settings);
+					});
+				}
+			},
+
+			/**
+			 * JQuery typeahead plug-in
+			 */
+			typeahead: function(element) {
+				var typeaheads = $('.typeahead', element);
+				if (typeaheads.length > 0) {
+					ams.ajax.check($.fn.typeahead,
+								   ams.baseURL + 'ext/jquery-typeahead' + (ams.devmode ? '.js' : '.min.js'),
+								   function() {
+										typeaheads.each(function() {
+											var input = $(this);
+											var data = input.data();
+											var data_options = {};
+											var settings = $.extend({}, data_options, data.amsTypeaheadOptions);
+											settings = ams.executeFunctionByName(data.amsTypeaheadInitCallback, input, settings) || settings;
+											var plugin = input.typeahead(settings);
+											ams.executeFunctionByName(data.amsTypeaheadAfterInitCallback, input, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * JQuery validation plug-in
+			 */
+			validate: function(element) {
+				var forms = $('FORM:not([novalidate])', element);
+				if (forms.length > 0) {
+					ams.ajax.check($.fn.validate,
+								   ams.baseURL + 'ext/jquery-validate-1.11.1' + (ams.devmode ? '.js' : '.min.js'),
+								   function(first_load) {
+										if (first_load) {
+											$.validator.setDefaults({
+												highlight: function(element) {
+													$(element).closest('.form-group, label:not(:parents(.form-group))').addClass('state-error');
+												},
+												unhighlight: function(element) {
+													$(element).closest('.form-group, label:not(:parents(.form-group))').removeClass('state-error');
+												},
+												errorElement: 'span',
+												errorClass: 'state-error',
+												errorPlacement: function(error, element) {
+													if (element.parent('label').length)
+														error.insertAfter(element.parent());
+													else
+														error.insertAfter(element);
+												}
+											});
+											if (ams.plugins.i18n) {
+												for (var key in ams.plugins.i18n.validate) {
+													var message = ams.plugins.i18n.validate[key];
+													if ((typeof(message) == 'string') &&
+														(message.indexOf('{0}') > -1))
+														ams.plugins.i18n.validate[key] = $.validator.format(message);
+												}
+												$.extend($.validator.messages, ams.plugins.i18n.validate);
+											}
+										}
+										forms.each(function() {
+											var form = $(this);
+											var data = form.data();
+											var data_options = {
+												ignore: null,
+												submitHandler: form.attr('data-async') !== undefined
+															   ? data.amsFormSubmitHandler === undefined
+																	? function() {
+																		// JQuery-form plug-in must be loaded synchronously!!
+																		// Otherwise, hidden input fields created by jquery-validate plug-in
+																		// and matching named buttons will be deleted (on first form submit)
+																		// before JQuery-form plug-in can get them when submitting the form...
+																		ams.ajax.check($.fn.ajaxSubmit,
+																					   ams.baseURL + 'ext/jquery-form-3.49' + (ams.devmode ? '.js' : '.min.js'));
+																		return ams.form.submit(form);
+																	}
+																	: ams.getFunctionByName(data.amsFormSubmitHandler)
+															   : undefined
+											};
+											var settings = $.extend({}, data_options, data.amsValidateOptions);
+											settings = ams.executeFunctionByName(data.amsValidateInitCallback, form, settings) || settings;
+											var plugin = form.validate(settings);
+											ams.executeFunctionByName(data.amsValidateAfterInitCallback, form, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * JQuery dataTables
+			 */
+			datatable: function(element) {
+				var tables = $('.datatable', element);
+				if (tables.length > 0) {
+					ams.ajax.check($.fn.dataTable,
+								   ams.baseURL + 'ext/jquery-dataTables-1.9.4' + (ams.devmode ? '.js' : '.min.js'),
+								   function(first_load) {
+										if (first_load) {
+											$.fn.dataTableExt.oSort['numeric-comma-asc']  = function(a, b) {
+												var x = a.replace(/,/, ".").replace(/ /g, '');
+												var y = b.replace(/,/, ".").replace(/ /g, '');
+												x = parseFloat(x);
+												y = parseFloat(y);
+												return ((x < y) ? -1 : ((x > y) ?  1 : 0));
+											};
+											$.fn.dataTableExt.oSort['numeric-comma-desc'] = function(a, b) {
+												var x = a.replace(/,/, ".").replace(/ /g, '');
+												var y = b.replace(/,/, ".").replace(/ /g, '');
+												x = parseFloat(x);
+												y = parseFloat(y);
+												return ((x < y) ?  1 : ((x > y) ? -1 : 0));
+											};
+										}
+										$(tables).each(function() {
+											ams.ajax.check($.fn.dataTableExt.oPagination['bootstrap_full'],
+														   ams.baseURL + 'myams-dataTables' + (ams.devmode ? '.js' : '.min.js'));
+											var table = $(this);
+											var data = table.data();
+											var extensions = (data.amsDatatableExtensions || '').split(/\s+/);
+											var sDom = data.amsDatatableSdom ||
+												"W" +
+												((extensions.indexOf('colreorder') >= 0 ||
+												  extensions.indexOf('colreorderwithresize') >= 0) ? 'R' : '') +
+												"<'dt-top-row'" +
+												(extensions.indexOf('colvis') >= 0 ? 'C' : '') +
+												((data.amsDatatablePagination === false ||
+												  data.amsDatatablePaginationSize === false) ? '' : 'L') +
+												(data.amsDatatableGlobalFilter === false ? '' : 'F') +
+												">r<'dt-wrapper't" +
+												(extensions.indexOf('scroller') >= 0 ? 'S' : '') +
+												"><'dt-row dt-bottom-row'<'row'<'col-sm-6'" +
+												(data.amsDatatableInformation === false ? '': 'i') +
+												"><'col-sm-6 text-right'p>>";
+											var data_options = {
+												bJQueryUI: false,
+												bFilter: data.amsDatatableGlobalFilter !== false,
+												bPaginate: data.amsDatatablePagination !== false,
+												bInfo: data.amsDatatableInfo !== false,
+												bSort: data.amsDatatableSort !== false,
+												bDeferRender: true,
+												bAutoWidth: false,
+												iDisplayLength: data.amsDatatableDisplayLength || 25,
+												sPaginationType: data.amsDatatablePaginationType || 'bootstrap_full',
+												sDom: sDom,
+												oLanguage: ams.plugins.i18n.datatables,
+												fnInitComplete: function(oSettings, json) {
+													$('.ColVis_Button').addClass('btn btn-default btn-sm')
+																	   .html((ams.plugins.i18n.datatables.sColumns || "Columns") + ' <i class="fa fa-fw fa-caret-down"></i>');
+												}
+											};
+											var settings = $.extend({}, data_options, data.amsDatatableOptions);
+											var index;
+											if (extensions.length > 0) {
+												for (index in extensions) {
+													switch (extensions[index]) {
+														case 'autofill':
+															ams.ajax.check($.fn.dataTable.AutoFill,
+																		   ams.baseURL + 'ext/jquery-dataTables-autoFill' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'columnfilter':
+															ams.ajax.check($.fn.columnFilter,
+																		   ams.baseURL + 'ext/jquery-dataTables-columnFilter' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'colreorder':
+															ams.ajax.check($.fn.dataTable.ColReorder,
+																		   ams.baseURL + 'ext/jquery-dataTables-colReorder' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'colreorderwithresize':
+															ams.ajax.check($.fn.dataTable.ColReorder,
+																		   ams.baseURL + 'ext/jquery-dataTables-colReorderWithResize' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'colvis':
+															ams.ajax.check($.fn.dataTable.ColVis,
+																		   ams.baseURL + 'ext/jquery-dataTables-colVis' + (ams.devmode ? '.js' : '.min.js'));
+															var cv_default = {
+																activate: 'click',
+																sAlign: 'right'
+															};
+															settings.oColVis = $.extend({}, cv_default, data.amsDatatableColvisOptions);
+															break;
+														case 'editable':
+															ams.ajax.check($.fn.editable,
+																		   ams.baseURL + 'ext/jquery-jeditable' + (ams.devmode ? '.js' : '.min.js'));
+															ams.ajax.check($.fn.makeEditable,
+																		   ams.baseURL + 'ext/jquery-dataTables-editable' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'fixedcolumns':
+															ams.ajax.check($.fn.dataTable.FixedColumns,
+																		   ams.baseURL + 'ext/jquery-dataTables-fixedColumns' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'fixedheader':
+															ams.ajax.check($.fn.dataTable.FixedHeader,
+																		   ams.baseURL + 'ext/jquery-dataTables-fixedHeader' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'keytable':
+															ams.ajax.check(window.KeyTable,
+																		   ams.baseURL + 'ext/jquery-dataTables-keyTable' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'rowgrouping':
+															ams.ajax.check($.fn.rowGrouping,
+																		   ams.baseURL + 'ext/jquery-dataTables-rowGrouping' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'rowreordering':
+															ams.ajax.check($.fn.rowReordering,
+																		   ams.baseURL + 'ext/jquery-dataTables-rowReordering' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'scroller':
+															ams.ajax.check($.fn.dataTable.Scroller,
+																		   ams.baseURL + 'ext/jquery-dataTables-scroller' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														default:
+															break;
+													}
+												}
+											}
+											settings = ams.executeFunctionByName(data.amsDatatableInitCallback, table, settings) || settings;
+											var plugin = table.dataTable(settings);
+											ams.executeFunctionByName(data.amsDatatableAfterInitCallback, table, plugin, settings);
+											if (extensions.length > 0) {
+												for (index in extensions) {
+													switch (extensions[index]) {
+														case 'autofill':
+															var af_settings = $.extend({}, data.amsDatatableAutofillOptions, settings.autofill);
+															af_settings = ams.executeFunctionByName(data.amsDatatableAutofillInitCallback, table, af_settings) || af_settings;
+															table.data('ams-autofill', data.amsDatatableAutofillConstructor === undefined
+																						? new $.fn.dataTable.AutoFill(table, af_settings)
+																						: ams.executeFunctionByName(data.amsDatatableAutofillConstructor, table, plugin, af_settings));
+															break;
+														case 'columnfilter':
+															var cf_default = {
+																sPlaceHolder: 'head:after'
+															};
+															var cf_settings = $.extend({}, cf_default, data.amsDatatableColumnfilterOptions, settings.columnfilter);
+															cf_settings = ams.executeFunctionByName(data.amsDatatableColumnfilterInitCallback, table, cf_settings) || cf_settings;
+															table.data('ams-columnfilter', data.amsDatatableColumnfilterConstructor === undefined
+																						? plugin.columnFilter(cf_settings)
+																						: ams.executeFunctionByName(data.amsDatatableColumnfilterConstructor, table, plugin, cf_settings));
+															break;
+														case 'editable':
+															var ed_settings = $.extend({}, data.amsDatatableEditableOptions, settings.editable);
+															ed_settings = ams.executeFunctionByName(data.amsDatatableEditableInitCallback, table, ed_settings) || ed_settings;
+															table.data('ams-editable', data.amsDatatableEditableConstructor === undefined
+																						? table.makeEditable(ed_settings)
+																						: ams.executeFunctionByName(data.amsDatatableEditableConstructor, table, plugin, ed_settings));
+															break;
+														case 'fixedcolumns':
+															var fc_settings = $.extend({}, data.amsDatatableFixedcolumnsOptions, settings.fixedcolumns);
+															fc_settings = ams.executeFunctionByName(data.amsDatatableFixedcolumnsInitCallback, table, fc_settings) || fc_settings;
+															table.data('ams-fixedcolumns', data.amsDatatableFixedcolumnsConstructor === undefined
+																						? new $.fn.dataTable.FixedColumns(table, fc_settings)
+																						: ams.executeFunctionByName(data.amsDatatableFixedcolumnsConstructor, table, plugin, fc_settings));
+															break;
+														case 'fixedheader':
+															var fh_settings = $.extend({}, data.amsDatatableFixedheaderOptions, settings.fixedheader);
+															fh_settings = ams.executeFunctionByName(data.amsDatatableFixedheadeInitCallback, table, fh_settings) || fh_settings;
+															table.data('ams-fixedheader', data.amsDatatableFixedheaderConstructor === undefined
+																						? new $.fn.dataTable.FixedHeader(table, fh_settings)
+																						: ams.executeFunctionByName(data.amsDatatableFixedheaderConstructor, table, plugin, fh_settings));
+															break;
+														case 'keytable':
+															var kt_default = {
+																table: table.get(0),
+																datatable: plugin
+															};
+															var kt_settings = $.extend({}, kt_default, data.amsDatatableKeytableOptions, settings.keytable);
+															kt_settings = ams.executeFunctionByName(data.amsDatatableKeytableInitCallback, table, kt_settings) || kt_settings;
+															table.data('ams-keytable', data.amsDatatableKeytableConstructor === undefined
+																						? new KeyTable(kt_settings)
+																						: ams.executeFunctionByName(data.amsDatatableKeytableConstructor, table, plugin, kt_settings));
+															break;
+														case 'rowgrouping':
+															var rg_settings = $.extend({}, data.amsDatatableRowgroupingOptions, settings.rowgrouping);
+															rg_settings = ams.executeFunctionByName(data.amsDatatableRowgroupingInitCallback, table, rg_settings) || rg_settings;
+															table.data('ams-rowgrouping', data.amsDatatableRowgroupingConstructor === undefined
+																						? table.rowGrouping(rg_settings)
+																						: ams.executeFunctionByName(data.amsDatatableRowgroupingConstructor, table, plugin, rg_settings));
+															break;
+														case 'rowreordering':
+															var rr_settings = $.extend({}, data.amsDatatableRowreorderingOptions, settings.rowreordering);
+															rr_settings = ams.executeFunctionByName(data.amsDatatableRowreorderingInitCallback, table, rr_settings) || rr_settings;
+															table.data('ams-rowreordering', data.amsDatatableRowreorderingConstructor === undefined
+																						? table.rowReordering(rr_settings)
+																						: ams.executeFunctionByName(data.amsDatatableRowreorderingConstructor, table, plugin, rr_settings));
+															break;
+														default:
+															break;
+													}
+												}
+											}
+											var finalizers = (data.amsDatatableFinalizeCallback || '').split(/\s+/);
+											if (finalizers.length > 0) {
+												for (index in finalizers) {
+													ams.executeFunctionByName(finalizers[index], table, plugin, settings);
+												}
+											}
+										});
+								   });
+				}
+			},
+
+			/**
+			 * TableDND plug-in
+			 */
+			tablednd: function(element) {
+				var tables = $('.table-dnd', element);
+				if (tables.length > 0) {
+					ams.ajax.check($.fn.tableDnD,
+								   ams.baseURL + 'ext/jquery-tablednd' + (ams.devmode ? '.js' : '.min.js'),
+								   function(first_load) {
+										tables.each(function() {
+											var table = $(this);
+											$('tbody tr', table).hover(function() {
+												$(this.cells[0]).addClass('drag-handle');
+											}, function() {
+												$(this.cells[0]).removeClass('drag-handle');
+											});
+											var data = table.data();
+											var data_options = {
+												onDragClass: data.amsTabledndDragClass || 'dragging-row',
+												onDragStart: data.amsTabledndDragStart,
+												dragHandle: data.amsTabledndDragHandle,
+												scrollAmount: data.amsTabledndScrollAmount,
+												onAllowDrop: data.amsTabledndAllowDrop,
+												onDrop: data.amsTabledndDrop || function(dnd_table, row) {
+													var target = data.amsTabledndDropTarget;
+													if (target) {
+														var rows = [];
+														$(dnd_table.rows).each(function() {
+															var row_id = $(this).data('ams-element-name');
+															if (row_id)
+																rows.push(row_id);
+														});
+														var local_target = ams.getFunctionByName(target);
+														if (typeof(local_target) == 'function') {
+															local_target.call(table, dnd_table, rows);
+														} else {
+															ams.ajax.post(target, {names: rows});
+														}
+													}
+												}
+											};
+											var settings = $.extend({}, data_options, data.amsTabledndOptions);
+											settings = ams.executeFunctionByName(data.amsTabledndInitCallback, table, settings) || settings;
+											var plugin = table.tableDnD(settings);
+											ams.executeFunctionByName(data.amsTabledndAfterInitCallback, table, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * FancyBox plug-in
+			 */
+			fancybox: function(element) {
+				var fancyboxes = $('.fancybox', element);
+				if (fancyboxes.length > 0) {
+					ams.ajax.check($.fn.fancybox,
+								   ams.baseURL + 'ext/jquery-fancybox-2.1.5' + (ams.devmode ? '.js' : '.min.js'),
+								   function(first_load) {
+										if (first_load)
+											ams.getCSS(ams.baseURL + '../css/ext/jquery-fancybox-2.1.5' + (ams.devmode ? '.css' : '.min.css'));
+										fancyboxes.each(function() {
+											var fancybox = $(this);
+											var data = fancybox.data();
+											var helpers = (data.amsFancyboxHelpers || '').split(/\s+/);
+											if (helpers.length > 0) {
+												for (var index in helpers) {
+													var helper = helpers[index];
+													switch (helper) {
+														case 'buttons':
+															ams.ajax.check($.fancybox.helpers.buttons,
+																		   ams.baseURL + 'ext/fancybox-helpers/fancybox-buttons.js' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'thumbs':
+															ams.ajax.check($.fancybox.helpers.thumbs,
+																		   ams.baseURL + 'ext/fancybox-helpers/fancybox-thumbs.js' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														case 'media':
+															ams.ajax.check($.fancybox.helpers.media,
+																		   ams.baseURL + 'ext/fancybox-helpers/fancybox-media.js' + (ams.devmode ? '.js' : '.min.js'));
+															break;
+														default:
+															break;
+													}
+												}
+											}
+											var data_options = {
+												type: data.amsFancyboxType,
+												padding: data.amsFancyboxPadding || 10,
+												margin: data.amsFancyboxMargin || 10,
+												beforeLoad: ams.getFunctionByName(data.amsFancyboxBeforeLoad) || function() {
+													this.title = ams.executeFunctionByName(data.amsFancyboxTitleGetter, this) || $(this.element).attr('original-title') || $(this.element).attr('title');
+												},
+												helpers: {
+													title: {
+														type: 'inside'
+													}
+												}
+											};
+											var settings = $.extend({}, data_options, data.amsFancyboxOptions);
+											settings = ams.executeFunctionByName(data.amsFancyboxInitCallback, fancybox, settings) || settings;
+											var plugin = fancybox.fancybox(settings);
+											ams.executeFunctionByName(data.amsFancyboxAfterInitCallback, fancybox, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * Image area select plug-in
+			 */
+			imgareaselect: function(element) {
+				var images = $('.imgareaselect', element);
+				if (images.length > 0) {
+					ams.ajax.check($.fn.imgAreaSelect,
+								   ams.baseURL + 'ext/jquery-imgareaselect-0.9.10' + (ams.devmode ? '.js' : '.min.js'),
+								   function(first_load) {
+										if (first_load)
+											ams.getCSS(ams.baseURL + '../css/ext/jquery-imgareaselect' + (ams.devmode ? '.css' : '.min.css'));
+										images.each(function() {
+											var image = $(this);
+											var data = image.data();
+											var parent = data.amsImgareaselectParent ? image.parents(data.amsImgareaselectParent) : 'body';
+											var data_options = {
+												instance: true,
+												handles: true,
+												parent: parent,
+												x1: data.amsImgareaselectX1 || 0,
+												y1: data.amsImgareaselectY1 || 0,
+												x2: data.amsImgareaselectX2 || data.amsImgareaselectImageWidth,
+												y2: data.amsImgareaselectY2 || data.amsImgareaselectImageHeight,
+												imageWidth: data.amsImgareaselectImageWidth,
+												imageHeight: data.amsImgareaselectImageHeight,
+												minWidth: 128,
+												minHeight: 128,
+												aspectRatio: data.amsImgareaselectRatio,
+												onSelectEnd: ams.getFunctionByName(data.amsImgareaselectSelectEnd) || function(img, selection) {
+													var target = data.amsImgareaselectTargetField || 'image_';
+													$('input[name="' + target + 'x1"]', parent).val(selection.x1);
+													$('input[name="' + target + 'y1"]', parent).val(selection.y1);
+													$('input[name="' + target + 'x2"]', parent).val(selection.x2);
+													$('input[name="' + target + 'y2"]', parent).val(selection.y2);
+												}
+											};
+											var settings = $.extend({}, data_options, data.amsImgareaselectOptions);
+											settings = ams.executeFunctionByName(data.amsImgareaselectInitCallback, image, settings) || settings;
+											var plugin = image.imgAreaSelect(settings);
+											ams.executeFunctionByName(data.amsImgareaselectAfterInitCallback, image, plugin, settings);
+											// Add update timeout when plug-in is displayed into a modal dialog
+											setTimeout(function() {
+												plugin.update();
+											}, 250);
+										});
+								   })
+				}
+			},
+
+			/**
+			 * Sparkline graphs
+			 */
+			graphs: function(element) {
+				var graphs = $('.sparkline', element);
+				if (graphs.length > 0) {
+					ams.ajax.check(ams.graphs,
+								   ams.baseURL + 'myams-graphs' + (ams.devmode ? '.js' : '.min.js'),
+								   function() {
+										ams.graphs.init(graphs);
+								   });
+				}
+			},
+
+			/**
+			 * Custom scrollbars
+			 */
+			scrollbars: function(element) {
+				var scrollbars = $('.scrollbar', element);
+				if (scrollbars.length > 0) {
+					ams.ajax.check($.event.special.mousewheel,
+								   ams.baseURL + 'ext/jquery-mousewheel.min.js',
+								   function() {
+										ams.ajax.check($.fn.mCustomScrollbar,
+													   ams.baseURL + 'ext/jquery-mCustomScrollbar' + (ams.devmode ? '.js' : '.min.js'),
+													   function(first_load) {
+															if (first_load)
+																ams.getCSS(ams.baseURL + '../css/ext/jquery-mCustomScrollbar.css',
+																		  'jquery-mCustomScrollbar');
+															scrollbars.each(function() {
+																var scrollbar = $(this);
+																var data = scrollbar.data();
+																var data_options = {
+																	theme: data.amsScrollbarTheme || 'light'
+																};
+																var settings = $.extend({}, data_options, data.amsScrollbarOptions);
+																settings = ams.executeFunctionByName(data.amsScrollbarInitCallback, scrollbar, settings) || settings;
+																var plugin = scrollbar.mCustomScrollbar(settings);
+																ams.executeFunctionByName(data.amsScrollbarAfterInitCallback, scrollbar, plugin, settings);
+															});
+													   });
+									});
+				}
+			}
+		}
+	};
+
+
+	/**
+	 * Callbacks management features
+	 */
+	MyAMS.callbacks = {
+
+		/**
+		 * Initialize list of callbacks
+		 *
+		 * Callbacks are initialized each time a page content is loaded and integrated into page's DOM.
+		 * Unlike plug-ins, callbacks are called once in current's content context but are not kept into
+		 * browser's memory for future use.
+		 * Callbacks are defined via several data attributes:
+		 * - data-ams-callback: name of function callback
+		 * - data-ams-callback-source: source URL of file containing callback's function; can contain variables names
+		 *   if enclosed between braces
+		 * - data-ams-callback-options: JSON object containing callback options
+		 */
+		init: function(element) {
+			$('[data-ams-callback]', element).each(function() {
+				var self = this;
+				var data = $(self).data();
+				var callback = ams.getFunctionByName(data.amsCallback);
+				if (callback === undefined) {
+					if (data.amsCallbackSource) {
+						ams.getScript(data.amsCallbackSource,
+									  function() {
+										ams.executeFunctionByName(data.amsCallback, self, data.amsCallbackOptions);
+									  });
+					} else if (window.console) {
+						console.warn("Undefined callback: " + data.amsCallback);
+					}
+				} else {
+					callback.call(self, data.amsCallbackOptions);
+				}
+			})
+		},
+
+		/**
+		 * Standard alert message callback
+		 *
+		 * An alert is an HTML div included on top of a "parent's" body
+		 * Alert options include:
+		 * - a status: 'info', 'warning', 'error' or 'success'
+		 * - a parent: jQuery selector of parent's element
+		 * - a header: alert's title
+		 * - a subtitle
+		 * - a message body
+		 * - a boolean margin marker; if true, a 10 pixels margin will be added to alert's body
+		 */
+		alert: function(options) {
+			var data = $(this).data();
+			var settings = $.extend({}, options, data.amsAlertOptions);
+			var parent = $(data.amsAlertParent || settings.parent || this);
+			var status = data.amsAlertStatus || settings.status || 'info';
+			var header = data.amsAlertHeader || settings.header;
+			var message = data.amsAlertMessage || settings.message;
+			var subtitle = data.amsAlertSubtitle || settings.subtitle;
+			var margin = data.amsAlertMargin === undefined ? (settings.margin === undefined ? false : settings.margin) : data.amsAlertMargin;
+			ams.skin.alert(parent, status, header, message, subtitle, margin);
+		},
+
+		/**
+		 * Standard message box callback
+		 *
+		 * Message boxes are small informations messages displayed on bottom right page's corner
+		 * Message box options include:
+		 * - data-ams-messagebox-status: determines message box color; given as 'info', 'warning', 'error' or 'success'
+		 * - data-ams-messagebox-title: message's title
+		 * - data-ams-messagebox-content: message's HTML content
+		 * - data-ams-messagebox-icon: if given, CSS class of message's icon
+		 * - data-ams-messagebox-number: if given, a small error/message number displayed below message
+		 * - data-ams-messagebox-timeout: if given, the message box will be automatically hidden passed this number
+		 *   of milliseconds
+		 * - data-ams-messagebox-callback: a callback's name, which will be called when message box is closed
+		 */
+		messageBox: function(options) {
+			var data = $(this).data();
+			var data_options = $.extend({}, options, data.amsMessageboxOptions);
+			var settings = $.extend({}, data_options, {
+				title: data.amsMessageboxTitle || data_options.title || '',
+				content: data.amsMessageboxContent || data_options.content || '',
+				icon: data.amsMessageboxIcon || data_options.icon,
+				number: data.amsMessageboxNumber || data_options.number,
+				timeout: data.amsMessageboxTimeout || data_options.timeout
+			});
+			var status = data.amsMessageboxStatus || data_options.status || 'info';
+			var callback = ams.getFunctionByName(data.amsMessageboxCallback || data_options.callback);
+			ams.skin.messageBox(status, settings, callback);
+		},
+
+		/**
+		 * Standard small box callback
+		 *
+		 * Small boxes are notification messages displayed on top right page's corner.
+		 * Small box options include:
+		 * - data-ams-smallbox-status: determines message box color; given as 'info', 'warning', 'error' or 'success'
+		 * - data-ams-smallbox-title: message's title
+		 * - data-ams-smallbox-content: message's HTML content
+		 * - data-ams-smallbox-icon: if given, CSS class of message's icon
+		 * - data-ams-smallbox-icon-small: if given, CSS class of small message's icon
+		 * - data-ams-smallbox-timeout: if given, the message box will be automatically hidden passed this number
+		 *   of milliseconds
+		 * - data-ams-smallbox-callback: a callback's name, which will be called when message box is closed
+		 */
+		smallBox: function(options) {
+			var data = $(this).data();
+			var data_options = $.extend({}, options, data.amsSmallboxOptions);
+			var settings = $.extend({}, data_options, {
+				title: data.amsSmallboxTitle || data_options.title || '',
+				content: data.amsSmallboxContent || data_options.content || '',
+				icon: data.amsSmallboxIcon || data_options.icon,
+				iconSmall: data.amsSmallboxIconSmall || data_options.iconSmall,
+				timeout: data.amsSmallboxTimeout || data_options.timeout
+			});
+			var status = data.amsSmallboxStatus || data_options.status || 'info';
+			var callback = ams.getFunctionByName(data.amsSmallboxCallback || data_options.callback);
+			ams.skin.smallBox(status, settings, callback);
+		}
+	};
+
+
+	/**
+	 * Events management
+	 */
+	MyAMS.events = {
+
+		/**
+		 * Initialize events listeners
+		 *
+		 * "data-ams-events-handlers" is a data attribute containing a JSON object where:
+		 *  - each key is an event name
+		 *  - value is a callback name.
+		 * For example: data-ams-events-handlers='{"change": "MyAPP.events.changeListener"}'
+		 */
+		init: function(element) {
+			$('[data-ams-events-handlers]', element).each(function() {
+				var element = $(this);
+				var handlers = element.data('ams-events-handlers');
+				for (var event in handlers) {
+					element.on(event, ams.getFunctionByName(handlers[event]));
+				}
+			});
+		}
+	};
+
+
+	/**
+	 * Generic skin features
+	 */
+	MyAMS.skin = {
+
+		/**
+		 * Compute navigation page height
+		 */
+		_setPageHeight: function() {
+			var main_height = $('#main').height();
+			var menu_height = ams.left_panel.height();
+			var window_height = $(window).height() - ams.navbar_height;
+			if (main_height > window_height) {
+				ams.left_panel.css('min-height', main_height);
+				ams.root.css('min-height', main_height + ams.navbar_height);
+			} else {
+				ams.left_panel.css('min-height', window_height);
+				ams.root.css('min-height', window_height);
+			}
+		},
+
+		/**
+		 * Check width for mobile devices
+		 */
+		_checkMobileWidth: function() {
+			if ($(window).width() < 979)
+				ams.root.addClass('mobile-view-activated')
+			else if (ams.root.hasClass('mobile-view-activated'))
+				ams.root.removeClass('mobile-view-activated');
+		},
+
+		/**
+		 * Show/hide shortcut buttons
+		 */
+		_showShortcutButtons: function() {
+			ams.shortcuts.animate({
+				height: 'show'
+			}, 200, 'easeOutCirc');
+			ams.root.addClass('shortcut-on');
+		},
+		
+		_hideShortcutButtons: function() {
+			ams.shortcuts.animate({
+				height: 'hide'
+			}, 300, 'easeOutCirc');
+			ams.root.removeClass('shortcut-on');
+		},
+
+		/**
+		 * Check notification badge
+		 */
+		checkNotification: function() {
+			$this = $('#activity > .badge');
+			if (parseInt($this.text()) > 0)
+				$this.removeClass("hidden")
+					 .addClass("bg-color-red bounceIn animated");
+			else
+				$this.addClass("hidden")
+					 .removeClass("bg-color-red bounceIn animated");
+		},
+
+		/**
+		 * Initialize desktop and mobile widgets
+		 */
+		_initDesktopWidgets: function(element) {
+			if (ams.enable_widgets) {
+				var widgets = $('.ams-widget', element);
+				if (widgets.length > 0)
+					ams.ajax.check($.fn.MyAMSWidget,
+								   ams.baseURL + 'myams-widgets' + (ams.devmode ? '.js' : '.min.js'),
+								   function() {
+										widgets.each(function() {
+											var widget = $(this);
+											var data = widget.data();
+											var data_options = {
+												deleteSettingsKey: '#deletesettingskey-options',
+												deletePositionKey: '#deletepositionkey-options'
+											};
+											var settings = $.extend({}, data_options, data.amsWidgetOptions);
+											settings = ams.executeFunctionByName(data.amsWidgetInitcallback, widget, settings) || settings;
+											widget.MyAMSWidget(settings);
+										});
+										MyAMSWidget.initWidgetsGrid($('.ams-widget-grid', element));
+									});
+			}
+		},
+
+		_initMobileWidgets: function(element) {
+			if (ams.enable_mobile && ams.enable_widgets)
+				ams.skin._initDesktopWidgets(element);
+		},
+
+		/**
+		 * Add an alert on top of a container
+		 *
+		 * @parent: parent container where the alert will be displayed
+		 * @status: info, success, warning or danger
+		 * @header: alert header
+		 * @message: main alert message
+		 * @subtitle: optional subtitle
+		 * @margin: if true, a margin will be displayed around alert
+		 */
+		alert: function(parent, status, header, message, subtitle, margin) {
+			$('.alert', parent).remove();
+			if (status == 'error')
+				status = 'danger';
+			var content = '<div class="' + (margin ? 'margin-10' : '') + ' alert alert-block alert-' + status + ' fade in">' +
+							'<a class="close" data-dismiss="alert"><i class="fa fa-check"></i></a>' +
+							'<h4 class="alert-heading">' +
+								'<i class="fa fa-fw fa-warning"></i> ' + header +
+							'</h4>' +
+							(subtitle ? ('<p>' + subtitle + '</p>') : '');
+			if (typeof(message) == 'string')
+				content += '<ul><li>' + message + '</li></ul>';
+			else if (message) {
+				content += '<ul>';
+				for (var index in message) {
+					if (!$.isNumeric(index))  // IE check
+						continue;
+					content += '<li>' + message[index] + '</li>';
+				};
+				content += '</ul>';
+			}
+			content += '</div>';
+			var alert = $(content).prependTo(parent);
+			if (parent.exists) {
+				ams.ajax.check($.scrollTo,
+							   ams.baseURL + 'ext/jquery-scrollTo.min.js',
+							   function() {
+									$.scrollTo(parent, {offset: {top: -50}});
+							   });
+			}
+		},
+
+		/**
+		 * Big message box
+		 */
+		bigBox: function(options, callback) {
+			ams.ajax.check(ams.notify,
+						   ams.baseURL + 'myams-notify' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+								ams.notify.messageBox(options, callback);
+						   });
+		},
+
+		/**
+		 * Medium notification message box, displayed on page's bottom right
+		 */
+		messageBox: function(status, options, callback) {
+			if (typeof(status) == 'object') {
+				callback = options;
+				options = status || {};
+				status = 'info';
+			}
+			ams.ajax.check(ams.notify,
+						   ams.baseURL + 'myams-notify' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+								switch (status) {
+									case 'error':
+									case 'danger':
+										options.color = '#C46A69';
+										break;
+									case 'warning':
+										options.color = '#C79121';
+										break;
+									case 'success':
+										options.color = '#739E73';
+										break;
+									default:
+										options.color = options.color || '#3276B1';
+								}
+								options.sound = false;
+								ams.notify.bigBox(options, callback);
+						   });
+		},
+
+		/**
+		 * Small notification message box, displayed on page's top right
+		 */
+		smallBox: function(status, options, callback) {
+			if (typeof(status) == 'object') {
+				callback = options;
+				options = status || {};
+				status = 'info';
+			}
+			ams.ajax.check(ams.notify,
+						   ams.baseURL + 'myams-notify' + (ams.devmode ? '.js' : '.min.js'),
+						   function() {
+								switch (status) {
+									case 'error':
+									case 'danger':
+										options.color = '#C46A69';
+										break;
+									case 'warning':
+										options.color = '#C79121';
+										break;
+									case 'success':
+										options.color = '#739E73';
+										break;
+									default:
+										options.color = options.color || '#3276B1';
+								}
+								options.sound = false;
+								ams.notify.smallBox(options, callback);
+						   });
+		},
+
+		/**
+		 * Initialize breadcrumbs based on active menu position
+		 */
+		_drawBreadCrumb: function() {
+			var crumb = $('#ribbon OL.breadcrumb');
+			crumb.empty()
+				 .append($('<li></li>').append($('<a></a>').text(ams.i18n.HOME)
+				 										   .attr('href', $('nav a[href!="#"]:first').attr('href'))));
+			$('nav LI.active >A').each(function() {
+				var menu = $(this);
+				var body = $.trim(menu.clone()
+									  .children(".badge")
+									  .remove()
+									  .end()
+									  .text());
+				var item = $("<li></li>").append(menu.attr('href').replace(/^#/, '')
+													? $("<a></a>").html(body)
+																  .attr('href', menu.attr('href'))
+													: body);
+				crumb.append(item);
+			});
+		},
+
+		/**
+		 * Check URL matching current location hash
+		 */
+		checkURL: function() {
+
+			function updateActiveMenus(menu) {
+				$('nav .active').removeClass('active');
+				menu.addClass('open')
+					.addClass('active');
+				menu.parents('li').addClass('open active')
+								  .children('ul').addClass('active')
+												 .show();
+				menu.parents('li:first').removeClass('open');
+				menu.parents('ul').addClass(menu.attr('href').replace(/^#/, '') ? 'active' : '')
+								  .show();
+			}
+
+			var hash = location.hash;
+			var url = hash.replace(/^#/, '');
+			if (url) {
+				var container = $('#content');
+				if (!container.exists())
+					container = $('body');
+				var menu = $('nav A[href="' + hash + '"]');
+				if (menu.exists())
+					updateActiveMenus(menu);
+				ams.skin.loadURL(url, container);
+				document.title = $('[data-ams-page-title]:first', container).data('ams-page-title') ||
+								 menu.attr('title') ||
+								 document.title;
+			} else {
+				var active_url = $('[data-ams-active-menu]').data('ams-active-menu');
+				if (active_url) {
+					menu = $('nav A[href="' + active_url + '"]');
+				} else {
+					menu = $('nav >UL >LI >A[href!="#"]').first();
+				}
+				if (menu.exists()) {
+					updateActiveMenus(menu);
+					if (active_url)
+						ams.skin._drawBreadCrumb();
+					else
+						window.location.hash = menu.attr('href');
+				}
+			}
+		},
+
+		/**
+		 * Load given URL into container
+		 */
+		loadURL: function(url, container, options, callback) {
+			if (url.startsWith('#')) {
+				url = url.substr(1);
+			}
+			if (typeof(options) == 'function') {
+				callback = options;
+				options = {};
+			}
+			container = $(container);
+			var defaults = {
+				type: 'GET',
+				url: url,
+				dataType: 'html',
+				cache: false,
+				beforeSend: function() {
+					container.html('<h1><i class="fa fa-cog fa-spin"></i> Loading... </h1>');
+					if (container[0] == $('#content')[0]) {
+						ams.skin._drawBreadCrumb();
+						document.title = $('.breadcrumb LI:last-child').text();
+						$('html, body').animate({scrollTop: 0}, 'fast');
+					} else {
+						container.animate({scrollTop: 0}, 'fast');
+					}
+				},
+				success: function(data, status, request) {
+					if (callback)
+						ams.executeFunctionByName(callback, this, data, status, request, options);
+					else {
+						var request_data = ams.ajax.getResponse(request);
+						var data_type = request_data.content_type;
+						var result = request_data.data;
+						switch (data_type) {
+							case 'json':
+								ams.ajax.handleJSON(result, container);
+								break;
+							case 'script':
+								break;
+							case 'xml':
+								break;
+							case 'html':
+							case 'text':
+							default:
+								container.parents('.hidden').removeClass('hidden');
+								$('.alert', container.parents('.alerts-container')).remove();
+								container.css({opacity: '0.0'})
+										 .html(data)
+										 .delay(50)
+										 .animate({opacity: '1.0'}, 300);
+								ams.initContent(container);
+						}
+					}
+				},
+				error: function(request, options, error) {
+					container.html('<h3 class="error"><i class="fa fa-warning txt-color-orangeDark"></i> ' +
+								   ams.i18n.ERROR + error + '</h3>' +
+								   request.responseText);
+				},
+				async: false
+			};
+			var settings = $.extend({}, defaults, options);
+			$.ajax(settings);
+		},
+
+		/**
+		 * Change user language
+		 */
+		setLanguage: function(options) {
+			var lang = options.lang;
+			var handler_type = options.handler_type || 'json';
+			switch (handler_type) {
+				case 'json':
+					var method = options.method || 'setUserLanguage';
+					ams.jsonrpc.post(method, {lang: lang}, function() {
+						window.location.reload(true);
+					});
+					break;
+				case 'ajax':
+					var href = options.href || 'setUserLanguage';
+					ams.ajax.post(href, {lang: lang}, function() {
+						window.location.reload(true);
+					});
+					break;
+			}
+		},
+
+		/**
+		 * Go to logout page
+		 */
+		logout: function() {
+			window.location = ams.loginURL;
+		}
+	};
+
+
+	/**
+	 * Main page initialization
+	 * This code is called only once to register global events and callbacks
+	 */
+	MyAMS.initPage = function() {
+
+		var body = $('body');
+
+		/* Init main components */
+		ams.root = body;
+		ams.left_panel = $('#left-panel');
+		ams.shortcuts = $('#shortcut');
+		ams.plugins.initData(body);
+
+		// Init main AJAX events
+		var jquery_xhr = $.ajaxSettings.xhr;
+		$.ajaxSetup({
+			progress: ams.ajax.progress,
+			progressUpload: ams.ajax.progress,
+			xhr: function() {
+				var request = jquery_xhr();
+				if (request && (typeof(request.addEventListener) == "function")) {
+					var that = this;
+					request.addEventListener("progress", function(evt) {
+						that.progress(evt);
+					}, false);
+				}
+				return request;
+			}
+		});
+		$(document).ajaxError(ams.error.ajax);
+
+		// Check mobile/desktop
+		if (!ams.isMobile) {
+			ams.root.addClass('desktop-detected');
+			ams.device = 'desktop';
+		} else {
+			ams.root.addClass('mobile-detected');
+			ams.device = 'mobile';
+			if (ams.enable_fastclick) {
+				ams.ajax.check($.fn.noClickDelay,
+							   ams.baseURL + '/ext/jquery-smartclick' + (ams.devmode ? '.js' : '.min.js'),
+							   function() {
+								   $('NAV UL A').noClickDelay();
+								   $('#hide-menu A').noClickDelay();
+							   });
+			}
+		}
+
+		// Hide menu button
+		$('#hide-menu >:first-child > A').click(function(e) {
+			body.toggleClass("hidden-menu");
+			e.preventDefault();
+		});
+
+		// Switch shortcuts
+		$('#show-shortcut').click(function(e) {
+			if (ams.shortcuts.is(":visible")) {
+				ams.skin._hideShortcutButtons();
+			} else {
+				ams.skin._showShortcutButtons();
+			}
+			e.preventDefault();
+		});
+
+		$(document).mouseup(function(e) {
+			if (!ams.shortcuts.is(e.target)
+				&& ams.shortcuts.has(e.target).length === 0) {
+				ams.skin._hideShortcutButtons();
+			}
+		});
+
+		// Show & hide mobile search field
+		$('#search-mobile').click(function() {
+			ams.root.addClass('search-mobile');
+		});
+
+		$('#cancel-search-js').click(function() {
+			ams.root.removeClass('search-mobile');
+		});
+
+		// Activity badge
+		$('#activity').click(function(e) {
+			var activity = $(this);
+			var dropdown = activity.next('.ajax-dropdown');
+			if (!dropdown.is(':visible')) {
+				dropdown.css('left', activity.position().left - dropdown.innerWidth() / 2 + activity.innerWidth() / 2)
+						.fadeIn(150);
+				activity.addClass('active');
+			} else {
+				dropdown.fadeOut(150);
+				activity.removeClass('active')
+			}
+			e.preventDefault();
+		});
+		ams.skin.checkNotification();
+
+		$(document).mouseup(function(e) {
+			var dropdown = $('.ajax-dropdown');
+			if (!dropdown.is(e.target) &&
+				dropdown.has(e.target).length === 0) {
+				dropdown.fadeOut(150)
+						.prev().removeClass("active");
+			}
+		});
+
+		$('input[name="activity"]').change(function() {
+			var url = $(this).data('ams-url');
+			container = $('.ajax-notifications');
+			ams.skin.loadURL(url, container);
+		});
+
+		// Logout button
+		$('#logout a').click(function(e) {
+			e.preventDefault();
+			e.stopPropagation();
+			//get the link
+			ams.loginURL = $(this).attr('href');
+			// ask verification
+			ams.skin.bigBox({
+				title : "<i class='fa fa-sign-out txt-color-orangeDark'></i> " + ams.i18n.LOGOUT +
+						" <span class='txt-color-orangeDark'><strong>" + $('#show-shortcut').text() + "</strong></span> ?",
+				content : ams.i18n.LOGOUT_COMMENT,
+				buttons : '['+ams.i18n.BTN_NO+']['+ams.i18n.BTN_YES+']'
+			}, function(ButtonPressed) {
+				if (ButtonPressed == ams.i18n.BTN_YES) {
+					ams.root.addClass('animated fadeOutUp');
+					setTimeout(ams.skin.logout, 1000)
+				}
+			});
+		});
+
+		// Initialize left nav
+		$('NAV UL').myams_menu({
+			accordion : true,
+			speed : ams.menu_speed
+		});
+
+		// Left navigation collapser
+		$('.minifyme').click(function(e) {
+			$('BODY').toggleClass("minified");
+			$(this).effect("highlight", {}, 500);
+			e.preventDefault();
+		});
+
+		// Reset widgets
+		$('#refresh').click(function(e) {
+			ams.skin.bigBox({
+				title: "<i class='fa fa-refresh' style='color: green'></i> " + ams.i18n.CLEAR_STORAGE_TITLE,
+				content: ams.i18n.CLEAR_STORAGE_CONTENT,
+				buttons: '['+ams.i18n.BTN_CANCEL+']['+ams.i18n.BTN_OK+']'
+			}, function(buttonPressed) {
+				if (buttonPressed == ams.i18n.BTN_OK && localStorage) {
+					localStorage.clear();
+					location.reload();
+				}
+			});
+			e.preventDefault();
+		});
+
+		// Check active pop-overs
+		body.on('click', function(e) {
+			var element = $(this);
+			if (!element.is(e.target) &&
+				element.has(e.target).length === 0 &&
+				$('.popover').has(e.target).length === 0)
+				element.popover('hide');
+		});
+
+		// Resize events
+		ams.ajax.check($.resize,
+					   ams.baseURL + 'ext/jquery-resize' + (ams.devmode ? '.js' : '.min.js'),
+					   function() {
+						   $('#main').resize(function() {
+							   ams.skin._setPageHeight();
+							   ams.skin._checkMobileWidth();
+						   });
+						   $('nav').resize(function() {
+							   ams.skin._setPageHeight();
+						   });
+					   });
+
+		// Init AJAX navigation
+		if (ams.ajax_nav) {
+			if ($('nav').length > 0)
+				ams.skin.checkURL();
+			$(document).on('click', 'a[href="#"]', function(e) {
+				e.preventDefault();
+			});
+			$(document).on('click', 'a[href!="#"]:not([data-toggle]), [data-ams-url]:not([data-toggle])', function(e) {
+				var link = $(e.currentTarget);
+				var href = link.attr('href') || link.data('ams-url');
+				if (!href || href.startsWith('javascript:') || link.attr('target'))
+					return;
+				e.preventDefault();
+				var href_getter = ams.getFunctionByName(href);
+				if (typeof(href_getter) == 'function') {
+					href = href_getter.call(link);
+				}
+				// Convert %23 chars to #
+				href = href.replace(/\%23/, '#');
+				var target = link.data('ams-target');
+				if (target) {
+					ams.form.confirmChangedForm(target, function() {
+						ams.skin.loadURL(href, target, link.data('ams-link-options'), link.data('ams-link-callback'));
+						e.stopPropagation();
+					});
+				} else {
+					ams.form.confirmChangedForm(function() {
+						if (href.startsWith('#')) {
+							if (href != location.hash) {
+								if (ams.root.hasClass('mobile-view-activated')) {
+									ams.root.removeClass('hidden-menu');
+									window.setTimeout(function () {
+										window.location.hash = href;
+									}, 150);
+								} else
+									window.location.hash = href;
+							}
+						} else
+							window.location = href;
+					});
+				}
+			});
+			$(document).on('click', 'a[target="_blank"]', function(e) {
+				e.preventDefault();
+				window.open($(e.currentTarget).attr('href'));
+			});
+			$(document).on('click', 'a[target="_top"]', function(e) {
+				e.preventDefault();
+				ams.form.confirmChangedForm(function() {
+					window.location = $(e.currentTarget).attr('href');
+				});
+			});
+
+			// Check URL when hash changed
+			$(window).on('hashchange', ams.skin.checkURL);
+		}
+
+		// Initialize modal dialogs links
+		$(document).off('click.modal')
+				   .on('click', '[data-toggle="modal"]', function(e) {
+			e.preventDefault();
+			var source = $(this);
+			ams.dialog.open(source);
+			if (source.parents('#shortcut').exists())
+				setTimeout(ams.skin._hideShortcutButtons, 300);
+		});
+		$(document).on('shown.bs.modal', ams.dialog.shown);
+
+		// Initialize form buttons
+		$(document).on('click', 'button[type="submit"], button.submit', function() {
+			var button = $(this);
+			$(button.get(0).form).data('ams-submit-button', button);
+		});
+
+		// Initialize custom click handlers
+		$(document).on('click', '[data-ams-click-handler]', function(e) {
+			var source = $(this);
+			var data = source.data();
+			if (data.amsClickHandler) {
+				if (data.amsClickStopPropagation === true)
+					e.stopPropagation();
+				if (data.amsClickKeepDefault !== true)
+					e.preventDefault();
+				var callback = ams.getFunctionByName(data.amsClickHandler);
+				if (callback !== undefined)
+					callback.call(source, data.amsClickHandlerOptions);
+			}
+		});
+
+		// Initialize custom change handlers
+		$(document).on('change', '[data-ams-change-handler]', function(e) {
+			var source = $(this);
+			var data = source.data();
+			if (data.amsChangeHandler) {
+				if (data.amsChangeKeepDefault !== true)
+					e.preventDefault();
+				var callback = ams.getFunctionByName(data.amsChangeHandler);
+				if (callback !== undefined)
+					callback.call(source, data.amsChangeHandlerOptions);
+			}
+		});
+
+		// Initialize custom reset handlers
+		$(document).on('reset', '[data-ams-reset-handler]', function(e) {
+			var form = $(this);
+			var data = form.data();
+			if (data.amsResetHandler) {
+				if (data.amsResetKeepDefault !== true)
+					e.preventDefault();
+				var callback = ams.getFunctionByName(data.amsResetHandler);
+				if (callback !== undefined)
+					callback.call(form, data.amsResetHandlerOptions);
+			}
+		});
+
+		// Handle update on file upload placeholder
+		$(document).on('change', 'input[type="file"]', function(e) {
+			e.preventDefault();
+			var input = $(this);
+			var button = input.parent('.button');
+			if (button.exists() && button.parent().hasClass('input-file')) {
+				button.next('input[type="text"]').val(input.val());
+			}
+		});
+
+		// Disable clicks on disabled tabs
+		$("a[data-toggle=tab]", ".nav-tabs").on("click", function(e) {
+			if ($(this).parent('li').hasClass("disabled")) {
+				e.preventDefault();
+				return false;
+			}
+		});
+
+		// Enable tabs dynamic loading
+		$(document).on('show.bs.tab', function(e) {
+			var link = $(e.target);
+			var data = link.data();
+			if (data.amsUrl) {
+				if (data.amsTabLoaded)
+					return;
+				ams.skin.loadURL(data.amsUrl, link.attr('href'));
+				if (data.amsTabLoadOnce)
+					link.data('ams-tab-loaded', true);
+			}
+		});
+
+		// Init plug-ins required by main layout
+		ams.plugins.enabled.hint(document);
+
+		// Init content when not loaded by AJAX request
+		// or when redirecting to authentication page...
+		if ((window.location.hash == '') || (ams.getQueryVar(window.location.href, 'came_from') != false))
+			ams.initContent(document);
+
+		// Add unload event listener to check for modified forms
+		$(window).on('beforeunload', ams.form.checkBeforeUnload);
+
+	};
+
+
+	/**
+	 * Main content plug-ins initializer
+	 * This code is called to initialize plugins, callbacks and events listeners each time an HTML content
+	 * is loaded dynamically from remote server.
+	 */
+	MyAMS.initContent = function(element) {
+
+		// Remove left tips
+		$('.tipsy').remove();
+
+		// Activate tooltips and popovers
+		$("[rel=tooltip]", element).tooltip();
+		$("[rel=popover]", element).popover();
+
+		// Activate popovers with hover states
+		$("[rel=popover-hover]", element).popover({
+			trigger : "hover"
+		});
+
+		// Init registered plug-ins and callbacks
+		ams.plugins.init(element);
+		ams.callbacks.init(element);
+		ams.events.init(element);
+		ams.form.init(element);
+
+		// Initialize widgets
+		if (ams.device === 'desktop')
+			ams.skin._initDesktopWidgets(element);
+		else
+			ams.skin._initMobileWidgets(element);
+		ams.skin._setPageHeight();
+
+	};
+
+
+	/**
+	 * MyAMS locale strings
+	 */
+	MyAMS.i18n = {
+
+		INFO: "Information",
+		WARNING: "!! WARNING !!",
+		ERROR: "ERROR: ",
+
+		WAIT: "Please wait!",
+		FORM_SUBMITTED: "This form was already submitted...",
+		NO_SERVER_RESPONSE: "No response from server!",
+		ERROR_OCCURED: "An error occured!",
+		ERRORS_OCCURED: "Some errors occured!",
+
+		BAD_LOGIN_TITLE: "Bad login!",
+		BAD_LOGIN_MESSAGE: "Your anthentication credentials didn't allow you to open a session; " +
+						   "please check your credentials or contact administrator.",
+
+		CONFIRM: "Confirm",
+		CONFIRM_REMOVE: "Removing this content can't be undone. Do you confirm?",
+
+		CLEAR_STORAGE_TITLE: "Clear Local Storage",
+		CLEAR_STORAGE_CONTENT: "Would you like to RESET all your saved widgets and clear LocalStorage?",
+
+		BTN_OK: "OK",
+		BTN_CANCEL: "Cancel",
+		BTN_YES: "Yes",
+		BTN_NO: "No",
+		BTN_OK_CANCEL: "[OK][Cancel]",
+
+		FORM_CHANGED_WARNING: "Some changes were not saved. These updates will be lost if you leave this page.",
+		NO_UPDATE: "No changes were applied.",
+		DATA_UPDATED: "Data successfully updated.",
+
+		HOME: "Home",
+		LOGOUT: "Logout?",
+		LOGOUT_COMMENT: "You can improve your security further after logging out by closing this opened browser",
+
+		SELECT2_PLURAL: 's',
+		SELECT2_MATCH: "One result is available, press enter to select it.",
+		SELECT2_MATCHES: " results are available, use up and down arrow keys to navigate.",
+		SELECT2_NOMATCHES: "No matches found",
+		SELECT2_SEARCHING: "Searching...",
+		SELECT2_LOADMORE: "Loading more results...",
+		SELECT2_INPUT_TOOSHORT: "Please enter {0} more character{1}",
+		SELECT2_INPUT_TOOLONG: "Please delete {0} character{1}",
+		SELECT2_SELECTION_TOOBIG: "You can only select {0} item{1}",
+		SELECT2_FREETAG_PREFIX: "Free text: ",
+
+		DT_COLUMNS: "Columns"
+
+	};
+
+
+	$(document).ready(function() {
+		$ = jQuery.noConflict();
+		var lang = $('HTML').attr('lang') || $('HTML').attr('xml:lang');
+		if (lang && !lang.startsWith('en'))
+			MyAMS.getScript(MyAMS.baseURL + 'i18n/myams_' + lang.substr(0,2) + '.js', function() {
+				MyAMS.initPage();
+			});
+		else {
+			MyAMS.initPage();
+		}
+	});
+
+})(jQuery);