src/ztfy/myams/resources/js/myams.js
changeset 118 c87d242a1176
parent 104 d25135ad3899
child 127 cf00ea83daa7
--- a/src/ztfy/myams/resources/js/myams.js	Fri Mar 27 15:59:45 2015 +0100
+++ b/src/ztfy/myams/resources/js/myams.js	Fri Mar 27 16:00:30 2015 +0100
@@ -7,7 +7,7 @@
  *
  * Custom administration and application skin tools
  * Released under Zope Public License ZPL 1.1
- * ©2014 Thierry Florac <tflorac@ulthar.net>
+ * ©2014-2015 Thierry Florac <tflorac@ulthar.net>
  */
 
 (function($) {
@@ -323,6 +323,7 @@
 	if (window.MyAMS === undefined) {
 		window.MyAMS = {
 			devmode: true,
+			lang: 'en',
 			throttle_delay: 350,
 			menu_speed: 235,
 			navbar_height: 49,
@@ -343,6 +344,7 @@
 	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);
 	})();
 
@@ -772,7 +774,7 @@
 				case 'reload':
 					if (result.close_form != false)
 						ams.dialog.close(form);
-					url = result.location;
+					url = result.location || window.location.hash;
 					if (url.startsWith('#'))
 						url = url.substr(1);
 					ams.skin.loadURL(url, result.target || target || '#content');
@@ -780,7 +782,7 @@
 				case 'redirect':
 					if (result.close_form == true)
 						ams.dialog.close(form);
-					url = result.location;
+					url = result.location || window.location.href;
 					if (result.window) {
 						window.open(url, result.window, result.options);
 					} else {
@@ -794,8 +796,29 @@
 			if (result.content) {
 				var content = result.content;
 				var container = $(content.target || target || form || '#content');
-				container.html(content.html);
-				ams.initContent(container);
+				if (content.raw == true) {
+					container.text(content.text);
+				} else {
+					container.html(content.html);
+					ams.initContent(container);
+				}
+				if (!content.keep_hidden)
+					container.removeClass('hidden');
+			}
+			if (result.contents) {
+				var contents = result.contents;
+				for (var index in contents) {
+					content = contents[index];
+					container = $(content.target);
+					if (content.raw == true) {
+						container.text(content.text);
+					} else {
+						container.html(content.html);
+						ams.initContent(container);
+					}
+					if (!content.keep_hidden)
+						container.removeClass('hidden');
+				}
 			}
 			if (result.message) {
 				var message = result.message;
@@ -814,6 +837,12 @@
 								   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')
@@ -830,7 +859,7 @@
 										 content: message.content,
 										 icon: message.icon,
 										 number: message.number,
-										 timeout: message.timeout || 10000});
+										 timeout: message.timeout == null ? undefined : (message.timeout || 10000)});
 				}
 			}
 			if (result.event)
