src/myams/resources/js/myams-core.js
changeset 0 f05d7aea098a
child 39 fed67f169e1c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/myams/resources/js/myams-core.js	Fri Jul 10 16:59:11 2020 +0200
@@ -0,0 +1,582 @@
+/*
+ * MyAMS
+ * « My Application Management Skin »
+ *
+ * $Tag$ (rev. 1)
+ * A bootstrap based application/administration skin
+ *
+ * Custom administration and application skin tools
+ * Released under Zope Public License ZPL 1.1
+ * ©2014-2020 Thierry Florac <tflorac@ulthar.net>
+ */
+
+"use strict";
+
+(function ($, globals) {
+
+	var console = globals.console;
+
+	/**
+	 * String prototype extensions
+	 */
+	String.prototype.startsWith = function (str) {
+		var slen = this.length,
+			dlen = str.length;
+		if (slen < dlen) {
+			return false;
+		}
+		return (this.substr(0, dlen) === str);
+	};
+
+	String.prototype.endsWith = function (str) {
+		var slen = this.length,
+			dlen = str.length;
+		if (slen < dlen) {
+			return false;
+		}
+		return (this.substr(slen - dlen) === str);
+	};
+
+	String.prototype.unserialize = function (str) {
+		var str = decodeURIComponent(this);
+		var chunks = str.split('&'),
+			obj = {};
+		for (var c = 0; c < chunks.length; c++) {
+			var split = chunks[c].split('=', 2);
+			obj[split[0]] = split[1];
+		}
+		return obj;
+	};
+
+	/**
+	 * Array prototype extensions
+	 */
+	if (!Array.prototype.indexOf) {
+		Array.prototype.indexOf = function (elt, from) {
+			var len = this.length;
+
+			from = Number(from) || 0;
+			from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+			if (from < 0) {
+				from += len;
+			}
+
+			for (; from < len; from++) {
+				if (from in this && this[from] === elt) {
+					return from;
+				}
+			}
+			return -1;
+		};
+	}
+
+
+	/**
+	 * JQuery 'hasvalue' expression
+	 * Filter inputs containing value
+	 */
+	$.expr[":"].hasvalue = function (obj, index, meta /*, stack*/) {
+		return $(obj).val() !== "";
+	};
+
+
+	/**
+	 * JQuery 'econtains' expression
+	 * Case insensitive contains expression
+	 */
+	$.expr[":"].econtains = function (obj, index, meta /*, stack*/) {
+		return (obj.textContent || obj.innerText || $(obj).text() || "").toLowerCase() === meta[3].toLowerCase();
+	};
+
+
+	/**
+	 * JQuery 'withtext' expression
+	 * Case sensitive exact search expression
+	 */
+	$.expr[":"].withtext = function (obj, index, meta /*, stack*/) {
+		return (obj.textContent || obj.innerText || $(obj).text() || "") === meta[3];
+	};
+
+
+	/**
+	 * JQuery filter on parents class
+	 * 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;
+	};
+
+
+	/**
+	 * JQuery 'scrollbarWidth' function
+	 * Get width of default vertical scrollbar
+	 */
+	if ($.scrollbarWidth === undefined) {
+		$.scrollbarWidth = function () {
+			var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body');
+			var child = parent.children();
+			var width = child.innerWidth() - child.height(99).innerWidth();
+			parent.remove();
+			return width;
+		};
+	}
+
+
+	/**
+	 * MyAMS JQuery extensions
+	 */
+	$.fn.extend({
+
+		/**
+		 * Check if current object is empty or not
+		 */
+		exists: function () {
+			return $(this).length > 0;
+		},
+
+		/**
+		 * Get object if it supports given CSS class,
+		 * otherwise look for parents
+		 */
+		objectOrParentWithClass: function (klass) {
+			if (this.hasClass(klass)) {
+				return this;
+			} else {
+				return this.parents('.' + klass);
+			}
+		},
+
+		/**
+		 * Build an array of attributes of the given selection
+		 */
+		listattr: function (attr) {
+			var result = [];
+			this.each(function () {
+				result.push($(this).attr(attr));
+			});
+			return result;
+		},
+
+		/**
+		 * CSS style function
+		 * Code from Aram Kocharyan on stackoverflow.com
+		 */
+		style: function (styleName, value, priority) {
+			// DOM node
+			var node = this.get(0);
+			// Ensure we have a DOM node
+			if (typeof(node) === 'undefined') {
+				return;
+			}
+			// CSSStyleDeclaration
+			var style = this.get(0).style;
+			// Getter/Setter
+			if (typeof(styleName) !== 'undefined') {
+				if (typeof(value) !== 'undefined') {
+					// Set style property
+					priority = typeof(priority) !== 'undefined' ? priority : '';
+					style.setProperty(styleName, value, priority);
+					return this;
+				} else {
+					// Get style property
+					return style.getPropertyValue(styleName);
+				}
+			} else {
+				// Get CSSStyleDeclaration
+				return style;
+			}
+		},
+
+		/**
+		 * Remove CSS classes starting with a given prefix
+		 */
+		removeClassPrefix: function (prefix) {
+			this.each(function (i, it) {
+				var classes = it.className.split(" ").map(function (item) {
+					return item.startsWith(prefix) ? "" : item;
+				});
+				it.className = $.trim(classes.join(" "));
+			});
+			return this;
+		}
+	});
+
+
+	/**
+	 * MyAMS extensions to JQuery
+	 */
+	if (globals.MyAMS === undefined) {
+		globals.MyAMS = {
+			devmode: true,
+			devext: '',
+			lang: 'en',
+			throttleDelay: 350,
+			menuSpeed: 235,
+			navbarHeight: 49,
+			ajaxNav: true,
+			safeMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
+			csrfCookieName: 'csrf_token',
+			csrfHeaderName: 'X-CSRF-Token',
+			enableWidgets: true,
+			enableMobile: false,
+			enableFastclick: false,
+			warnOnFormChange: false,
+			formChangedCallback: null,
+			ismobile: (/iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent.toLowerCase()))
+		};
+	}
+	var MyAMS = globals.MyAMS;
+	var ams = MyAMS;
+
+	/**
+	 * Get MyAMS base URL
+	 * Copyright Andrew Davy: https://forrst.com/posts/Get_the_URL_of_the_current_javascript_file-Dst
+	 */
+	MyAMS.baseURL = (function () {
+		var script = $('script[src*="/myams.js"], script[src*="/myams.min.js"], ' +
+					   'script[src*="/myams-core.js"], script[src*="/myams-core.min.js"], ' +
+					   'script[src*="/myams-require.js"], script[src*="/myams-require.min.js"]');
+		var src = script.attr("src");
+		ams.devmode = src.indexOf('.min.js') < 0;
+		ams.devext = ams.devmode ? '' : '.min';
+		return src.substring(0, src.lastIndexOf('/') + 1);
+	})();
+
+
+	/**
+	 * Basic logging function which log all arguments to console
+	 */
+	MyAMS.log = function () {
+		if (console) {
+			console.debug && console.debug(this, arguments);
+		}
+	};
+
+
+	/**
+	 * Extract parameter value from given query string
+	 */
+	MyAMS.getQueryVar = function (src, varName) {
+		// Check src
+		if (src.indexOf('?') < 0) {
+			return false;
+		}
+		if (!src.endsWith('&')) {
+			src += '&';
+		}
+		// Dynamic replacement RegExp
+		var regex = new RegExp('.*?[&\\?]' + varName + '=(.*?)&.*');
+		// Apply RegExp to the query string
+		var val = src.replace(regex, "$1");
+		// If the string is the same, we didn't find a match - return false
+		return val === src ? false : val;
+	};
+
+
+	/**
+	 * Color conversion function
+	 */
+	MyAMS.rgb2hex = function (color) {
+		return "#" + $.map(color.match(/\b(\d+)\b/g), function (digit) {
+			return ('0' + parseInt(digit).toString(16)).slice(-2);
+		}).join('');
+	};
+
+
+	/**
+	 * Generate a random ID
+	 */
+	MyAMS.generateId = function () {
+		function s4() {
+			return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+		}
+
+		return s4() + s4() + s4() + s4();
+	};
+
+
+	/**
+	 * Generate a random UUID
+	 */
+	MyAMS.generateUUID = function () {
+		var d = new Date().getTime();
+		var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+			var r = (d + Math.random() * 16) % 16 | 0;
+			d = Math.floor(d / 16);
+			return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
+		});
+		return uuid;
+	};
+
+
+	/**
+	 * Get an object given by name
+	 */
+	MyAMS.getObject = function (objectName, context) {
+		if (!objectName) {
+			return undefined;
+		}
+		if (typeof(objectName) !== 'string') {
+			return objectName;
+		}
+		var namespaces = objectName.split(".");
+		context = (context === undefined || context === null) ? window : context;
+		for (var i = 0; i < namespaces.length; i++) {
+			try {
+				context = context[namespaces[i]];
+			} catch (e) {
+				return undefined;
+			}
+		}
+		return context;
+	};
+
+	/**
+	 * Get and execute a function given by name
+	 * Small piece of code by Jason Bunting
+	 */
+	MyAMS.getFunctionByName = function (functionName, context) {
+		if (functionName === undefined) {
+			return undefined;
+		} else if (typeof(functionName) === 'function') {
+			return functionName;
+		}
+		var namespaces = functionName.split(".");
+		var func = namespaces.pop();
+		context = (context === undefined || context === null) ? window : context;
+		for (var i = 0; i < namespaces.length; i++) {
+			try {
+				context = context[namespaces[i]];
+			} catch (e) {
+				return undefined;
+			}
+		}
+		try {
+			return context[func];
+		} catch (e) {
+			return undefined;
+		}
+	};
+
+	MyAMS.executeFunctionByName = function (functionName, context /*, args */) {
+		var func = ams.getFunctionByName(functionName, window);
+		if (typeof(func) === 'function') {
+			var args = Array.prototype.slice.call(arguments, 2);
+			return func.apply(context, args);
+		}
+	};
+
+	/**
+	 * Check to know if given element is still present in DOM
+	 */
+	MyAMS.isInDOM = function (element) {
+		element = $(element);
+		if (!element.exists()) {
+			return false;
+		}
+		return globals.document.body.contains(element[0]);
+	};
+
+	/**
+	 * 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) {
+			return ams.getFunctionByName(match.substr(1, match.length - 2));
+		});
+	};
+
+	/**
+	 * 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
+	 * @param onerror: an error callback to be called instead of generic callback
+	 */
+	MyAMS.getScript = function (url, callback, options, onerror) {
+		if (typeof(callback) === 'object') {
+			onerror = options;
+			options = callback;
+			callback = null;
+		}
+		if (options === undefined) {
+			options = {};
+		}
+		var defaults = {
+			dataType: 'script',
+			url: ams.getSource(url),
+			success: callback,
+			error: onerror || ams.error.show,
+			cache: !ams.devmode,
+			async: options.async === undefined ? typeof(callback) === 'function' : options.async
+		};
+		var settings = $.extend({}, defaults, options);
+		return $.ajax(settings);
+	};
+
+	/**
+	 * 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 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);
+			}
+		}
+	};
+
+	/**
+	 * Initialize main events handlers
+	 */
+	MyAMS.initHandlers = function(element) {
+
+		// Initialize custom click handlers
+		$(element).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')) {
+				return;
+			}
+			var data = source.data();
+			if (data.amsClickHandler) {
+				if ((data.amsStopPropagation === true) || (data.amsClickStopPropagation === true)) {
+					event.stopPropagation();
+				}
+				if (data.amsClickKeepDefault !== true) {
+					event.preventDefault();
+				}
+				var clickHandlers = data.amsClickHandler.split(/\s+/);
+				for (var index=0; index < clickHandlers.length; index++) {
+					var callback = ams.getFunctionByName(clickHandlers[index]);
+					if (callback !== undefined) {
+						callback.call(source, event, data.amsClickHandlerOptions);
+					}
+				}
+			}
+		});
+
+		// Initialize custom change handlers
+		$(element).on('change', '[data-ams-change-handler]', function(event) {
+			var source = $(this);
+			// Disable change handlers for readonly inputs
+			// These change handlers are activated by IE!!!
+			if (source.prop('readonly')) {
+				return;
+			}
+			var handlers = source.data('ams-disabled-handlers');
+			if ((handlers === true) || (handlers === 'change') || (handlers === 'all')) {
+				return;
+			}
+			var data = source.data();
+			if (data.amsChangeHandler) {
+				if ((data.amsStopPropagation === true) || (data.amsChangeStopPropagation === true)) {
+					event.stopPropagation();
+				}
+				if (data.amsChangeKeepDefault !== true) {
+					event.preventDefault();
+				}
+				var changeHandlers = data.amsChangeHandler.split(/\s+/);
+				for (var index=0; index < changeHandlers.length; index++) {
+					var callback = ams.getFunctionByName(changeHandlers[index]);
+					if (callback !== undefined) {
+						callback.call(source, event, data.amsChangeHandlerOptions);
+					}
+				}
+			}
+		});
+
+		// Notify reset to update Select2 widgets
+		$(element).on('reset', 'form', function(e) {
+			var form = $(this);
+			setTimeout(function() {
+				$('.alert-danger, SPAN.state-error', form).not('.persistent').remove();
+				$('LABEL.state-error', form).removeClass('state-error');
+				$('INPUT.select2[type="hidden"]', form).each(function() {
+					var input = $(this);
+					var select = input.data('select2');
+					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() {
+					var element = $(this);
+					var data = element.data();
+					var callback = ams.getFunctionByName(data.amsResetCallback);
+					if (callback !== undefined) {
+						callback.call(form, element, data.amsResetCallbackOptions);
+					}
+				});
+			}, 10);
+			ams.form && ams.form.setFocus(form);
+		});
+
+		// Initialize custom reset handlers
+		$(element).on('reset', '[data-ams-reset-handler]', function(e) {
+			var form = $(this);
+			var data = form.data();
+			if (data.amsResetHandler) {
+				if (data.amsResetKeepDefault !== true) {
+					e.preventDefault();
+				}
+				var callback = ams.getFunctionByName(data.amsResetHandler);
+				if (callback !== undefined) {
+					callback.call(form, data.amsResetHandlerOptions);
+				}
+			}
+		});
+
+		// Initialize custom event on click
+		$(element).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'));
+		});
+
+		// Cancel clicks on readonly checkbox
+		$(element).on('click', 'input[type="checkbox"][readonly]', function() {
+			return false;
+		});
+	};
+
+})(jQuery, this);