src/ztfy/myams/resources/js/myams.js
changeset 210 a4497eed4ff7
parent 206 02a40997d8cb
child 213 48092a18c2c0
--- a/src/ztfy/myams/resources/js/myams.js	Mon Jun 04 12:32:06 2018 +0200
+++ b/src/ztfy/myams/resources/js/myams.js	Mon Jun 04 12:32:38 2018 +0200
@@ -10,10 +10,10 @@
  * ©2014-2016 Thierry Florac <tflorac@ulthar.net>
  */
 
+"use strict";
+
 (function($, globals) {
 
-	"use strict";
-
 	var console = globals.console;
 
 	/**
@@ -90,6 +90,13 @@
 
 	/**
 	 * JQuery filter on parents class
+	 * This filter is often combined with ":not()" to select DOM objects which don't have
+	 * parents of a given class.
+	 * For example:
+	 *
+	 *   $('.hint:not(:parents(.nohints))', element);
+	 *
+	 * will select all elements with ".hint" class which don't have a parent with '.nohints' class.
 	 */
 	$.expr[':'].parents = function(obj, index, meta /*, stack*/) {
 		return $(obj).parents(meta[3]).length > 0;
@@ -98,7 +105,7 @@
 
 	/**
 	 * JQuery 'scrollbarWidth' function
-	 * Get width of vertical scrollbar
+	 * Get width of default vertical scrollbar
 	 */
 	if ($.scrollbarWidth === undefined) {
 		$.scrollbarWidth = function() {
@@ -116,16 +123,16 @@
 	 */
 	$.fn.extend({
 
-		/*
+		/**
 		 * Check if current object is empty or not
 		 */
 		exists: function() {
 			return $(this).length > 0;
 		},
 
-		/*
+		/**
 		 * Get object if it supports given CSS class,
-		 * otherwise looks for parents
+		 * otherwise look for parents
 		 */
 		objectOrParentWithClass: function(klass) {
 			if (this.hasClass(klass)) {
@@ -135,7 +142,7 @@
 			}
 		},
 
-		/*
+		/**
 		 * Build an array of attributes of the given selection
 		 */
 		listattr: function(attr) {
@@ -146,7 +153,7 @@
 			return result;
 		},
 
-		/*
+		/**
 		 * CSS style function
 		 * Code from Aram Kocharyan on stackoverflow.com
 		 */
@@ -176,7 +183,7 @@
 			}
 		},
 
-		/*
+		/**
 		 * Remove CSS classes starting with a given prefix
 		 */
 		removeClassPrefix: function (prefix) {
@@ -189,7 +196,7 @@
 			return this;
 		},
 
-		/*
+		/**
 		 * Context menu handler
 		 */
 		contextMenu: function(settings) {
@@ -216,7 +223,7 @@
 					if (e.ctrlKey) {
 						return;
 					}
-					//open menu
+					// open menu
 					$(settings.menuSelector).data("invokedOn", $(e.target))
 											.show()
 											.css({
@@ -235,7 +242,7 @@
 					return false;
 				});
 
-				//make sure menu closes on any click
+				// make sure menu closes on any click
 				$(document).click(function () {
 					$(settings.menuSelector).hide();
 				});
@@ -576,9 +583,9 @@
 	};
 
 	/**
-	 * Get script or CSS file using browser cache
-	 * Script or CSS URLs can include variable names, given between braces, as in
-	 * {MyAMS.baseURL}
+	 * Get target URL matching given source
+	 *
+	 * Given URL can include variable names (with their namespace), given between braces, as in {MyAMS.baseURL}
 	 */
 	MyAMS.getSource = function(url) {
 		return url.replace(/{[^{}]*}/g, function(match) {
@@ -586,6 +593,13 @@
 		});
 	};
 
+	/**
+	 * Script loader function
+	 *
+	 * @param url: script URL
+	 * @param callback: a callback to be called after script loading
+	 * @param options: a set of options to be added to AJAX call
+	 */
 	MyAMS.getScript = function(url, callback, options) {
 		if (typeof(callback) === 'object') {
 			options = callback;
@@ -606,19 +620,44 @@
 		return $.ajax(settings);
 	};
 
-	MyAMS.getCSS = function(url, id) {
+	/**
+	 * CSS file loader function
+	 * Cross-browser code copied from Stoyan Stefanov blog to be able to
+	 * call a callback when CSS is realy loaded.
+	 * See: https://www.phpied.com/when-is-a-stylesheet-really-loaded
+	 *
+	 * @param url: CSS file URL
+	 * @param id: a unique ID given to CSS file
+	 * @param callback: optional callback function to be called when CSS file is loaded. If set, callback is called
+	 *   with a 'first_load' boolean argument to indicate is CSS was already loaded (*false* value) or not (*true*
+	 *   value).
+	 * @param options: callback options
+	 */
+	MyAMS.getCSS = function(url, id, callback, options) {
+		if (callback) {
+			callback = ams.getFunctionByName(callback);
+		}
 		var head = $('HEAD');
-		var css = $('link[data-ams-id="' + id + '"]', head);
-		if (css.length === 0) {
-			var source = ams.getSource(url);
-			if (ams.devmode) {
-				source += '?_=' + new Date().getTime();
-			}
-			$('<link />').attr({rel: 'stylesheet',
-								type: 'text/css',
-								href: source,
-								'data-ams-id': id})
-						 .appendTo(head);
+		var style = $('style[data-ams-id="' + id + '"]', head);
+		if (style.length === 0) {
+			style = $('<style>').attr('data-ams-id', id)
+								.text('@import "' + ams.getSource(url) + '";');
+			if (callback) {
+				var styleInterval = setInterval(function() {
+					try {
+						var _check = style[0].sheet.cssRules;  // Is only populated when file is loaded
+						callback.call(window, true, options);
+						clearInterval(styleInterval);
+					} catch (e) {
+						// CSS is not loaded yet...
+					}
+				}, 10);
+			}
+			style.appendTo(head);
+		} else {
+			if (callback) {
+				callback.call(window, false, options);
+			}
 		}
 	};
 
@@ -628,11 +667,14 @@
 	 */
 	MyAMS.event = {
 
+		/**
+		 * Stop current event propagation
+		 */
 		stop: function(event) {
 			if (!event) {
 				event = window.event;
 			}
-			if (event) {
+			if (event && (typeof(event) !== 'string')) {
 				if (event.stopPropagation) {
 					event.stopPropagation();
 					event.preventDefault();
@@ -650,6 +692,9 @@
 	 */
 	MyAMS.browser = {
 
+		/**
+		 * Get IE version
+		 */
 		getInternetExplorerVersion: function() {
 			var rv = -1;
 			if (navigator.appName === "Microsoft Internet Explorer") {
@@ -662,6 +707,9 @@
 			return rv;
 		},
 
+		/**
+		 * Display alert for old IE version
+		 */
 		checkVersion: function() {
 			var msg = "You're not using Windows Internet Explorer.";
 			var ver = this.getInternetExplorerVersion();
@@ -677,6 +725,9 @@
 			}
 		},
 
+		/**
+		 * Check if IE is in version 8 or lower
+		 */
 		isIE8orlower: function() {
 			var msg = "0";
 			var ver = this.getInternetExplorerVersion();
@@ -691,6 +742,14 @@
 		},
 
 
+		/**
+		 * Copy selection to clipboard
+		 *
+		 * If 'text' argument is provided, given text is copied to clipboard.
+		 * Otherwise, text ou event's source is copied.
+		 * Several methods are tested to do clipboard copy (based on browser features); il copy can't be done,
+		 * a prompt is displayed to allow user to make a manual copy. 
+		 */
 		copyToClipboard: function(text) {
 
 			function doCopy(text) {
@@ -722,7 +781,7 @@
 											  ? ams.i18n.CLIPBOARD_TEXT_COPY_OK
 											  : ams.i18n.CLIPBOARD_CHARACTER_COPY_OK,
 										  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
-										  timeout: 1000
+										  timeout: 3000
 									  });
 				} else if (globals.prompt) {
 					globals.prompt(MyAMS.i18n.CLIPBOARD_COPY, text);
@@ -812,12 +871,12 @@
 		/**
 		 * 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
+		 * @param checker: pointer to a javascript object which will be downloaded in undefined
+		 * @param source: URL of a javascript file containing requested feature
+		 * @param 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
+		 * @param options: callback options
 		 */
 		check: function(checker, source, callback, options) {
 
@@ -1142,8 +1201,7 @@
 				message = result.message;
 				if (typeof(message) === 'string') {
 					if ((status === 'info') || (status === 'success')) {
-						ams.skin.smallBox(status,
-										  {
+						ams.skin.smallBox(status, {
 											  title: message,
 											  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
 											  timeout: 3000
@@ -1160,16 +1218,25 @@
 				}
 			}
 			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});
+				message = result.smallbox;
+				if (typeof(message) === 'string') {
+					ams.skin.smallBox(result.smallbox_status || status, {
+						title: result.smallbox,
+						icon: result.smallbox_icon || 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
+						timeout: result.smallbox_timeout || 3000
+					});
+				} else {
+					ams.skin.smallBox(message.status || status, {
+						title: message.message,
+						icon: message.icon || 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
+						timeout: message.timeout || 3000
+					});
+				}
 			}
 			if (result.messagebox) {
 				message = result.messagebox;
 				if (typeof(message) === 'string') {
-					ams.skin.messageBox('info',
-										{
+					ams.skin.messageBox('info', {
 											title: ams.i18n.ERROR_OCCURED,
 											content: message,
 											timeout: 10000
@@ -1179,12 +1246,13 @@
 					if (messageStatus === 'error' && form && target) {
 						ams.executeFunctionByName(form.data('ams-form-submit-error') || 'MyAMS.form.finalizeSubmitOnError', form, target);
 					}
-					ams.skin.messageBox(messageStatus,
-										{title: message.title || ams.i18n.ERROR_OCCURED,
-										 content: message.content,
-										 icon: message.icon,
-										 number: message.number,
-										 timeout: message.timeout === null ? undefined : (message.timeout || 10000)});
+					ams.skin.messageBox(messageStatus, {
+											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) {
@@ -1197,6 +1265,9 @@
 				}
 				for (index  =0; index < result.events.length; index++) {
 					event = result.events[index];
+					if (event === null) {
+						continue;
+					}
 					if (typeof(event) === 'string') {
 						form.trigger(event, result.events_options);
 					} else {
@@ -2117,14 +2188,16 @@
 							widget = $('[name="' + widgetData.name + ':list"]', form);
 						}
 						if (widget.exists()) {
+							// Update widget state
 							widget.parents('label:first')
 								  .removeClassPrefix('state-')
 								  .addClass('state-error')
 								  .after('<span for="name" class="state-error">' + widgetData.message + '</span>');
-						}
-						// complete form alert message
-						if (widgetData.label) {
-							message.push(widgetData.label + ' : ' + widgetData.message);
+						} else {
+							// complete form alert message
+							if (widgetData.label) {
+								message.push(widgetData.label + ' : ' + widgetData.message);
+							}
 						}
 						// mark parent tab (if any) with error status
 						var tabIndex = widget.parents('.tab-pane').index() + 1;
@@ -2136,7 +2209,7 @@
 						}
 					}
 				}
-				ams.skin.alert($('fieldset:first', form), errors.error_level || 'error', header, message, errors.error_message);
+				ams.skin.alert($('.form-group:first', form), errors.error_level || 'error', header, message, errors.error_message);
 			}
 		}
 	};
@@ -2459,21 +2532,16 @@
 
 		/** 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);
+			if (!(object instanceof Array)) {
+				object = [object];
+			}
+			$(object).each(function() {
+				if (typeof(this) === 'object') {
+					container.append(this.text);
 				} else {
-					container.append(object);
+					container.append(this);
 				}
-			}
+			});
 		},
 
 		/** Select2 'select-all' helper */
@@ -2538,6 +2606,48 @@
 			}
 		},
 
+		/** Select2 helper to automate selection change */
+		select2ChangeHelper: function() {
+			var source = $(this);
+			var data = source.data();
+			var target = $(data.amsSelect2HelperTarget);
+			switch (data.amsSelect2HelperType) {
+				case 'html':
+					target.html('<div class="text-center"><i class="fa fa-2x fa-gear fa-spin"></i></div>');
+					var params = {};
+					params[data.amsSelect2HelperArgument || 'value'] = source.val();
+					$.get(data.amsSelect2HelperUrl, params,
+						ams.getFunctionByName(data.amsSelect2HelperCallback) || function(result) {
+							if (result) {
+								target.html(result);
+								ams.initContent(target);
+							} else {
+								target.empty();
+							}
+						});
+					break;
+				case 'json-rpc':
+					target.html('<div class="text-center"><i class="fa fa-2x fa-gear fa-spin"></i></div>');
+					ams.jsonrpc.post(data.amsSelect2HelperMethod,
+									 {value: source.val()},
+									 {url: data.amsSelect2HelperUrl},
+									 ams.getFunctionByName(data.amsSelect2HelperCallback) || function(result) {
+										if (result.result) {
+											target.html(result.result);
+											ams.initContent(target);
+										} else {
+											target.empty();
+										}
+									 });
+					break;
+				default:
+					var callback = data.amsSelect2HelperCallback;
+					if (callback) {
+						ams.executeFunctionByName(callback, source, data);
+					}
+			}
+		},
+
 		/** Context menu handler */
 		contextMenuHandler: function(target, menu) {
 			var menuData = menu.data();
@@ -2590,6 +2700,23 @@
 		/** Datetimepicker dialog cleaner callback */
 		datetimepickerDialogHiddenCallback: function() {
 			$('.datepicker, .timepicker, .datetimepicker', this).datetimepicker('destroy');
+		},
+
+		/** Set SEO status */
+		setSEOStatus: function() {
+			var input = $(this);
+			var progress = input.siblings('.progress').children('.progress-bar');
+			var length = Math.min(input.val().length, 100);
+			var status = 'success';
+			if (length < 20 || length > 80) {
+				status = 'danger';
+			} else if (length < 40 || length > 66) {
+				status = 'warning';
+			}
+			progress.removeClassPrefix('progress-bar')
+					.addClass('progress-bar')
+					.addClass('progress-bar-' + status)
+					.css('width', length + '%');
 		}
 	};
 
@@ -2881,30 +3008,31 @@
 				if (hints.length > 0) {
 					ams.ajax.check($.fn.tipsy,
 								   ams.baseURL + 'ext/jquery-tipsy' + ams.devext + '.js',
-								   function () {
+								   function() {
 									   ams.getCSS(ams.baseURL + '../css/ext/jquery-tipsy' + ams.devext + '.css',
-												  'jquery-tipsy');
-									   hints.each(function () {
-										   var hint = $(this);
-										   var data = hint.data();
-										   var dataOptions = {
-											   html: data.amsHintHtml,
-											   title: ams.getFunctionByName(data.amsHintTitleGetter) || function () {
-												   var hint = $(this);
-												   var result = hint.attr('original-title') ||
-																hint.attr(data.amsHintTitleAttr || 'title') ||
-																(data.amsHintHtml ? hint.html() : hint.text());
-												   result = result.replace(/\?_="/, '?_=' + new Date().getTime() + '"');
-												   return result;
-											   },
-											   opacity: data.amsHintOpacity || 0.95,
-											   gravity: data.amsHintGravity || 'sw',
-											   offset: data.amsHintOffset || 0
-										   };
-										   var settings = $.extend({}, dataOptions, data.amsHintOptions);
-										   settings = ams.executeFunctionByName(data.amsHintInitCallback, hint, settings) || settings;
-										   var plugin = hint.tipsy(settings);
-										   ams.executeFunctionByName(data.amsHintAfterInitCallback, hint, plugin, settings);
+												  'jquery-tipsy', function() {
+										   hints.each(function () {
+											   var hint = $(this);
+											   var data = hint.data();
+											   var dataOptions = {
+												   html: data.amsHintHtml === undefined ? (hint.attr('title') || '').startsWith('<') : data.amsHintHtml,
+												   title: ams.getFunctionByName(data.amsHintTitleGetter) || function () {
+													   var hint = $(this);
+													   var result = hint.attr('original-title') ||
+																	hint.attr(data.amsHintTitleAttr || 'title') ||
+																	(data.amsHintHtml ? hint.html() : hint.text());
+													   result = result.replace(/\?_="/, '?_=' + new Date().getTime() + '"');
+													   return result;
+												   },
+												   opacity: data.amsHintOpacity || 0.95,
+												   gravity: data.amsHintGravity || 'sw',
+												   offset: data.amsHintOffset || 0
+											   };
+											   var settings = $.extend({}, dataOptions, data.amsHintOptions);
+											   settings = ams.executeFunctionByName(data.amsHintInitCallback, hint, settings) || settings;
+											   var plugin = hint.tipsy(settings);
+											   ams.executeFunctionByName(data.amsHintAfterInitCallback, hint, plugin, settings);
+										   });
 									   });
 								   });
 				}
@@ -3032,6 +3160,7 @@
 									if (isChecked) {
 										if (data.amsCheckerMode === 'disable') {
 											fieldset.removeAttr('disabled');
+											$('.select2', fieldset).removeAttr('disabled');
 										} else {
 											fieldset.removeClass('switched');
 										}
@@ -3043,6 +3172,7 @@
 									} else {
 										if (data.amsCheckerMode === 'disable') {
 											fieldset.prop('disabled', 'disabled');
+											$('.select2', fieldset).attr('disabled', 'disabled');
 										} else {
 											fieldset.addClass('switched');
 										}
@@ -3066,6 +3196,7 @@
 						} else {
 							if (data.amsCheckerMode === 'disable') {
 								fieldset.attr('disabled', 'disabled');
+								$('.select2', fieldset).attr('disabled', 'disabled');
 							} else {
 								fieldset.addClass('switched');
 							}
@@ -3108,7 +3239,9 @@
 						var draggable = $(this);
 						var data = draggable.data();
 						var dataOptions = {
+							cursor: data.amsDraggableCursor || 'move',
 							containment: data.amsDraggableContainment,
+							connectToSortable: data.amsDraggableConnectSortable,
 							helper: ams.getFunctionByName(data.amsDraggableHelper) || data.amsDraggableHelper,
 							start: ams.getFunctionByName(data.amsDraggableStart),
 							stop: ams.getFunctionByName(data.amsDraggableStop)
@@ -3123,6 +3256,27 @@
 			},
 
 			/**
+			 * Droppable plug-in
+			 */
+			droppable: function(element) {
+				var droppables = $('.droppable', element);
+				if (droppables.length > 0) {
+					droppables.each(function() {
+						var droppable = $(this);
+						var data = droppable.data();
+						var dataOptions = {
+							accept: data.amsdroppableAccept,
+							drop: ams.getFunctionByName(data.amsDroppableDrop)
+						};
+						var settings = $.extend({}, dataOptions, data.amsDroppableOptions);
+						settings = ams.executeFunctionByName(data.amsDroppableInitCallback, droppable, settings) || settings;
+						var plugin = droppable.droppable(settings);
+						ams.executeFunctionByName(data.amsDroppableAfterInitCallback, droppable, plugin, settings);
+					});
+				}
+			},
+
+			/**
 			 * Sortable plug-in
 			 */
 			sortable: function(element) {
@@ -3200,17 +3354,85 @@
 			},
 
 			/**
+			 * Treeview plug-in
+			 */
+			treeview: function(element) {
+				var treeviews = $('.treeview', element);
+				if (treeviews.length > 0) {
+					ams.ajax.check($.fn.treview,
+								   ams.baseURL + 'ext/bootstrap-treeview' + ams.devext + '.js',
+								   function() {
+										ams.getCSS(ams.baseURL + '../css/ext/bootstrap-treeview' + ams.devext + '.css',
+												   'bootstrap-treeview',
+												   function() {
+													   treeviews.each(function () {
+														   var treeview = $(this);
+														   var data = treeview.data();
+														   var dataOptions = {
+															   data: data.amsTreeviewData,
+															   levels: data.amsTreeviewLevels,
+															   injectStyle: data.amsTreeviewInjectStyle,
+															   expandIcon: data.amsTreeviewExpandIcon || 'fa fa-fw fa-plus-square-o',
+															   collapseIcon: data.amsTreeviewCollaspeIcon || 'fa fa-fw fa-minus-square-o',
+															   emptyIcon: data.amsTreeviewEmptyIcon || 'fa fa-fw',
+															   nodeIcon: data.amsTreeviewNodeIcon,
+															   selectedIcon: data.amsTreeviewSelectedIcon,
+															   checkedIcon: data.amsTreeviewCheckedIcon || 'fa fa-fw fa-check-square-o',
+															   uncheckedIcon: data.amsTreeviewUncheckedIcon || 'fa fa-fw fa-square-o',
+															   color: data.amsTreeviewColor,
+															   backColor: data.amsTreeviewBackColor,
+															   borderColor: data.amsTreeviewBorderColor,
+															   onHoverColor: data.amsTreeviewHoverColor,
+															   selectedColor: data.amsTreeviewSelectedColor,
+															   selectedBackColor: data.amsTreeviewSelectedBackColor,
+															   unselectableColor: data.amsTreeviewUnselectableColor || 'rgba(1,1,1,0.25)',
+															   unselectableBackColor: data.amsTreeviewUnselectableBackColor || 'rgba(1,1,1,0.25)',
+															   enableLinks: data.amsTreeviewEnableLinks,
+															   highlightSelected: data.amsTreeviewHighlightSelected,
+															   highlightSearchResults: data.amsTreeviewhighlightSearchResults,
+															   showBorder: data.amsTreeviewShowBorder,
+															   showIcon: data.amsTreeviewShowIcon,
+															   showCheckbox: data.amsTreeviewShowCheckbox,
+															   showTags: data.amsTreeviewShowTags,
+															   toggleUnselectable: data.amsTreeviewToggleUnselectable,
+															   multiSelect: data.amsTreeviewMultiSelect,
+															   onNodeChecked: ams.getFunctionByName(data.amsTreeviewNodeChecked),
+															   onNodeCollapsed: ams.getFunctionByName(data.amsTreeviewNodeCollapsed),
+															   onNodeDisabled: ams.getFunctionByName(data.amsTreeviewNodeDisabled),
+															   onNodeEnabled: ams.getFunctionByName(data.amsTreeviewNodeEnabled),
+															   onNodeExpanded: ams.getFunctionByName(data.amsTreeviewNodeExpanded),
+															   onNodeSelected: ams.getFunctionByName(data.amsTreeviewNodeSelected),
+															   onNodeUnchecked: ams.getFunctionByName(data.amsTreeviewNodeUnchecked),
+															   onNodeUnselected: ams.getFunctionByName(data.amsTreeviewNodeUnselected),
+															   onSearchComplete: ams.getFunctionByName(data.amsTreeviewSearchComplete),
+															   onSearchCleared: ams.getFunctionByName(data.amsTreeviewSearchCleared)
+														   };
+														   var settings = $.extend({}, dataOptions, data.amsTreeviewOptions);
+														   settings = ams.executeFunctionByName(data.amsTreeviewInitcallback, treeview, settings) || settings;
+														   var plugin = treeview.treeview(settings);
+														   ams.executeFunctionByName(data.amsTreeviewAfterInitCallback, treeview, 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.devext + '.js',
+								   ams.baseURL + 'ext/jquery-select2-3.5.4' + ams.devext + '.js',
 								   function() {
 										selects.each(function() {
 											var select = $(this);
 											var data = select.data();
+											if (data.select2) {
+												// Already initialized
+												return;
+											}
 											var dataOptions = {
 												placeholder: data.amsSelect2Placeholder,
 												multiple: data.amsSelect2Multiple,
@@ -3459,26 +3681,29 @@
 								   ams.baseURL + 'ext/jquery-datetimepicker' + ams.devext + '.js',
 								   function(first_load) {
 										if (first_load) {
-											ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + ams.devext + '.css', 'jquery-datetimepicker');
 											ams.dialog.registerHideCallback(ams.helpers.datetimepickerDialogHiddenCallback);
 										}
-										datepickers.each(function() {
-											var input = $(this);
-											var data = input.data();
-											var dataOptions = {
-												lang: data.amsDatetimepickerLang || ams.lang,
-												format: data.amsDatetimepickerFormat || 'd/m/y',
-												datepicker: true,
-												dayOfWeekStart: 1,
-												timepicker: false,
-												closeOnDateSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect,
-												weeks: data.amsDatetimepickerWeeks
-											};
-											var settings = $.extend({}, dataOptions, data.amsDatetimepickerOptions);
-											settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
-											var plugin = input.datetimepicker(settings);
-											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
-										});
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + ams.devext + '.css',
+												   'jquery-datetimepicker',
+												   function () {
+													   datepickers.each(function () {
+														   var input = $(this);
+														   var data = input.data();
+														   var dataOptions = {
+															   lang: data.amsDatetimepickerLang || ams.lang,
+															   format: data.amsDatetimepickerFormat || 'd/m/y',
+															   datepicker: true,
+															   dayOfWeekStart: 1,
+															   timepicker: false,
+															   closeOnDateSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect,
+															   weeks: data.amsDatetimepickerWeeks
+														   };
+														   var settings = $.extend({}, dataOptions, data.amsDatetimepickerOptions);
+														   settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
+														   var plugin = input.datetimepicker(settings);
+														   ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+													   });
+												   });
 								   });
 				}
 			},
@@ -3493,27 +3718,30 @@
 								   ams.baseURL + 'ext/jquery-datetimepicker' + ams.devext + '.js',
 								   function(first_load) {
 										if (first_load) {
-											ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + ams.devext + '.css', 'jquery-datetimepicker');
 											ams.dialog.registerHideCallback(ams.helpers.datetimepickerDialogHiddenCallback);
 										}
-										datetimepickers.each(function() {
-											var input = $(this);
-											var data = input.data();
-											var dataOptions = {
-												lang: data.amsDatetimepickerLang || ams.lang,
-												format: data.amsDatetimepickerFormat || 'd/m/y H:i',
-												datepicker: true,
-												dayOfWeekStart: 1,
-												timepicker: true,
-												closeOnDateSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect,
-												closeOnTimeSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect,
-												weeks: data.amsDatetimepickerWeeks
-											};
-											var settings = $.extend({}, dataOptions, data.amsDatetimepickerOptions);
-											settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
-											var plugin = input.datetimepicker(settings);
-											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
-										});
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + ams.devext + '.css',
+												   'jquery-datetimepicker',
+												   function () {
+													   datetimepickers.each(function () {
+														   var input = $(this);
+														   var data = input.data();
+														   var dataOptions = {
+															   lang: data.amsDatetimepickerLang || ams.lang,
+															   format: data.amsDatetimepickerFormat || 'd/m/y H:i',
+															   datepicker: true,
+															   dayOfWeekStart: 1,
+															   timepicker: true,
+															   closeOnDateSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect,
+															   closeOnTimeSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect,
+															   weeks: data.amsDatetimepickerWeeks
+														   };
+														   var settings = $.extend({}, dataOptions, data.amsDatetimepickerOptions);
+														   settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
+														   var plugin = input.datetimepicker(settings);
+														   ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+													   });
+												   });
 								   });
 				}
 			},
@@ -3528,24 +3756,27 @@
 								   ams.baseURL + 'ext/jquery-datetimepicker' + ams.devext + '.js',
 								   function(first_load) {
 										if (first_load) {
-											ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + ams.devext + '.css', 'jquery-datetimepicker');
 											ams.dialog.registerHideCallback(ams.helpers.datetimepickerDialogHiddenCallback);
 										}
-										timepickers.each(function() {
-											var input = $(this);
-											var data = input.data();
-											var dataOptions = {
-												lang: data.amsDatetimepickerLang || ams.lang,
-												format: data.amsDatetimepickerFormat || 'H:i',
-												datepicker: false,
-												timepicker: true,
-												closeOnTimeSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect
-											};
-											var settings = $.extend({}, dataOptions, data.amsDatetimepickerOptions);
-											settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
-											var plugin = input.datetimepicker(settings);
-											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
-										});
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + ams.devext + '.css',
+												   'jquery-datetimepicker',
+												   function() {
+													   timepickers.each(function () {
+														   var input = $(this);
+														   var data = input.data();
+														   var dataOptions = {
+															   lang: data.amsDatetimepickerLang || ams.lang,
+															   format: data.amsDatetimepickerFormat || 'H:i',
+															   datepicker: false,
+															   timepicker: true,
+															   closeOnTimeSelect: data.amsDatetimepickerCloseOnSelect === undefined ? true : data.amsDatetimepickerCloseOnSelect
+														   };
+														   var settings = $.extend({}, dataOptions, data.amsDatetimepickerOptions);
+														   settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
+														   var plugin = input.datetimepicker(settings);
+														   ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+													   });
+												   });
 								   });
 				}
 			},
@@ -3558,21 +3789,52 @@
 				if (colorpickers.length > 0) {
 					ams.ajax.check($.fn.minicolors,
 								   ams.baseURL + 'ext/jquery-minicolors' + ams.devext + '.js',
-								   function(first_load) {
-										if (first_load) {
-											ams.getCSS(ams.baseURL + '../css/ext/jquery-minicolors' + ams.devext + '.css', 'jquery-minicolors');
-										}
-										colorpickers.each(function() {
-											var input = $(this);
-											var data = input.data();
-											var dataOptions = {
-												position: data.amsColorpickerPosition || input.closest('label.input').data('ams-colorpicker-position') || 'bottom left'
-											};
-											var settings = $.extend({}, dataOptions, data.amsColorpickerOptions);
-											settings = ams.executeFunctionByName(data.amsColorpickerInitCallback, input, settings) || settings;
-											var plugin = input.minicolors(settings);
-											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
-										});
+								   function() {
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-minicolors' + ams.devext + '.css',
+												   'jquery-minicolors',
+												   function () {
+													   colorpickers.each(function () {
+														   var input = $(this);
+														   var data = input.data();
+														   var dataOptions = {
+															   position: data.amsColorpickerPosition || input.closest('label.input').data('ams-colorpicker-position') || 'bottom left'
+														   };
+														   var settings = $.extend({}, dataOptions, data.amsColorpickerOptions);
+														   settings = ams.executeFunctionByName(data.amsColorpickerInitCallback, input, settings) || settings;
+														   var plugin = input.minicolors(settings);
+														   ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+													   });
+												   });
+								   });
+				}
+			},
+
+			/**
+			 * Drag & drop upload plug-in
+			 */
+			dndupload: function(element) {
+				var uploads = $('.dndupload', element);
+				if (uploads.length > 0) {
+					ams.ajax.check($.fn.dndupload,
+								   ams.baseURL + 'ext/jquery-dndupload' + ams.devext + '.js',
+								   function() {
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-dndupload' + ams.devext + '.css',
+												   'jquery-dndupload',
+												   function () {
+													   uploads.each(function () {
+														   var upload = $(this);
+														   var data = upload.data();
+														   var dataOptions = {
+															   action: data.amsDnduploadAction || upload.attr('action') || 'upload-files',
+															   fieldname: data.amsDnduploadFieldname || 'files',
+															   autosubmit: data.amsDnduploadAutosubmit
+														   };
+														   var settings = $.extend({}, dataOptions, data.amsDnduploadOptions);
+														   settings = ams.executeFunctionByName(data.amsDnduploadInitCallback, upload, settings) || settings;
+														   var plugin = upload.dndupload(settings);
+														   ams.executeFunctionByName(data.amsDnduploadAfterInitcallback, upload, plugin, settings);
+													   });
+												   });
 								   });
 				}
 			},
@@ -3680,7 +3942,7 @@
 				if (tables.length > 0) {
 					ams.ajax.check($.fn.dataTable,
 								   ams.baseURL + 'ext/jquery-dataTables-1.9.4' + ams.devext + '.js',
-								   function(first_load) {
+								   function() {
 										ams.ajax.check($.fn.dataTableExt.oPagination.bootstrap_full,
 													   ams.baseURL + 'myams-dataTables' + ams.devext + '.js',
 													   function() {
@@ -3724,8 +3986,10 @@
 																   var sortable = sortables[index];
 																   if (sortable !== undefined) {
 																	   column = columns[index] || {};
-																	   column.bSortable = sortable;
+																	   column.bSortable = typeof(sortable) === 'string' ? JSON.parse(sortable) : sortable;
 																	   columns[index] = column;
+																   } else {
+																	   columns[index] = columns[index] || {};
 																   }
 															   }
 															   // Check columns types
@@ -3736,11 +4000,16 @@
 																	   column = columns[index] || {};
 																	   column.sType = sortType;
 																	   columns[index] = column;
+																   } else {
+																	   columns[index] = columns[index] || {};
 																   }
 															   }
 															   // Set options
 															   var dataOptions = {
 																   bJQueryUI: false,
+																   bServerSide: data.amsDatatableServerSide || false,
+																   sAjaxSource: data.amsDatatableServerSide === true ? data.amsDatatableAjaxSource : undefined,
+																   sServerMethod: data.amsDatatableServerSide === true ? 'POST' : undefined,
 																   bFilter: data.amsDatatableGlobalFilter !== false || extensions.indexOf('columnfilter') >= 0,
 																   bPaginate: data.amsDatatablePagination !== false,
 																   bInfo: data.amsDatatableInfo !== false,
@@ -3812,7 +4081,7 @@
 																			   sources.push(ams.baseURL + 'ext/jquery-dataTables-keyTable' + ams.devext + '.js');
 																			   break;
 																		   case 'rowgrouping':
-																			   checkers.push($.fn.rowGrouping());
+																			   checkers.push($.fn.rowGrouping);
 																			   sources.push(ams.baseURL + 'ext/jquery-dataTables-rowGrouping' + ams.devext + '.js');
 																			   break;
 																		   case 'rowreordering':
@@ -3934,7 +4203,7 @@
 				if (tables.length > 0) {
 					ams.ajax.check($.fn.tableDnD,
 								   ams.baseURL + 'ext/jquery-tablednd' + ams.devext + '.js',
-								   function(first_load) {
+								   function() {
 										tables.each(function() {
 											var table = $(this);
 											var data = table.data();
@@ -3958,29 +4227,32 @@
 													if (target) {
 														// Disable row click handler
 														$(row).data('ams-disabled-handlers', 'click');
-														var rows = [];
-														$(dnd_table.rows).each(function() {
-															var rowId = $(this).data('ams-element-name');
-															if (rowId) {
-																rows.push(rowId);
+														try {
+															var rows = [];
+															$(dnd_table.rows).each(function() {
+																var rowId = $(this).data('ams-element-name');
+																if (rowId) {
+																	rows.push(rowId);
+																}
+															});
+															var localTarget = ams.getFunctionByName(target);
+															if (typeof(localTarget) === 'function') {
+																localTarget.call(table, dnd_table, rows);
+															} else {
+																if (!target.startsWith(window.location.protocol)) {
+																	var location = data.amsLocation;
+																	if (location) {
+																		target = location + '/' + target;
+																	}
+																}
+																ams.ajax.post(target, {names: JSON.stringify(rows)});
 															}
-														});
-														var localTarget = ams.getFunctionByName(target);
-														if (typeof(localTarget) === 'function') {
-															localTarget.call(table, dnd_table, rows);
-														} else {
-															if (!target.startsWith(window.location.protocol)) {
-																var location = data.amsLocation;
-																if (location) {
-																	target = location + '/' + target;
-																}
-															}
-															ams.ajax.post(target, {names: JSON.stringify(rows)});
+														} finally {
+															// Restore row click handler
+															setTimeout(function() {
+																$(row).removeData('ams-disabled-handlers');
+															}, 50);
 														}
-														// Restore row click handler
-														setTimeout(function() {
-															$(row).removeData('ams-disabled-handlers');
-														}, 50);
 													}
 													return false;
 												}
@@ -4002,7 +4274,7 @@
 				if (wizards.length > 0) {
 					ams.ajax.check($.fn.bootstrapWizard,
 								   ams.baseURL + 'ext/bootstrap-wizard-1.4.2' + ams.devext + '.js',
-								   function(first_load) {
+								   function() {
 										wizards.each(function() {
 											var wizard = $(this);
 											var data = wizard.data();
@@ -4064,14 +4336,22 @@
 												var dataOptions = {
 													theme: data.amsTinymceTheme || "modern",
 													language: ams.lang,
-													plugins: [
-														"advlist autosave autolink lists link image charmap print preview hr anchor pagebreak",
+													menubar: data.amsTinymceMenubar !== false,
+													statusbar: data.amsTinymceStatusbar !== false,
+													plugins: data.amsTinymcePlugins || [
+														"advlist autosave autolink lists link charmap print preview hr anchor pagebreak",
 														"searchreplace wordcount visualblocks visualchars code fullscreen",
-														"insertdatetime media nonbreaking save table contextmenu directionality",
+														"insertdatetime nonbreaking save table contextmenu directionality",
 														"emoticons paste textcolor colorpicker textpattern autoresize"
 													],
-													toolbar1: data.amsTinymceToolbar1 || "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent",
-													toolbar2: data.amsTinymceToolbar2 || "forecolor backcolor emoticons | charmap link image media | fullscreen preview print | code",
+													toolbar: data.amsTinymceToolbar,
+													toolbar1: data.amsTinymceToolbar1 === false ? false : data.amsTinymceToolbar1 ||
+														"undo redo | styleselect | bold italic | alignleft " +
+														"aligncenter alignright alignjustify | bullist numlist " +
+														"outdent indent",
+													toolbar2: data.amsTinymceToolbar2 === false ? false : data.amsTinymceToolbar2 ||
+														"forecolor backcolor emoticons | charmap link image media | " +
+														"fullscreen preview print | code",
 													content_css: data.amsTinymceContentCss,
 													formats: data.amsTinymceFormats,
 													style_formats: data.amsTinymceStyleFormats,
@@ -4091,6 +4371,9 @@
 												if (data.amsTinymceExternalPlugins) {
 													var names = data.amsTinymceExternalPlugins.split(/\s+/);
 													for (var index in names) {
+														if (!names.hasOwnProperty(index)) {
+															continue;
+														}
 														var pluginSrc = editor.data('ams-tinymce-plugin-' + names[index]);
 														tinymce.PluginManager.load(names[index], ams.getSource(pluginSrc));
 													}
@@ -4124,44 +4407,45 @@
 				if (images.length > 0) {
 					ams.ajax.check($.fn.imgAreaSelect,
 								   ams.baseURL + 'ext/jquery-imgareaselect-0.9.11-rc1' + ams.devext + '.js',
-								   function(first_load) {
-									   if (first_load) {
-										   ams.getCSS(ams.baseURL + '../css/ext/jquery-imgareaselect' + ams.devext + '.css');
-									   }
-									   images.each(function() {
-										   var image = $(this);
-										   var data = image.data();
-										   var parent = data.amsImgareaselectParent ? image.parents(data.amsImgareaselectParent) : 'body';
-										   var dataOptions = {
-											   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({}, dataOptions, 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);
-									   });
+								   function() {
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-imgareaselect' + ams.devext + '.css',
+												  'jquery-imgareaselect',
+												   function() {
+													   images.each(function () {
+														   var image = $(this);
+														   var data = image.data();
+														   var parent = data.amsImgareaselectParent ? image.parents(data.amsImgareaselectParent) : 'body';
+														   var dataOptions = {
+															   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({}, dataOptions, 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);
+													   });
+												   });
 								   });
 				}
 			},
@@ -4174,90 +4458,141 @@
 				if (fancyboxes.length > 0) {
 					ams.ajax.check($.fn.fancybox,
 								   ams.baseURL + 'ext/jquery-fancybox-2.1.5' + ams.devext + '.js',
-								   function(first_load) {
-										if (first_load) {
-											ams.getCSS(ams.baseURL + '../css/ext/jquery-fancybox-2.1.5' + ams.devext + '.css');
-										}
-										fancyboxes.each(function() {
-											var fancybox = $(this);
-											var data = fancybox.data();
-											var elements = fancybox;
-											if (data.amsFancyboxElements) {
-												elements = $(data.amsFancyboxElements, fancybox);
+								   function() {
+										ams.getCSS(ams.baseURL + '../css/ext/jquery-fancybox-2.1.5' + ams.devext + '.css',
+											'jquery-fancybox',
+											function() {
+												fancyboxes.each(function () {
+													var fancybox = $(this);
+													var data = fancybox.data();
+													var elements = fancybox;
+													var index,
+														helper;
+													if (data.amsFancyboxElements) {
+														elements = $(data.amsFancyboxElements, fancybox);
+													}
+													var helpers = (data.amsFancyboxHelpers || '').split(/\s+/);
+													if (helpers.length > 0) {
+														for (index = 0; index < helpers.length; index++) {
+															helper = helpers[index];
+															switch (helper) {
+																case 'buttons':
+																	ams.ajax.check($.fancybox.helpers.buttons,
+																		ams.baseURL + 'ext/fancybox-helpers/fancybox-buttons' + ams.devext + '.js');
+																	break;
+																case 'thumbs':
+																	ams.ajax.check($.fancybox.helpers.thumbs,
+																		ams.baseURL + 'ext/fancybox-helpers/fancybox-thumbs' + ams.devext + '.js');
+																	break;
+																case 'media':
+																	ams.ajax.check($.fancybox.helpers.media,
+																		ams.baseURL + 'ext/fancybox-helpers/fancybox-media' + ams.devext + '.js');
+																	break;
+																default:
+																	break;
+															}
+														}
+													}
+													var dataOptions = {
+														type: data.amsFancyboxType,
+														padding: data.amsFancyboxPadding || 10,
+														margin: data.amsFancyboxMargin || 10,
+														loop: data.amsFancyboxLoop,
+														beforeLoad: ams.getFunctionByName(data.amsFancyboxBeforeLoad) || function () {
+															var title;
+															if (data.amsFancyboxTitleGetter) {
+																title = ams.executeFunctionByName(data.amsFancyboxTitleGetter, this);
+															}
+															if (!title) {
+																var content = $('*:first', this.element);
+																title = content.attr('original-title') || content.attr('title');
+																if (!title) {
+																	title = $(this.element).attr('original-title') || $(this.element).attr('title');
+																}
+															}
+															this.title = title;
+														},
+														afterLoad: ams.getFunctionByName(data.amsFancyboxAfterLoad),
+														helpers: {
+															title: {
+																type: 'inside'
+															}
+														}
+													};
+													if (helpers.length > 0) {
+														for (index = 0; index < helpers.length; index++) {
+															helper = helpers[index];
+															switch (helper) {
+																case 'buttons':
+																	dataOptions.helpers.buttons = {
+																		position: data.amsFancyboxButtonsPosition || 'top'
+																	};
+																	break;
+																case 'thumbs':
+																	dataOptions.helpers.thumbs = {
+																		width: data.amsFancyboxThumbsWidth || 50,
+																		height: data.amsFancyboxThumbsHeight || 50
+																	};
+																	break;
+																case 'media':
+																	dataOptions.helpers.media = true;
+																	break;
+															}
+														}
+													}
+													var settings = $.extend({}, dataOptions, data.amsFancyboxOptions);
+													settings = ams.executeFunctionByName(data.amsFancyboxInitCallback, fancybox, settings) || settings;
+													var plugin = elements.fancybox(settings);
+													ams.executeFunctionByName(data.amsFancyboxAfterInitCallback, fancybox, plugin, settings);
+												});
+											});
+								   });
+				}
+			},
+
+			/**
+			 * Flot charts
+			 */
+			chart: function(element) {
+				var charts = $('.chart', element);
+				if (charts.length > 0) {
+					ams.ajax.check($.fn.plot,
+								   ams.baseURL + 'flot/jquery.flot' + ams.devext + '.js',
+								   function() {
+										charts.each(function() {
+
+											function checkPlugin(plugin) {
+												for (var index in $.plot.plugins) {
+													if ($.plot.plugins.hasOwnProperty(index)) {
+														var pluginInfo = $.plot.plugins[index];
+														if (pluginInfo.name === plugin) {
+															return pluginInfo;
+														}
+													}
+												}
+												return null;
 											}
-											var helpers = (data.amsFancyboxHelpers || '').split(/\s+/);
-											if (helpers.length > 0) {
-												for (var index=0; index < helpers.length; index++) {
-													var helper = helpers[index];
-													switch (helper) {
-														case 'buttons':
-															ams.ajax.check($.fancybox.helpers.buttons,
-																		   ams.baseURL + 'ext/fancybox-helpers/fancybox-buttons' + ams.devext + '.js');
-															break;
-														case 'thumbs':
-															ams.ajax.check($.fancybox.helpers.thumbs,
-																		   ams.baseURL + 'ext/fancybox-helpers/fancybox-thumbs' + ams.devext + '.js');
-															break;
-														case 'media':
-															ams.ajax.check($.fancybox.helpers.media,
-																		   ams.baseURL + 'ext/fancybox-helpers/fancybox-media' + ams.devext + '.js');
-															break;
-														default:
-															break;
+
+											var chart = $(this);
+											var data = chart.data();
+											var dataOptions = {};
+											var plugins = (data.amsChartPlugins || '').split(/\s+/);
+											if (plugins.length > 0) {
+												for (var index in plugins) {
+													if (plugins.hasOwnProperty(index)) {
+														var pluginName = plugins[index];
+														if (!checkPlugin(pluginName)) {
+															ams.getScript(ams.baseURL + 'flot/jquery.flot.' + pluginName + ams.devext + '.js');
+														}
 													}
 												}
 											}
-											var dataOptions = {
-												type: data.amsFancyboxType,
-												padding: data.amsFancyboxPadding || 10,
-												margin: data.amsFancyboxMargin || 10,
-												loop: data.amsFancyboxLoop,
-												beforeLoad: ams.getFunctionByName(data.amsFancyboxBeforeLoad) || function() {
-													var title;
-													if (data.amsFancyboxTitleGetter) {
-														title = ams.executeFunctionByName(data.amsFancyboxTitleGetter, this);
-													}
-													if (!title) {
-														var content = $('*:first', this.element);
-														title = content.attr('original-title') || content.attr('title');
-														if (!title) {
-															title = $(this.element).attr('original-title') || $(this.element).attr('title');
-														}
-													}
-													this.title = title;
-												},
-												afterLoad: ams.getFunctionByName(data.amsFancyboxAfterLoad),
-												helpers: {
-													title: {
-														type: 'inside'
-													}
-												}
-											};
-											if (helpers.length > 0) {
-												for (index = 0; index < helpers.length; index++) {
-													helper = helpers[index];
-													switch (helper) {
-														case 'buttons':
-															dataOptions.helpers.buttons = {
-																position: data.amsFancyboxButtonsPosition || 'top'
-															};
-															break;
-														case 'thumbs':
-															dataOptions.helpers.thumbs = {
-																width: data.amsFancyboxThumbsWidth || 50,
-																height: data.amsFancyboxThumbsHeight || 50
-															};
-															break;
-														case 'media':
-															dataOptions.helpers.media = true;
-															break;
-													}
-												}
-											}
-											var settings = $.extend({}, dataOptions, data.amsFancyboxOptions);
-											settings = ams.executeFunctionByName(data.amsFancyboxInitCallback, fancybox, settings) || settings;
-											var plugin = elements.fancybox(settings);
-											ams.executeFunctionByName(data.amsFancyboxAfterInitCallback, fancybox, plugin, settings);
+											var settings = $.extend({}, dataOptions, data.amsChartOptions);
+											settings = ams.executeFunctionByName(data.amsChartInitCallback, chart, settings) || settings;
+											var chartData = data.amsChartData;
+											chartData = ams.executeFunctionByName(data.amsChartInitData, chart, chartData) || chartData;
+											var plugin = chart.plot(chartData, settings);
+											ams.executeFunctionByName(data.amsChartAfterInitCallback, chart, plugin, settings);
 										});
 								   });
 				}
@@ -4288,22 +4623,22 @@
 								   function() {
 										ams.ajax.check($.fn.mCustomScrollbar,
 													   ams.baseURL + 'ext/jquery-mCustomScrollbar' + ams.devext + '.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 dataOptions = {
-																	theme: data.amsScrollbarTheme || 'light'
-																};
-																var settings = $.extend({}, dataOptions, data.amsScrollbarOptions);
-																settings = ams.executeFunctionByName(data.amsScrollbarInitCallback, scrollbar, settings) || settings;
-																var plugin = scrollbar.mCustomScrollbar(settings);
-																ams.executeFunctionByName(data.amsScrollbarAfterInitCallback, scrollbar, plugin, settings);
-															});
+													   function() {
+															ams.getCSS(ams.baseURL + '../css/ext/jquery-mCustomScrollbar.css',
+																	   'jquery-mCustomScrollbar',
+																	   function () {
+																		   scrollbars.each(function () {
+																			   var scrollbar = $(this);
+																			   var data = scrollbar.data();
+																			   var dataOptions = {
+																				   theme: data.amsScrollbarTheme || 'light'
+																			   };
+																			   var settings = $.extend({}, dataOptions, data.amsScrollbarOptions);
+																			   settings = ams.executeFunctionByName(data.amsScrollbarInitCallback, scrollbar, settings) || settings;
+																			   var plugin = scrollbar.mCustomScrollbar(settings);
+																			   ams.executeFunctionByName(data.amsScrollbarAfterInitCallback, scrollbar, plugin, settings);
+																		   });
+																	   });
 													   });
 									});
 				}
@@ -4483,10 +4818,9 @@
 		/**
 		 * Delete an element from a container table
 		 *
-		 * @param element
 		 * @returns {Function}
 		 */
-		deleteElement: function(element) {
+		deleteElement: function() {
 			return function() {
 				var link = $(this);
 				MyAMS.skin.bigBox({
@@ -4496,12 +4830,15 @@
 					buttons: ams.i18n.BTN_OK_CANCEL
 				}, function(button) {
 					if (button === ams.i18n.BTN_OK) {
-						var table = link.parents('table').first();
-						var location = table.data('ams-location') || '';
 						var tr = link.parents('tr').first();
+						var table = tr.parents('table').first();
+						var location = tr.data('ams-location') || table.data('ams-location') || '';
+						if (location) {
+							location += '/';
+						}
 						var deleteTarget = tr.data('ams-delete-target') || table.data('ams-delete-target') || 'delete-element.json';
 						var objectName = tr.data('ams-element-name');
-						MyAMS.ajax.post(location + '/' + deleteTarget, {'object_name': objectName}, function(result, status) {
+						MyAMS.ajax.post(location + deleteTarget, {'object_name': objectName}, function(result, status) {
 							if (result.status === 'success') {
 								if (table.hasClass('datatable')) {
 									table.dataTable().fnDeleteRow(tr[0]);
@@ -4518,6 +4855,251 @@
 					}
 				});
 			};
+		},
+
+		/**
+		 * Switch element visibility
+		 */
+		switchElementVisibility: function() {
+			return function() {
+				var source = $(this);
+				var element = source.parents('tr').first();
+				var container = element.parents('table');
+				ams.ajax.post(container.data('ams-location') + '/' +
+							  container.data('ams-visibility-switcher'),
+					{object_name: element.data('ams-element-name')},
+					function(result, status) {
+						if (result.visible) {
+							$('i', source).attr('class', 'fa fa-fw fa-eye');
+						} else {
+							$('i', source).attr('class', 'fa fa-fw fa-eye-slash text-danger');
+						}
+					});
+			}
+		}
+	};
+
+
+	/**
+	 * Tree management
+	 */
+	MyAMS.tree = {
+
+		/**
+		 * Open close tree node inside a table
+		 */
+		switchTableNode: function() {
+
+			function removeChildNodes(node_id) {
+				$('tr[data-ams-tree-node-parent-id="' + node_id + '"]').each(function() {
+					var row = $(this);
+					removeChildNodes(row.data('ams-tree-node-id'));
+					row.remove();
+				})
+			}
+
+			var node = $(this);
+			var switcher = $('i.switch', node);
+			var tr = node.parents('tr').first();
+			var table = tr.parents('table').first();
+			if (switcher.hasClass('fa-minus-square-o')) {
+				removeChildNodes(tr.data('ams-tree-node-id'));
+				switcher.removeClass('fa-minus-square-o')
+						.addClass('fa-plus-square-o');
+			} else {
+				var location = tr.data('ams-location') || table.data('ams-location') || '';
+				var treeNodesTarget = tr.data('ams-tree-nodes-target') || table.data('ams-tree-nodes-target') || 'get-tree-nodes.json';
+				var sourceName = tr.data('ams-element-name');
+				switcher.removeClass('fa-plus-square-o')
+						.addClass('fa-cog fa-spin');
+				MyAMS.ajax.post(location + '/' + sourceName + '/' + treeNodesTarget, {
+					can_sort: !$('td.sorter', tr).is(':empty')
+				}, function(result, status) {
+					if (result.length > 0) {
+						var old_row = tr;
+						for (var index = 0; index < result.length; index++) {
+							var new_row = $(result[index]);
+							new_row.insertAfter(old_row)
+								   .addClass('no-drag-handle');
+							ams.initContent(new_row);
+							old_row = new_row;
+						}
+						if (table.hasClass('table-dnd')) {
+							table.tableDnDUpdate();
+						}
+					}
+					switcher.removeClass('fa-cog fa-spin')
+							.addClass('fa-minus-square-o');
+				});
+			}
+		},
+
+		/**
+		 * Open close all tree nodes
+		 */
+		switchTree: function() {
+			var th = $(this);
+			var switcher = $('i.switch', th);
+			var table = $(this).parents('table').first();
+			var tableID = table.data('ams-tree-node-id');
+			if (switcher.hasClass('fa-minus-square-o')) {
+				$('tr[data-ams-tree-node-parent-id]').filter('tr[data-ams-tree-node-parent-id!="' + tableID + '"]').remove();
+				$('i.switch', table).removeClass('fa-minus-square-o')
+									.addClass('fa-plus-square-o');
+			} else {
+				var tr = $('tbody tr', table).first();
+				var location = table.data('ams-location') || '';
+				var target = table.data('ams-tree-nodes-target') || 'get-tree.json';
+				switcher.removeClass('fa-plus-square-o')
+						.addClass('fa-cog fa-spin');
+				MyAMS.ajax.post(location + '/' + target, {
+					can_sort: !$('td.sorter', tr).is(':empty')
+				}, function(result, status) {
+					$('tr[data-ams-tree-node-id]', table).remove();
+					var old_row = null;
+					for (var index = 0; index < result.length; index++) {
+						var new_row = $(result[index]);
+						if (old_row === null) {
+							new_row.appendTo($('tbody', table));
+						} else {
+							new_row.insertAfter(old_row);
+						}
+						new_row.addClass('no-drag-handle');
+						ams.initContent(new_row);
+						old_row = new_row;
+					}
+					if (table.hasClass('table-dnd')) {
+						table.tableDnDUpdate();
+					}
+					$('i.switch', table).removeClass('fa-plus-square-o')
+										.addClass('fa-minus-square-o');
+					switcher.removeClass('fa-cog fa-spin')
+							.addClass('fa-minus-square-o');
+				});
+			}
+		},
+
+		/**
+		 * Sort and re-parent tree elements
+		 */
+		sortTree: function(dnd_table, row) {
+			var data = $(dnd_table).data();
+			var target = data.amsTabledndDropTarget;
+			if (target) {
+				// Disable row click handler
+				row = $(row);
+				row.data('ams-disabled-handlers', 'click');
+				try {
+					// Get root ID
+					var tableID = row.parents('table').first().data('ams-tree-node-id');
+					// Get moved row ID
+					var rowID = row.data('ams-tree-node-id');
+					var rowParentID = row.data('ams-tree-node-parent-id');
+					// Get new parent ID
+					var parent = row.prev('tr');
+					if (parent.exists()) {
+						// Move below an existing row
+						var parentID = parent.data('ams-tree-node-id');
+						// Check switcher state
+						var switcher = $('.switch', parent);
+						if (switcher.hasClass('fa-minus-square-o')) {
+							// Opened folder: move as child
+							if (rowParentID === parentID) {
+								// Don't change parent
+								var action = 'reorder';
+							} else {
+								// Change parent
+								action = 'reparent';
+							}
+						} else {
+							// Closed folder or simple item: move as sibling
+							parentID = parent.data('ams-tree-node-parent-id');
+							if (rowParentID === parentID) {
+								// Don't change parent
+								action = 'reorder';
+							} else {
+								// Change parent
+								action = 'reparent';
+							}
+						}
+					} else {
+						// Move to site root
+						parentID = tableID;
+						switcher = null;
+						if (rowParentID === parentID) {
+							// Already child of site root
+							action = 'reorder';
+						} else {
+							// Move from inner folder to site root
+							action = 'reparent';
+						}
+					}
+					// Call ordering target
+					var localTarget = ams.getFunctionByName(target);
+					if (typeof(localTarget) === 'function') {
+						localTarget.call(table, dnd_table, post_data);
+					} else {
+						if (!target.startsWith(window.location.protocol)) {
+							var location = data.amsLocation;
+							if (location) {
+								target = location + '/' + target;
+							}
+						}
+						var post_data = {
+							action: action,
+							child: rowID,
+							parent: parentID,
+							order: JSON.stringify($('tr[data-ams-tree-node-id]').listattr('data-ams-tree-node-id')),
+							can_sort: !$('td.sorter', row).is(':empty')
+						};
+						ams.ajax.post(target, post_data, function(result) {
+
+							function removeChildRows(rowID) {
+								var childs = $('tr[data-ams-tree-node-parent-id="' + rowID + '"]');
+								childs.each(function() {
+									var childRow = $(this);
+									var childID = childRow.attr('data-ams-tree-node-id');
+									removeChildRows(childID);
+									childRow.remove();
+								});
+							}
+
+							if (result.status) {
+								ams.ajax.handleJSON(result);
+							} else {
+								// Remove moved row childrens
+								var body = $(row).parents('tbody').first();
+								removeChildRows(rowID);
+								if (post_data.action === 'reparent') {
+									// Remove new parent childrens
+									removeChildRows(parentID);
+									row.remove();
+									var old_row = $('tr[data-ams-tree-node-id="' + parentID + '"]');
+									for (var index = 0; index < result.length; index++) {
+										var new_row = $(result[index]);
+										if (old_row.exists()) {
+											new_row.insertAfter(old_row)
+												.addClass('no-drag-handle');
+										} else {
+											new_row.prependTo(body)
+												.addClass('no-drag-handle');
+										}
+										ams.initContent(new_row);
+										old_row = new_row;
+									}
+								}
+								$('tr').parents('table').tableDnDUpdate();
+							}
+						});
+					}
+				} finally {
+					// Restore row click handler
+					setTimeout(function() {
+						$(row).removeData('ams-disabled-handlers');
+					}, 50);
+				}
+			}
+			return false;
 		}
 	};
 
@@ -4595,6 +5177,89 @@
 		},
 
 		/**
+		 * Replace given form with new content
+		 */
+		refreshContent: function(changes) {
+			var target = $('[id="' + changes.object_id + '"]');
+			target.replaceWith($(changes.content));
+			target = $('[id="' + changes.object_id + '"]');
+			MyAMS.initContent(target);
+			return target;
+		},
+
+		/**
+		 * Replace given widget with given content
+		 */
+		refreshWidget: function(changes) {
+			var target = $('[id="' + changes.parent_id + '"]');
+			var widget = $('[name="' + changes.widget_name + '"]', target);
+			if (!widget.exists()) {
+				widget = $('[name="' + changes.widget_name + ':list"]', target);
+			}
+			var label = widget.parents('label.input').last();
+			label.html(changes.content);
+			MyAMS.initContent(label);
+			return label;
+		},
+
+		/**
+		 * Replace given table with new content
+		 */
+		refreshTable: function(changes) {
+			var widget = $('[id="' + changes.object_id + '"]').parents('.ams-widget:first');
+			widget.replaceWith($(changes.table));
+			widget = $('[id="' + changes.object_id + '"]').parents('.ams-widget:first');
+			MyAMS.initContent(widget);
+			return widget;
+		},
+
+		/**
+		 * Replace given table with new content
+		 * If table is located inside a switched fieldset, fieldset is opened
+		 *
+		 * @param changes
+		 */
+		refreshSwitchedTable: function(changes) {
+			var widget = ams.skin.refreshTable(changes);
+			var legend = widget.siblings('legend');
+			if (legend.parents('fieldset:first').hasClass('switched')) {
+				legend.click();
+			}
+		},
+
+		/**
+		 * Replace given row with new content
+		 */
+		refreshRow: function(changes) {
+			var tr = $('tr[id="' + changes.object_id + '"]');
+			var table = tr.parents('table').first();
+			var new_tr = $(changes.row);
+			tr.replaceWith(new_tr);
+			MyAMS.initContent(new_tr);
+			if (table.hasClass('table-dnd')) {
+				new_tr.addClass('no-drag-handle');
+				table.tableDnDUpdate();
+			}
+			return new_tr;
+		},
+
+		/**
+		 * Replace given row cell with new content
+		 */
+		refreshRowCell: function(changes) {
+			var tr = $('tr[id="' + changes.object_id + '"]');
+			var table = tr.parents('table').first();
+			var headRow = $('tr', $('thead', table));
+			var headCell = $('th[data-ams-column-name="' + changes.col_name + '"]', headRow);
+			var index = $('th', headRow).index(headCell);
+			if (index > -1) {
+				var cell = $($('td', tr).get(index));
+				cell.html(changes.cell);
+				MyAMS.initContent(cell);
+			}
+		},
+
+		/**
 		 * Initialize desktop and mobile widgets
 		 */
 		_initDesktopWidgets: function(element) {
@@ -4661,13 +5326,9 @@
 				content += '</ul>';
 			}
 			content += '</div>';
-			var alert = $(content).prependTo(parent);
+			$(content).insertBefore(parent);
 			if (parent.exists) {
-				ams.ajax.check($.scrollTo,
-							   ams.baseURL + 'ext/jquery-scrollTo.min.js',
-							   function() {
-								   $.scrollTo(parent, {offset: {top: -50}});
-							   });
+				ams.skin.scrollTo(parent, {offset: {top: -50}});
 			}
 		},
 
@@ -4745,6 +5406,29 @@
 		},
 
 		/**
+		 * Scroll to given element
+		 *
+		 * @param element: the element to which to scroll
+		 * @param options: scroll options
+		 */
+		scrollTo: function(element, options) {
+			ams.ajax.check($.scrollTo,
+						   ams.baseURL + 'ext/jquery-scrollto-2.1.2' + ams.devext + '.js',
+						   function() {
+								var body = $('body');
+								var offset = options.offset || 0;
+								if (body.hasClass('fixed-header')) {
+									offset -= $('#header').height();
+								}
+								if (body.hasClass('fixed-ribbon')) {
+									offset -= $('#ribbon').height();
+								}
+								options = $.extend({}, options, {offset: offset});
+								$.scrollTo(element, options);
+						   });
+		},
+
+		/**
 		 * Initialize breadcrumbs based on active menu position
 		 */
 		_drawBreadCrumb: function() {
@@ -4934,10 +5618,13 @@
 						ams.stats.logPageview();
 					}
 				},
-				error: function(request, options, error) {
+				error: function(request, errorOptions, error) {
 					container.html('<h3 class="error"><i class="fa fa-warning txt-color-orangeDark"></i> ' +
 								   ams.i18n.ERROR + error + '</h3>' +
 								   request.responseText);
+					if (options && options.afterErrorCallback) {
+						ams.executeFunctionByName(options.afterErrorCallback, this);
+					}
 				},
 				async: options.async === undefined ? true : options.async
 			};
@@ -4948,7 +5635,7 @@
 		/**
 		 * Change user language
 		 */
-		setLanguage: function(options) {
+		setLanguage: function(event, options) {
 			var lang = options.lang;
 			var handlerType = options.handler_type || 'json';
 			switch (handlerType) {
@@ -5067,7 +5754,7 @@
 		}
 
 		// Hide menu button
-		$('#hide-menu >:first-child > A').click(function(e) {
+		$('#hide-menu').find('>:first-child >A').click(function(e) {
 			body.toggleClass("hidden-menu");
 			e.preventDefault();
 		});
@@ -5326,7 +6013,7 @@
 		});
 
 		// Initialize custom click handlers
-		$(document).on('click', '[data-ams-click-handler]', function(e) {
+		$(document).on('click', '[data-ams-click-handler]', function(event) {
 			var source = $(this);
 			var handlers = source.data('ams-disabled-handlers');
 			if ((handlers === true) || (handlers === 'click') || (handlers === 'all')) {
@@ -5335,20 +6022,20 @@
 			var data = source.data();
 			if (data.amsClickHandler) {
 				if ((data.amsStopPropagation === true) || (data.amsClickStopPropagation === true)) {
-					e.stopPropagation();
+					event.stopPropagation();
 				}
 				if (data.amsClickKeepDefault !== true) {
-					e.preventDefault();
+					event.preventDefault();
 				}
 				var callback = ams.getFunctionByName(data.amsClickHandler);
 				if (callback !== undefined) {
-					callback.call(source, data.amsClickHandlerOptions);
+					callback.call(source, event, data.amsClickHandlerOptions);
 				}
 			}
 		});
 
 		// Initialize custom change handlers
-		$(document).on('change', '[data-ams-change-handler]', function(e) {
+		$(document).on('change', '[data-ams-change-handler]', function(event) {
 			var source = $(this);
 			// Disable change handlers for readonly inputs
 			// These change handlers are activated by IE!!!
@@ -5361,16 +6048,26 @@
 			}
 			var data = source.data();
 			if (data.amsChangeHandler) {
+				if ((data.amsStopPropagation === true) || (data.amsChangeStopPropagation === true)) {
+					event.stopPropagation();
+				}
 				if (data.amsChangeKeepDefault !== true) {
-					e.preventDefault();
+					event.preventDefault();
 				}
 				var callback = ams.getFunctionByName(data.amsChangeHandler);
 				if (callback !== undefined) {
-					callback.call(source, data.amsChangeHandlerOptions);
+					callback.call(source, event, data.amsChangeHandlerOptions);
 				}
 			}
 		});
 
+		// Submit form when CTRL+Enter key is pressed in textarea
+		$(document).on('keydown', 'textarea', function(e) {
+			if ((e.keyCode === 10 || e.keyCode === 13) && (e.ctrlKey || e.metaKey)) {
+				$(this).closest('form').submit();
+			}
+		});
+
 		// Notify reset to update Select2 widgets
 		$(document).on('reset', 'form', function(e) {
 			var form = $(this);
@@ -5380,7 +6077,10 @@
 				$('INPUT.select2[type="hidden"]', form).each(function() {
 					var input = $(this);
 					var select = input.data('select2');
-					input.select2('val', input.data('ams-select2-input-value').split(select.opts.separator));
+					var value = input.data('ams-select2-input-value');
+					if (value) {
+						input.select2('val', value.split(select.opts.separator));
+					}
 				});
 				form.find('.select2').trigger('change');
 				$('[data-ams-reset-callback]', form).each(function() {
@@ -5410,6 +6110,12 @@
 			}
 		});
 
+		// Initialize custom event on click
+		$(document).on('click', '[data-ams-click-event]', function(e) {
+			var source = $(this);
+			$(e.target).trigger(source.data('ams-click-event'), source.data('ams-click-event-options'));
+		});
+
 		// Handle update on file upload placeholder
 		$(document).on('change', 'input[type="file"]', function(e) {
 			e.preventDefault();
@@ -5420,6 +6126,11 @@
 			}
 		});
 
+		// Always blur readonly inputs
+		$(document).on('focus', 'input[readonly="readonly"]', function() {
+			$(this).blur();
+		});
+
 		// Prevent bootstrap dialog from blocking TinyMCE focus
 		$(document).on('focusin', function(e) {
 			if ($(e.target).closest('.mce-window').length) {
@@ -5428,32 +6139,52 @@
 		});
 
 		// Disable clicks on disabled tabs
-		$("a[data-toggle=tab]", ".nav-tabs").on("click", function(e) {
+		$(document).on("click", '.nav-tabs a[data-toggle=tab]', function(e) {
 			if ($(this).parent('li').hasClass("disabled")) {
 				e.preventDefault();
 				return false;
 			}
 		});
 
+		// Automatically set orientation of dropdown menus
+		$(document).on('show.bs.dropdown', '.btn-group', function() {
+			var menu = $(this);
+			var ul = menu.children('.dropdown-menu');
+			var menuRect = menu.get(0).getBoundingClientRect();
+			var position = menuRect.top;
+			var buttonHeight = menuRect.height;
+			var menuHeight = ul.outerHeight();
+			if (position > menuHeight && $(window).height() - position < buttonHeight + menuHeight) {
+				menu.addClass("dropup");
+			}
+		}).on('hidden.bs.dropdown', '.btn-group', function() {
+			// always reset after close
+			$(this).removeClass('dropup');
+		});
+
 		// Enable tabs dynamic loading
 		$(document).on('show.bs.tab', function(e) {
 			var link = $(e.target);
+			if (link.exists() && (link.get(0).tagName !== 'A')) {
+				link = $('a[href]', link);
+			}
 			var data = link.data();
 			if (data.amsUrl) {
 				if (data.amsTabLoaded) {
 					return;
 				}
-				try {
-					link.append('<i class="fa fa-spin fa-cog margin-left-5"></i>');
-					ams.skin.loadURL(data.amsUrl, link.attr('href'), {afterLoadCallback: function() {
+				link.append('<i class="fa fa-spin fa-cog margin-left-5"></i>');
+				ams.skin.loadURL(data.amsUrl, link.attr('href'), {
+					afterLoadCallback: function() {
 						if (data.amsTabLoadOnce) {
 							link.data('ams-tab-loaded', true);
 						}
-					}});
-				}
-				finally {
-					$('i', link).remove();
-				}
+						$('i', link).remove();
+					},
+					afterErrorCallback: function() {
+						$('i', link).remove();
+					}
+				});
 			}
 		});
 
@@ -5471,6 +6202,11 @@
 			});
 		});
 
+		// Enable custom MyAMS refresh events
+		$(document).on('myams.refresh', function(event, settings) {
+			MyAMS.executeFunctionByName(settings.handler || MyAMS.skin.refreshContent, event.target, settings);
+		});
+
 		// Init page content
 		ams.initContent(document);
 		if (ams.ajaxNav && nav.exists()) {
@@ -5593,6 +6329,18 @@
 			CLOSE: "Close",
 			NEXT: "Next",
 			PREVIOUS: "Previous"
+		},
+		dndupload: {
+			FILES_SELECTED: '{count} files selected',
+			CHOOSE_FILE: 'Select file(s)',
+			ADD_INFO: 'to add them to current folder,',
+			DRAG_FILE: 'or drag and drop them here!',
+			UPLOAD: 'Upload',
+			UPLOADING: 'Uploading&hellip;',
+			DONE: 'Done!',
+			UPLOAD_MORE: 'Upload more?',
+			ERROR: 'Error!',
+			TRY_AGAIN: 'Try again?'
 		}
 	};
 
@@ -5600,6 +6348,8 @@
 	$(document).ready(function() {
 		$ = jQuery.noConflict();
 		var html = $('HTML');
+		html.removeClass('no-js')
+			.addClass('js');
 		var lang = html.attr('lang') || html.attr('xml:lang');
 		if (lang && !lang.startsWith('en')) {
 			MyAMS.lang = lang;