@@ -838,7 +867,7 @@
 			if (result.callback)
 				ams.executeFunctionByName(result.callback, form, result.options);
 			if (result.callbacks) {
-				for (var index in result.callbacks) {
+				for (index in result.callbacks) {
 					if (!$.isNumeric(index))
 						continue;
 					var callback = result.callbacks[index];
@@ -1044,12 +1073,14 @@
 			}
 			// Prevent multiple submits of the same form
 			if (form.data('submitted')) {
-				ams.skin.messageBox('warning', {
-					title: ams.i18n.WAIT,
-					content: ams.i18n.FORM_SUBMITTED,
-					icon: 'fa fa-save shake animated',
-					timeout: 5000
-				});
+				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
@@ -1395,6 +1426,7 @@
 				var header = errors.length == 1 ? ams.i18n.ERROR_OCCURED : ams.i18n.ERRORS_OCCURED;
 				ams.skin.alert(form, 'error', header, errors);
 			} else {
+				$('.state-error', form).removeClass('state-error');
 				header = errors.widgets && (errors.widgets.length > 1) ? ams.i18n.ERRORS_OCCURED : ams.i18n.ERROR_OCCURED;
 				var message = new Array();
 				var index;
@@ -1406,13 +1438,24 @@
 				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);
+					// set widget status message
+					var widget_data = errors.widgets[index];
+					var widget = $('[name="' + widget_data.name + '"]', form);
+					widget.parents('label:first')
+						  .removeClassPrefix('state-')
+						  .addClass('state-error')
+						  .after('<span for="name" class="state-error">' + widget_data.message + '</span>');
+					// complete form alert message
+					if (widget_data.label) {
+						message.push(widget_data.label + ' : ' + widget_data.message);
+					}
+					// mark parent tab (if any) with error status
+					var tab_index = widget.parents('.tab-pane').index() + 1;
+					if (tab_index > 0) {
+						var nav_tabs = $('.nav-tabs', $(widget).parents('.tabforms'));
+						$('li:nth-child(' + tab_index + ')', nav_tabs).removeClassPrefix('state-')
+																	  .addClass('state-error');
+						$('li.state-error:first a', form).click();
 					}
 				}
 				ams.skin.alert(form, 'error', header, message, errors.error_message);
@@ -1872,20 +1915,26 @@
 					if (!data.amsChecker) {
 						var checker = $('<label class="checkbox"></label>');
 						var fieldname = data.amsCheckerFieldname || ('checker_'+ams.generateId());
+						var checkbox_id = fieldname.replace(/\./, '_');
 						var prefix = data.amsCheckerHiddenPrefix;
 						var hidden = null;
 						var checkedValue = data.amsCheckerHiddenValueOn || 'true';
 						var uncheckedValue = data.amsCheckerHiddenValueOff || 'false';
+						var marker = data.amsCheckerMarker || false;
 						if (prefix) {
 							hidden = $('<input type="hidden">').attr('name', prefix + fieldname)
 															   .val(data.amsCheckerState == 'on' ? checkedValue : uncheckedValue)
 															   .prependTo(legend);
+						} else if (marker) {
+							$('<input type="hidden">').attr('name', marker)
+													  .attr('value', 1)
+													  .prependTo(legend);
 						}
 						var input = $('<input type="checkbox">').attr('name', fieldname)
-																.attr('id', fieldname.replace(/\./, '_'))
+																.attr('id', checkbox_id)
 																.data('ams-checker-hidden-input', hidden)
 																.data('ams-checker-init', true)
-																.val(true)
+																.val(data.amsCheckerValue || true)
 																.attr('checked', data.amsCheckerState == 'on' ? 'checked' : null);
 						if (data.amsCheckerReadonly) {
 							input.attr('disabled', 'disabled');
@@ -1923,7 +1972,7 @@
 							});
 						}
 						input.appendTo(checker);
-						$('.legend', legend).attr('for', input.attr('id'));
+						$('>label', legend).attr('for', input.attr('id'));
 						checker.append('<i></i>')
 							   .prependTo(legend);
 						var required = $('[required]', fieldset);
