src/pyams_skin/resources/js/myams-core.js
changeset 566 a1707c607eec
parent 565 318533413200
child 567 bca1726b1d85
equal deleted inserted replaced
565:318533413200 566:a1707c607eec
     1 /*
       
     2  * MyAMS
       
     3  * « My Application Management Skin »
       
     4  *
       
     5  * $Tag$ (rev. 1)
       
     6  * A bootstrap based application/administration skin
       
     7  *
       
     8  * Custom administration and application skin tools
       
     9  * Released under Zope Public License ZPL 1.1
       
    10  * ©2014-2020 Thierry Florac <tflorac@ulthar.net>
       
    11  */
       
    12 
       
    13 "use strict";
       
    14 
       
    15 (function ($, globals) {
       
    16 
       
    17 	var console = globals.console;
       
    18 
       
    19 	/**
       
    20 	 * String prototype extensions
       
    21 	 */
       
    22 	String.prototype.startsWith = function (str) {
       
    23 		var slen = this.length,
       
    24 			dlen = str.length;
       
    25 		if (slen < dlen) {
       
    26 			return false;
       
    27 		}
       
    28 		return (this.substr(0, dlen) === str);
       
    29 	};
       
    30 
       
    31 	String.prototype.endsWith = function (str) {
       
    32 		var slen = this.length,
       
    33 			dlen = str.length;
       
    34 		if (slen < dlen) {
       
    35 			return false;
       
    36 		}
       
    37 		return (this.substr(slen - dlen) === str);
       
    38 	};
       
    39 
       
    40 	String.prototype.unserialize = function (str) {
       
    41 		var str = decodeURIComponent(this);
       
    42 		var chunks = str.split('&'),
       
    43 			obj = {};
       
    44 		for (var c = 0; c < chunks.length; c++) {
       
    45 			var split = chunks[c].split('=', 2);
       
    46 			obj[split[0]] = split[1];
       
    47 		}
       
    48 		return obj;
       
    49 	};
       
    50 
       
    51 	/**
       
    52 	 * Array prototype extensions
       
    53 	 */
       
    54 	if (!Array.prototype.indexOf) {
       
    55 		Array.prototype.indexOf = function (elt, from) {
       
    56 			var len = this.length;
       
    57 
       
    58 			from = Number(from) || 0;
       
    59 			from = (from < 0) ? Math.ceil(from) : Math.floor(from);
       
    60 			if (from < 0) {
       
    61 				from += len;
       
    62 			}
       
    63 
       
    64 			for (; from < len; from++) {
       
    65 				if (from in this && this[from] === elt) {
       
    66 					return from;
       
    67 				}
       
    68 			}
       
    69 			return -1;
       
    70 		};
       
    71 	}
       
    72 
       
    73 
       
    74 	/**
       
    75 	 * JQuery 'hasvalue' expression
       
    76 	 * Filter inputs containing value
       
    77 	 */
       
    78 	$.expr[":"].hasvalue = function (obj, index, meta /*, stack*/) {
       
    79 		return $(obj).val() !== "";
       
    80 	};
       
    81 
       
    82 
       
    83 	/**
       
    84 	 * JQuery 'econtains' expression
       
    85 	 * Case insensitive contains expression
       
    86 	 */
       
    87 	$.expr[":"].econtains = function (obj, index, meta /*, stack*/) {
       
    88 		return (obj.textContent || obj.innerText || $(obj).text() || "").toLowerCase() === meta[3].toLowerCase();
       
    89 	};
       
    90 
       
    91 
       
    92 	/**
       
    93 	 * JQuery 'withtext' expression
       
    94 	 * Case sensitive exact search expression
       
    95 	 */
       
    96 	$.expr[":"].withtext = function (obj, index, meta /*, stack*/) {
       
    97 		return (obj.textContent || obj.innerText || $(obj).text() || "") === meta[3];
       
    98 	};
       
    99 
       
   100 
       
   101 	/**
       
   102 	 * JQuery filter on parents class
       
   103 	 * This filter is often combined with ":not()" to select DOM objects which don't have
       
   104 	 * parents of a given class.
       
   105 	 * For example:
       
   106 	 *
       
   107 	 *   $('.hint:not(:parents(.nohints))', element);
       
   108 	 *
       
   109 	 * will select all elements with ".hint" class which don't have a parent with '.nohints' class.
       
   110 	 */
       
   111 	$.expr[':'].parents = function (obj, index, meta /*, stack*/) {
       
   112 		return $(obj).parents(meta[3]).length > 0;
       
   113 	};
       
   114 
       
   115 
       
   116 	/**
       
   117 	 * JQuery 'scrollbarWidth' function
       
   118 	 * Get width of default vertical scrollbar
       
   119 	 */
       
   120 	if ($.scrollbarWidth === undefined) {
       
   121 		$.scrollbarWidth = function () {
       
   122 			var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body');
       
   123 			var child = parent.children();
       
   124 			var width = child.innerWidth() - child.height(99).innerWidth();
       
   125 			parent.remove();
       
   126 			return width;
       
   127 		};
       
   128 	}
       
   129 
       
   130 
       
   131 	/**
       
   132 	 * MyAMS JQuery extensions
       
   133 	 */
       
   134 	$.fn.extend({
       
   135 
       
   136 		/**
       
   137 		 * Check if current object is empty or not
       
   138 		 */
       
   139 		exists: function () {
       
   140 			return $(this).length > 0;
       
   141 		},
       
   142 
       
   143 		/**
       
   144 		 * Get object if it supports given CSS class,
       
   145 		 * otherwise look for parents
       
   146 		 */
       
   147 		objectOrParentWithClass: function (klass) {
       
   148 			if (this.hasClass(klass)) {
       
   149 				return this;
       
   150 			} else {
       
   151 				return this.parents('.' + klass);
       
   152 			}
       
   153 		},
       
   154 
       
   155 		/**
       
   156 		 * Build an array of attributes of the given selection
       
   157 		 */
       
   158 		listattr: function (attr) {
       
   159 			var result = [];
       
   160 			this.each(function () {
       
   161 				result.push($(this).attr(attr));
       
   162 			});
       
   163 			return result;
       
   164 		},
       
   165 
       
   166 		/**
       
   167 		 * CSS style function
       
   168 		 * Code from Aram Kocharyan on stackoverflow.com
       
   169 		 */
       
   170 		style: function (styleName, value, priority) {
       
   171 			// DOM node
       
   172 			var node = this.get(0);
       
   173 			// Ensure we have a DOM node
       
   174 			if (typeof(node) === 'undefined') {
       
   175 				return;
       
   176 			}
       
   177 			// CSSStyleDeclaration
       
   178 			var style = this.get(0).style;
       
   179 			// Getter/Setter
       
   180 			if (typeof(styleName) !== 'undefined') {
       
   181 				if (typeof(value) !== 'undefined') {
       
   182 					// Set style property
       
   183 					priority = typeof(priority) !== 'undefined' ? priority : '';
       
   184 					style.setProperty(styleName, value, priority);
       
   185 					return this;
       
   186 				} else {
       
   187 					// Get style property
       
   188 					return style.getPropertyValue(styleName);
       
   189 				}
       
   190 			} else {
       
   191 				// Get CSSStyleDeclaration
       
   192 				return style;
       
   193 			}
       
   194 		},
       
   195 
       
   196 		/**
       
   197 		 * Remove CSS classes starting with a given prefix
       
   198 		 */
       
   199 		removeClassPrefix: function (prefix) {
       
   200 			this.each(function (i, it) {
       
   201 				var classes = it.className.split(" ").map(function (item) {
       
   202 					return item.startsWith(prefix) ? "" : item;
       
   203 				});
       
   204 				it.className = $.trim(classes.join(" "));
       
   205 			});
       
   206 			return this;
       
   207 		}
       
   208 	});
       
   209 
       
   210 
       
   211 	/**
       
   212 	 * MyAMS extensions to JQuery
       
   213 	 */
       
   214 	if (globals.MyAMS === undefined) {
       
   215 		globals.MyAMS = {
       
   216 			devmode: true,
       
   217 			devext: '',
       
   218 			lang: 'en',
       
   219 			throttleDelay: 350,
       
   220 			menuSpeed: 235,
       
   221 			navbarHeight: 49,
       
   222 			ajaxNav: true,
       
   223 			safeMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
       
   224 			csrfCookieName: 'csrf_token',
       
   225 			csrfHeaderName: 'X-CSRF-Token',
       
   226 			enableWidgets: true,
       
   227 			enableMobile: false,
       
   228 			enableFastclick: false,
       
   229 			warnOnFormChange: false,
       
   230 			formChangedCallback: null,
       
   231 			ismobile: (/iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent.toLowerCase()))
       
   232 		};
       
   233 	}
       
   234 	var MyAMS = globals.MyAMS;
       
   235 	var ams = MyAMS;
       
   236 
       
   237 	/**
       
   238 	 * Get MyAMS base URL
       
   239 	 * Copyright Andrew Davy: https://forrst.com/posts/Get_the_URL_of_the_current_javascript_file-Dst
       
   240 	 */
       
   241 	MyAMS.baseURL = (function () {
       
   242 		var script = $('script[src*="/myams.js"], script[src*="/myams.min.js"], ' +
       
   243 					   'script[src*="/myams-core.js"], script[src*="/myams-core.min.js"], ' +
       
   244 					   'script[src*="/myams-require.js"], script[src*="/myams-require.min.js"]');
       
   245 		var src = script.attr("src");
       
   246 		ams.devmode = src.indexOf('.min.js') < 0;
       
   247 		ams.devext = ams.devmode ? '' : '.min';
       
   248 		return src.substring(0, src.lastIndexOf('/') + 1);
       
   249 	})();
       
   250 
       
   251 
       
   252 	/**
       
   253 	 * Basic logging function which log all arguments to console
       
   254 	 */
       
   255 	MyAMS.log = function () {
       
   256 		if (console) {
       
   257 			console.debug && console.debug(this, arguments);
       
   258 		}
       
   259 	};
       
   260 
       
   261 
       
   262 	/**
       
   263 	 * Extract parameter value from given query string
       
   264 	 */
       
   265 	MyAMS.getQueryVar = function (src, varName) {
       
   266 		// Check src
       
   267 		if (src.indexOf('?') < 0) {
       
   268 			return false;
       
   269 		}
       
   270 		if (!src.endsWith('&')) {
       
   271 			src += '&';
       
   272 		}
       
   273 		// Dynamic replacement RegExp
       
   274 		var regex = new RegExp('.*?[&\\?]' + varName + '=(.*?)&.*');
       
   275 		// Apply RegExp to the query string
       
   276 		var val = src.replace(regex, "$1");
       
   277 		// If the string is the same, we didn't find a match - return false
       
   278 		return val === src ? false : val;
       
   279 	};
       
   280 
       
   281 
       
   282 	/**
       
   283 	 * Color conversion function
       
   284 	 */
       
   285 	MyAMS.rgb2hex = function (color) {
       
   286 		return "#" + $.map(color.match(/\b(\d+)\b/g), function (digit) {
       
   287 			return ('0' + parseInt(digit).toString(16)).slice(-2);
       
   288 		}).join('');
       
   289 	};
       
   290 
       
   291 
       
   292 	/**
       
   293 	 * Generate a random ID
       
   294 	 */
       
   295 	MyAMS.generateId = function () {
       
   296 		function s4() {
       
   297 			return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
       
   298 		}
       
   299 
       
   300 		return s4() + s4() + s4() + s4();
       
   301 	};
       
   302 
       
   303 
       
   304 	/**
       
   305 	 * Generate a random UUID
       
   306 	 */
       
   307 	MyAMS.generateUUID = function () {
       
   308 		var d = new Date().getTime();
       
   309 		var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
       
   310 			var r = (d + Math.random() * 16) % 16 | 0;
       
   311 			d = Math.floor(d / 16);
       
   312 			return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
       
   313 		});
       
   314 		return uuid;
       
   315 	};
       
   316 
       
   317 
       
   318 	/**
       
   319 	 * Get an object given by name
       
   320 	 */
       
   321 	MyAMS.getObject = function (objectName, context) {
       
   322 		if (!objectName) {
       
   323 			return undefined;
       
   324 		}
       
   325 		if (typeof(objectName) !== 'string') {
       
   326 			return objectName;
       
   327 		}
       
   328 		var namespaces = objectName.split(".");
       
   329 		context = (context === undefined || context === null) ? window : context;
       
   330 		for (var i = 0; i < namespaces.length; i++) {
       
   331 			try {
       
   332 				context = context[namespaces[i]];
       
   333 			} catch (e) {
       
   334 				return undefined;
       
   335 			}
       
   336 		}
       
   337 		return context;
       
   338 	};
       
   339 
       
   340 	/**
       
   341 	 * Get and execute a function given by name
       
   342 	 * Small piece of code by Jason Bunting
       
   343 	 */
       
   344 	MyAMS.getFunctionByName = function (functionName, context) {
       
   345 		if (functionName === undefined) {
       
   346 			return undefined;
       
   347 		} else if (typeof(functionName) === 'function') {
       
   348 			return functionName;
       
   349 		}
       
   350 		var namespaces = functionName.split(".");
       
   351 		var func = namespaces.pop();
       
   352 		context = (context === undefined || context === null) ? window : context;
       
   353 		for (var i = 0; i < namespaces.length; i++) {
       
   354 			try {
       
   355 				context = context[namespaces[i]];
       
   356 			} catch (e) {
       
   357 				return undefined;
       
   358 			}
       
   359 		}
       
   360 		try {
       
   361 			return context[func];
       
   362 		} catch (e) {
       
   363 			return undefined;
       
   364 		}
       
   365 	};
       
   366 
       
   367 	MyAMS.executeFunctionByName = function (functionName, context /*, args */) {
       
   368 		var func = ams.getFunctionByName(functionName, window);
       
   369 		if (typeof(func) === 'function') {
       
   370 			var args = Array.prototype.slice.call(arguments, 2);
       
   371 			return func.apply(context, args);
       
   372 		}
       
   373 	};
       
   374 
       
   375 	/**
       
   376 	 * Check to know if given element is still present in DOM
       
   377 	 */
       
   378 	MyAMS.isInDOM = function (element) {
       
   379 		element = $(element);
       
   380 		if (!element.exists()) {
       
   381 			return false;
       
   382 		}
       
   383 		return globals.document.body.contains(element[0]);
       
   384 	};
       
   385 
       
   386 	/**
       
   387 	 * Get target URL matching given source
       
   388 	 *
       
   389 	 * Given URL can include variable names (with their namespace), given between braces, as in {MyAMS.baseURL}
       
   390 	 */
       
   391 	MyAMS.getSource = function (url) {
       
   392 		return url.replace(/{[^{}]*}/g, function (match) {
       
   393 			return ams.getFunctionByName(match.substr(1, match.length - 2));
       
   394 		});
       
   395 	};
       
   396 
       
   397 	/**
       
   398 	 * Script loader function
       
   399 	 *
       
   400 	 * @param url: script URL
       
   401 	 * @param callback: a callback to be called after script loading
       
   402 	 * @param options: a set of options to be added to AJAX call
       
   403 	 * @param onerror: an error callback to be called instead of generic callback
       
   404 	 */
       
   405 	MyAMS.getScript = function (url, callback, options, onerror) {
       
   406 		if (typeof(callback) === 'object') {
       
   407 			onerror = options;
       
   408 			options = callback;
       
   409 			callback = null;
       
   410 		}
       
   411 		if (options === undefined) {
       
   412 			options = {};
       
   413 		}
       
   414 		var defaults = {
       
   415 			dataType: 'script',
       
   416 			url: ams.getSource(url),
       
   417 			success: callback,
       
   418 			error: onerror || ams.error.show,
       
   419 			cache: !ams.devmode,
       
   420 			async: options.async === undefined ? typeof(callback) === 'function' : options.async
       
   421 		};
       
   422 		var settings = $.extend({}, defaults, options);
       
   423 		return $.ajax(settings);
       
   424 	};
       
   425 
       
   426 	/**
       
   427 	 * CSS file loader function
       
   428 	 * Cross-browser code copied from Stoyan Stefanov blog to be able to
       
   429 	 * call a callback when CSS is realy loaded.
       
   430 	 * See: https://www.phpied.com/when-is-a-stylesheet-really-loaded
       
   431 	 *
       
   432 	 * @param url: CSS file URL
       
   433 	 * @param id: a unique ID given to CSS file
       
   434 	 * @param callback: optional callback function to be called when CSS file is loaded. If set, callback is called
       
   435 	 *   with a 'first_load' boolean argument to indicate is CSS was already loaded (*false* value) or not (*true*
       
   436 	 *   value).
       
   437 	 * @param options: callback options
       
   438 	 */
       
   439 	MyAMS.getCSS = function (url, id, callback, options) {
       
   440 		if (callback) {
       
   441 			callback = ams.getFunctionByName(callback);
       
   442 		}
       
   443 		var head = $('HEAD');
       
   444 		var style = $('style[data-ams-id="' + id + '"]', head);
       
   445 		if (style.length === 0) {
       
   446 			style = $('<style>').attr('data-ams-id', id)
       
   447 				.text('@import "' + ams.getSource(url) + '";');
       
   448 			if (callback) {
       
   449 				var styleInterval = setInterval(function () {
       
   450 					try {
       
   451 						var _check = style[0].sheet.cssRules;  // Is only populated when file is loaded
       
   452 						callback.call(window, true, options);
       
   453 						clearInterval(styleInterval);
       
   454 					} catch (e) {
       
   455 						// CSS is not loaded yet...
       
   456 					}
       
   457 				}, 10);
       
   458 			}
       
   459 			style.appendTo(head);
       
   460 		} else {
       
   461 			if (callback) {
       
   462 				callback.call(window, false, options);
       
   463 			}
       
   464 		}
       
   465 	};
       
   466 
       
   467 	/**
       
   468 	 * Initialize main events handlers
       
   469 	 */
       
   470 	MyAMS.initHandlers = function(element) {
       
   471 
       
   472 		// Initialize custom click handlers
       
   473 		$(element).on('click', '[data-ams-click-handler]', function(event) {
       
   474 			var source = $(this);
       
   475 			var handlers = source.data('ams-disabled-handlers');
       
   476 			if ((handlers === true) || (handlers === 'click') || (handlers === 'all')) {
       
   477 				return;
       
   478 			}
       
   479 			var data = source.data();
       
   480 			if (data.amsClickHandler) {
       
   481 				if ((data.amsStopPropagation === true) || (data.amsClickStopPropagation === true)) {
       
   482 					event.stopPropagation();
       
   483 				}
       
   484 				if (data.amsClickKeepDefault !== true) {
       
   485 					event.preventDefault();
       
   486 				}
       
   487 				var clickHandlers = data.amsClickHandler.split(/\s+/);
       
   488 				for (var index=0; index < clickHandlers.length; index++) {
       
   489 					var callback = ams.getFunctionByName(clickHandlers[index]);
       
   490 					if (callback !== undefined) {
       
   491 						callback.call(source, event, data.amsClickHandlerOptions);
       
   492 					}
       
   493 				}
       
   494 			}
       
   495 		});
       
   496 
       
   497 		// Initialize custom change handlers
       
   498 		$(element).on('change', '[data-ams-change-handler]', function(event) {
       
   499 			var source = $(this);
       
   500 			// Disable change handlers for readonly inputs
       
   501 			// These change handlers are activated by IE!!!
       
   502 			if (source.prop('readonly')) {
       
   503 				return;
       
   504 			}
       
   505 			var handlers = source.data('ams-disabled-handlers');
       
   506 			if ((handlers === true) || (handlers === 'change') || (handlers === 'all')) {
       
   507 				return;
       
   508 			}
       
   509 			var data = source.data();
       
   510 			if (data.amsChangeHandler) {
       
   511 				if ((data.amsStopPropagation === true) || (data.amsChangeStopPropagation === true)) {
       
   512 					event.stopPropagation();
       
   513 				}
       
   514 				if (data.amsChangeKeepDefault !== true) {
       
   515 					event.preventDefault();
       
   516 				}
       
   517 				var changeHandlers = data.amsChangeHandler.split(/\s+/);
       
   518 				for (var index=0; index < changeHandlers.length; index++) {
       
   519 					var callback = ams.getFunctionByName(changeHandlers[index]);
       
   520 					if (callback !== undefined) {
       
   521 						callback.call(source, event, data.amsChangeHandlerOptions);
       
   522 					}
       
   523 				}
       
   524 			}
       
   525 		});
       
   526 
       
   527 		// Notify reset to update Select2 widgets
       
   528 		$(element).on('reset', 'form', function(e) {
       
   529 			var form = $(this);
       
   530 			setTimeout(function() {
       
   531 				$('.alert-danger, SPAN.state-error', form).not('.persistent').remove();
       
   532 				$('LABEL.state-error', form).removeClass('state-error');
       
   533 				$('INPUT.select2[type="hidden"]', form).each(function() {
       
   534 					var input = $(this);
       
   535 					var select = input.data('select2');
       
   536 					var value = input.data('ams-select2-input-value');
       
   537 					if (value) {
       
   538 						input.select2('val', value.split(select.opts.separator));
       
   539 					}
       
   540 				});
       
   541 				form.find('.select2').trigger('change');
       
   542 				$('[data-ams-reset-callback]', form).each(function() {
       
   543 					var element = $(this);
       
   544 					var data = element.data();
       
   545 					var callback = ams.getFunctionByName(data.amsResetCallback);
       
   546 					if (callback !== undefined) {
       
   547 						callback.call(form, element, data.amsResetCallbackOptions);
       
   548 					}
       
   549 				});
       
   550 			}, 10);
       
   551 			ams.form && ams.form.setFocus(form);
       
   552 		});
       
   553 
       
   554 		// Initialize custom reset handlers
       
   555 		$(element).on('reset', '[data-ams-reset-handler]', function(e) {
       
   556 			var form = $(this);
       
   557 			var data = form.data();
       
   558 			if (data.amsResetHandler) {
       
   559 				if (data.amsResetKeepDefault !== true) {
       
   560 					e.preventDefault();
       
   561 				}
       
   562 				var callback = ams.getFunctionByName(data.amsResetHandler);
       
   563 				if (callback !== undefined) {
       
   564 					callback.call(form, data.amsResetHandlerOptions);
       
   565 				}
       
   566 			}
       
   567 		});
       
   568 
       
   569 		// Initialize custom event on click
       
   570 		$(element).on('click', '[data-ams-click-event]', function(e) {
       
   571 			var source = $(this);
       
   572 			$(e.target).trigger(source.data('ams-click-event'),
       
   573 								source.data('ams-click-event-options'));
       
   574 		});
       
   575 
       
   576 		// Cancel clicks on readonly checkbox
       
   577 		$(element).on('click', 'input[type="checkbox"][readonly]', function() {
       
   578 			return false;
       
   579 		});
       
   580 	};
       
   581 
       
   582 })(jQuery, this);