--- /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);