@@ -1971,7 +2020,7 @@
 				var selects = $('.select2', element);
 				if (selects.length > 0) {
 					ams.ajax.check($.fn.select2,
-								   ams.baseURL + 'ext/jquery-select2-3.4.5' + (ams.devmode ? '.js' : '.min.js'),
+								   ams.baseURL + 'ext/jquery-select2-3.5.2' + (ams.devmode ? '.js' : '.min.js'),
 								   function() {
 										selects.each(function() {
 											var select = $(this);
@@ -1989,6 +2038,14 @@
 																	? 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;
@@ -2053,7 +2110,11 @@
 													break;
 											}
 
-											if (data.amsSelect2Query) {
+											if (select.attr('readonly')) {
+												data_options.query = function() {
+													return [];
+												};
+											} else if (data.amsSelect2Query) {
 												// Custom query method
 												data_options.query = ams.getFunctionByName(data.amsSelect2Query);
 												data_options.minimumInputLength = data.amsSelect2MinimumInputLength || 1;
@@ -2119,6 +2180,17 @@
 											settings = ams.executeFunctionByName(data.amsSelect2InitCallback, select, settings) || settings;
 											var plugin = select.select2(settings);
 											ams.executeFunctionByName(data.amsSelect2AfterInitCallback, select, plugin, settings);
+											if (select.hasClass('ordered')) {
+												$(plugin.context).parents('.select2-parent').find('ul.select2-choices').sortable({
+													containment: 'parent',
+													start: function() {
+														select.select2('onSortStart');
+													},
+													update: function() {
+														select.select2('onSortEnd');
+													}
+											   })
+											}
 
 											select.on('change', function() {
 												var validator = $(select.get(0).form).data('validator');
@@ -2155,27 +2227,94 @@
 			},
 
 			/**
-			 * JQuery-UI date picker
+			 * JQuery 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);
-					});
+					ams.ajax.check($.fn.datetimepicker,
+								   ams.baseURL + 'ext/jquery-datetimepicker' + (ams.devmode ? '.js': '.min.js'),
+								   function(first_load) {
+										if (first_load) {
+											ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + (ams.devmode ? '.css' : '.min.css'), 'jquery-datetimepicker');
+										}
+										datepickers.each(function() {
+											var input = $(this);
+											var data = input.data();
+											var data_options = {
+												lang: data.amsDatetimepickerLang || ams.lang,
+												format: data.amsDatetimepickerFormat || 'd/m/y',
+												datepicker: true,
+												dayOfWeekStart: 1,
+												timepicker: false
+											};
+											var settings = $.extend({}, data_options, data.amsDatetimepickerOptions);
+											settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
+											var plugin = input.datetimepicker(settings);
+											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * JQuery datetime picker
+			 */
+			datetimepicker: function(element) {
+				var datetimepickers = $('.datetimepicker', element);
+				if (datetimepickers.length > 0) {
+					ams.ajax.check($.fn.datetimepicker,
+								   ams.baseURL + 'ext/jquery-datetimepicker' + (ams.devmode ? '.js': '.min.js'),
+								   function(first_load) {
+										if (first_load) {
+											ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + (ams.devmode ? '.css' : '.min.css'), 'jquery-datetimepicker');
+										}
+										datetimepickers.each(function() {
+											var input = $(this);
+											var data = input.data();
+											var data_options = {
+												lang: data.amsDatetimepickerLang || ams.lang,
+												format: data.amsDatetimepickerFormat || 'd/m/y H:i',
+												datepicker: true,
+												dayOfWeekStart: 1,
+												timepicker: true
+											};
+											var settings = $.extend({}, data_options, data.amsDatetimepickerOptions);
+											settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
+											var plugin = input.datetimepicker(settings);
+											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+										});
+								   });
+				}
+			},
+
+			/**
+			 * JQuery time picker
+			 */
+			timepicker: function(element) {
+				var timepickers = $('.timepicker', element);
+				if (timepickers.length > 0) {
+					ams.ajax.check($.fn.datetimepicker,
+								   ams.baseURL + 'ext/jquery-datetimepicker' + (ams.devmode ? '.js': '.min.js'),
+								   function(first_load) {
+										if (first_load) {
+											ams.getCSS(ams.baseURL + '../css/ext/jquery-datetimepicker' + (ams.devmode ? '.css' : '.min.css'), 'jquery-datetimepicker');
+										}
+										timepickers.each(function() {
+											var input = $(this);
+											var data = input.data();
+											var data_options = {
+												lang: data.amsDatetimepickerLang || ams.lang,
+												format: data.amsDatetimepickerFormat || 'H:i',
+												datepicker: false,
+												timepicker: true
+											};
+											var settings = $.extend({}, data_options, data.amsDatetimepickerOptions);
+											settings = ams.executeFunctionByName(data.amsDatetimepickerInitCallback, input, settings) || settings;
+											var plugin = input.datetimepicker(settings);
+											ams.executeFunctionByName(data.amsDatetimepickerAfterInitCallback, input, plugin, settings);
+										});
+								   });
 				}
 			},
 
@@ -2221,8 +2360,9 @@
 												errorElement: 'span',
 												errorClass: 'state-error',
 												errorPlacement: function(error, element) {
-													if (element.parent('label').length)
-														error.insertAfter(element.parent());
+													var label = element.parents('label:first');
+													if (label.length)
+														error.insertAfter(label);
 													else
 														error.insertAfter(element);
 												}
@@ -2249,12 +2389,31 @@
 																		// 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...
+																		$('.state-error', form).removeClass('state-error');
 																		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
+															   : undefined,
+												invalidHandler: form.attr('data-async') !== undefined
+																? data.amsFormInvalidHandler === undefined
+																	? function(event, validator) {
+																		$('.state-error', form).removeClass('state-error');
+																		for (var index in validator.errorList) {
+																			var error = validator.errorList[index];
+																			var tab_index = $(error.element).parents('.tab-pane').index() + 1;
+																			if (tab_index > 0) {
+																				var nav_tabs = $('.nav-tabs', $(error.element).parents('.tabforms'));
+																				$('li:nth-child(' + tab_index + ')', nav_tabs)
+																						.removeClassPrefix('state-')
+																						.addClass('state-error');
+																				$('li.state-error:first a', nav_tabs).click();
+																			}
+																		}
+																	}
+																	: ams.getFunctionByName(data.amsFormInvalidHandler)
+																: undefined
 											};
 											var settings = $.extend({}, data_options, data.amsValidateOptions);
 											settings = ams.executeFunctionByName(data.amsValidateInitCallback, form, settings) || settings;
@@ -2397,7 +2556,7 @@
 											ams.executeFunctionByName(data.amsDatatableAfterInitCallback, table, plugin, settings);
 											if (extensions.length > 0) {
 												for (index in extensions) {
-													switch(extensions[index]) {
+													switch (extensions[index]) {
 														case 'autofill':
 															var af_settings = $.extend({}, data.amsDatatableAutofillOptions, settings.autofill);
 															af_settings = ams.executeFunctionByName(data.amsDatatableAutofillInitCallback, table, af_settings) || af_settings;
@@ -2478,6 +2637,163 @@
 			},
 
 			/**
+			 * 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);
+											$(table).on('mouseover', 'tr', function() {
+												$(this.cells[0]).addClass('drag-handle');
+											}).on('mouseout', 'tr', 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) {
@@ -2608,7 +2924,7 @@
 				content: data.amsMessageboxContent || data_options.content || '',
 				icon: data.amsMessageboxIcon || data_options.icon,
 				number: data.amsMessageboxNumber || data_options.number,
-				timeout: data.amsMessageboxTimeout || data_options.icon
+				timeout: data.amsMessageboxTimeout || data_options.timeout
 			});
 			var status = data.amsMessageboxStatus || data_options.status || 'info';
 			var callback = ams.getFunctionByName(data.amsMessageboxCallback || data_options.callback);
@@ -2637,7 +2953,7 @@
 				content: data.amsSmallboxContent || data_options.content || '',
 				icon: data.amsSmallboxIcon || data_options.icon,
 				iconSmall: data.amsSmallboxIconSmall || data_options.iconSmall,
-				timeout: data.amsSmallboxTimeout || data_options.icon
+				timeout: data.amsSmallboxTimeout || data_options.timeout
 			});
 			var status = data.amsSmallboxStatus || data_options.status || 'info';
 			var callback = ams.getFunctionByName(data.amsSmallboxCallback || data_options.callback);
@@ -2672,6 +2988,60 @@
 
 
 	/**
+	 * Container management
+	 */
+	MyAMS.container = {
+
+		/**
+		 * Change container elements order
+		 *
+		 * This is a callback which may be used with TableDnD plug-in which allows you to
+		 * change order of table rows.
+		 * Rows order is stored in an hidden input which is defined in table's data attribute
+		 * called 'data-ams-input-name'
+		 */
+		changeOrder: function(table, names) {
+			var input = $('input[name="' + $(this).data('ams-input-name') + '"]', $(this));
+			input.val(names.join(';'));
+		},
+
+		/**
+		 * Delete an element from a container table
+		 *
+		 * @param element
+		 * @returns {Function}
+		 */
+		deleteElement: function(element) {
+			return function() {
+				var link = $(this);
+				MyAMS.skin.bigBox({
+					title: MyAMS.i18n.WARNING,
+					content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; ' + MyAMS.i18n.DELETE_WARNING,
+					buttons: MyAMS.i18n.BTN_OK_CANCEL
+				}, function(button) {
+					if (button == MyAMS.i18n.BTN_OK) {
+						var table = link.parents('table');
+						var location = table.data('ams-location');
+						var tr = link.parents('tr');
+						var delete_target = tr.data('ams-delete-target') || table.data('ams-delete-target') || 'delete-element.json';
+						var object_name = tr.data('ams-element-name');
+						MyAMS.ajax.post(location + '/' + delete_target, {'object_name': object_name}, function(result, status) {
+							if (result.status == 'success') {
+								if (table.hasClass('datatable')) {
+									table.dataTable().fnDeleteRow(tr[0]);
+								} else {
+									tr.remove();
+								}
+							}
+						});
+					}
+				});
+			}
+		}
+	};
+
+
+	/**
 	 * Generic skin features
 	 */
 	MyAMS.skin = {
@@ -3051,10 +3421,13 @@
 	 */
 	MyAMS.initPage = function() {
 
+		var body = $('body');
+
 		/* Init main components */
-		ams.root = $('BODY');
+		ams.root = body;
 		ams.left_panel = $('#left-panel');
 		ams.shortcuts = $('#shortcut');
+		ams.plugins.initData(body);
 
 		// Init main AJAX events
 		var jquery_xhr = $.ajaxSettings.xhr;
@@ -3093,7 +3466,7 @@
 
 		// Hide menu button
 		$('#hide-menu >:first-child > A').click(function(e) {
-			$('BODY').toggleClass("hidden-menu");
+			body.toggleClass("hidden-menu");
 			e.preventDefault();
 		});
 
@@ -3203,7 +3576,7 @@
 		});
 
 		// Check active pop-overs
-		$('BODY').on('click', function(e) {
+		body.on('click', function(e) {
 			var element = $(this);
 			if (!element.is(e.target) &&
 				element.has(e.target).length === 0 &&
@@ -3241,26 +3614,36 @@
 				if (typeof(href_getter) == 'function') {
 					href = href_getter.call(link);
 				}
-				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'));
-					});
+				if (typeof(href) == 'function') {
+					// Javascript function call
+					e.stopPropagation();
+					href.call(link);
 				} 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 () {
+					// Standard AJAX or browser URL call
+					// 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;
-									}, 150);
-								} else
-									window.location.hash = href;
-							}
-						} else
-							window.location = href;
-					});
+								}
+							} else
+								window.location = href;
+						});
+					}
 				}
 			});
 			$(document).on('click', 'a[target="_blank"]', function(e) {
@@ -3281,8 +3664,10 @@
 		// Initialize modal dialogs links
 		$(document).off('click.modal')
 				   .on('click', '[data-toggle="modal"]', function(e) {
+			var source = $(this);
+			if (source.data('ams-stop-propagation') === true)
+				e.stopPropagation();
 			e.preventDefault();
-			var source = $(this);
 			ams.dialog.open(source);
 			if (source.parents('#shortcut').exists())
 				setTimeout(ams.skin._hideShortcutButtons, 300);
@@ -3300,7 +3685,7 @@
 			var source = $(this);
 			var data = source.data();
 			if (data.amsClickHandler) {
-				if (data.amsClickStopPropagation === true)
+				if ((data.amsStopPropagation === true) || (data.amsClickStopPropagation === true))
 					e.stopPropagation();
 				if (data.amsClickKeepDefault !== true)
 					e.preventDefault();
@@ -3443,11 +3828,13 @@
 
 		BTN_OK: "OK",
 		BTN_CANCEL: "Cancel",
+		BTN_OK_CANCEL: "[OK][Cancel]",
 		BTN_YES: "Yes",
 		BTN_NO: "No",
-		BTN_OK_CANCEL: "[OK][Cancel]",
+		BTN_YES_NO: "[Yes][No]",
 
 		FORM_CHANGED_WARNING: "Some changes were not saved. These updates will be lost if you leave this page.",
+		DELETE_WARNING: "This change can't be undone. Are you sure that you want to delete this element?",
 		NO_UPDATE: "No changes were applied.",
 		DATA_UPDATED: "Data successfully updated.",
 
@@ -3456,6 +3843,8 @@
 		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...",