All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.yadaframework.views.yada.js.yada.js Maven / Gradle / Ivy

There is a newer version: 0.7.7.R4
Show newest version
// yada.js

(function( yada ) {
	"use strict";
	
	// Namespace trick explained here: http://stackoverflow.com/a/5947280/587641
	// For a public property or function, use "yada.xxx = ..."
	// For a private property use "var xxx = "
	// For a private function use "function xxx(..."

	yada.baseLoaded = true; // Flag that this file has been loaded
	
	yada.devMode = false; // Set to true in development mode (also via thymeleaf)
	yada.baseUrl = null;	// Set it via thymeleaf
	yada.resourceDir = null; // Set it via thymeleaf
	var loaderStart = 0;
	yada.messages = {};
	yada.messages.connectionError = { // Set it via thymeleaf
		"title": "Connection Error",
		"message": "Failed to contact server - please try again later"
	}; 
	yada.messages.forbiddenError = { // Set it via thymeleaf
			"title": "Authorization Error",
			"message": "You don't have permission to access the requested page"
	}; 
	yada.messages.serverError = { // Set it via thymeleaf
			"title": "Server Error",
			"message": "Something is wrong - please try again later"
	}; 
	yada.messages.confirmButtons = { // Set it via thymeleaf
			"ok": "Ok",
			"cancel": "Cancel"
	}; 
	
	var siteMatcher=RegExp("(?:http.?://)?([^/:]*).*"); // Extract the server name from a url like "http://www.aaa.com/xxx" or "www.aaa.com"
	
	const findSelector = "yadaFind:"; // Used to indicate that a CSS selector should be searched in the children using find()
	const parentSelector = "yadaParents:"; // Used to indicate that a CSS selector should be searched in the parents()
	const siblingsSelector = "yadaSiblings:"; // Used to indicate that a CSS selector should be searched in the siblings()
	const closestFindSelector = "yadaClosestFind:"; // Used to indicate that a two-part CSS selector should be searched with closest() then with find()
	const siblingsFindSelector = "yadaSiblingsFind:"; // Used to indicate that a two-part CSS selector should be searched with siblings() then with find()
	
	const sessionStorageKeyTimezone = "yada.timezone.sent";
	const scrollTopParamName = "scrolltop";
	
	yada.stickyModalMarker = "yadaStickyModal";
	
	$(document).ready(function() {
		handleScrollTop();
		// Be aware that all ajax links and forms will NOT be ajax if the user clicks while the document is still loading.
		// To prevent that, call yada.initAjaxHandlersOn($('form,a')); at the html bottom and just after including the yada.ajax.js script
		initHandlers();
		if (typeof yada.initYadaDialect == "function") {
			yada.initYadaDialect();
		}
		
		// Send the current timezone offset to the server, once per browser session
		const timezoneSent = sessionStorage.getItem(sessionStorageKeyTimezone);
		if (!timezoneSent) {
			const data = {
				'timezone': Intl.DateTimeFormat().resolvedOptions().timeZone
			}
			jQuery.post("/yadaTimezone", data, function(){
				sessionStorage.setItem(sessionStorageKeyTimezone, true);
			});
		}
	});
	
	function initHandlers() {
		// Mostra il loader sugli elementi con le classi s_showLoaderClick oppure s_showLoaderForm
		$('body').on('click', ':not(form).yadaShowLoader', yada.loaderOn);
		$('body').on('submit', '.yadaShowLoader', yada.loaderOn);
		// Legacy
		$('body').on('click', '.s_showLoaderClick', yada.loaderOn);
		$('body').on('submit', '.s_showLoaderForm', yada.loaderOn);
		yada.enableScrollTopButton(); // Abilita il torna-su
		yada.initHandlersOn();
		// When the page is pulled from bfcache (firefox/safari) turn the loader off
		$(window).bind("pageshow", function(event) {
		    if (event.originalEvent.persisted) {
		        yada.loaderOff();
		    }
		});		
	}
	
	/**
	 * Init yada handlers on the specified element
	 * @param $element the element, or null for the entire body
	 */
	yada.initHandlersOn = function($element) {
		// Use case for $element being not null: after an ajax call in order to init just the added HTML
		yada.enableParentForm($element);
		yada.enableShowPassword($element);
		yada.enableRefreshButtons($element);
		yada.enableConfirmLinks($element);
		yada.enableHelpButton($element);
		yada.enableTooltip($element);
		yada.makeCustomPopover($element);
		if (typeof yada.initAjaxHandlersOn == "function") {
			yada.initAjaxHandlersOn($element);
		}
		yada.enableHashing($element);
		yada.enableFormGroup($element);
	}
	
	yada.log = function(message) {
		if (yada.devMode) {
			console.log("[yada] " + message);
		}
	}
	
	yada.loaderOn = function() {
		loaderStart = Date.now();
		$(".loader").show();
	};
	
	yada.loaderOff = function() {
		// The loader must be shown at least for 200 milliseconds or it gets annoying
		var elapsedMillis = Date.now() - loaderStart;
		if (elapsedMillis>200) {
			$(".loader").hide();
		} else {
			setTimeout(function(){ $(".loader").hide(); }, 200-elapsedMillis);
		}
	};

	
	/**
	 * Execute a comma-separated list of function names or an inline function (a function body).
	 * Each function is called only if the previous one didn't return null.
	 * See https://stackoverflow.com/a/359910/587641
	 * @param functionList comma-separated list of function names, in the window scope, that can have namespaces like "mylib.myfunc".
	 *			It can also be an inline function (with or without function(){} declaration).
	 * @param thisObject the object that will become the this object in the called function
	 * Any number of arguments can be passed to the function
	 * @return the functions return value in "and" (with null being true), or null if there was no function/body in the list.
	 */
	 yada.executeFunctionListByName = function(functionList, thisObject /*, optional args are also taken */) {
		var args = Array.prototype.slice.call(arguments, 2); // creates a new array containing all arguments starting from the third one
		var result = true;
		// Try the case where there's a list of function names
		var functionArray = yada.listToArray(functionList); // Split at comma followed by any spaces
		for (var i = 0; i < functionArray.length; i++) {
			const functionResult = yada.executeFunctionByName(functionArray[i], thisObject, ...args);
			if (functionResult==null) {
				result = null;
				break;
			}
			result &&= functionResult;
		}
		if (result==null && functionArray.length>1) {
			// Could be a function body containing a comma, so try the whole string
			 result = yada.executeFunctionByName(functionList, thisObject, ...args);
		}
		if (result==null) {
			yada.log("Invalid function list: " + functionList);
		}
		return result;
	}
		
	/**
	 * Execute function by name. Also execute an inline function (a function body).
	 * See https://stackoverflow.com/a/359910/587641
	 * @param functionName the name of the function, in the window scope, that can have namespaces like "mylib.myfunc".
	 *			It can also be an inline function (with or without function(){} declaration).
	 * @param thisObject the object that will become the this object in the called function
	 * Any number of arguments can be passed to the function
	 * @return the function return value (with null converted to true), or null in case of error calling the function (e.g. function not found or invalid function body)
	 */
	 yada.executeFunctionByName = function(functionName, thisObject /*, args */) {
		var context = window; // The functionName is always searched in the current window
		var args = Array.prototype.slice.call(arguments, 2); // creates a new array containing all arguments starting from the third one
		var namespaces = functionName.split(".");
		var func = namespaces.pop();
		for(var i = 0; i < namespaces.length && context!=null; i++) {
			context = context[namespaces[i]];
		}
		var functionObject = context?context[func]:null;
		if (functionObject==null) {
			// It might be a function body
			try {
				var functionBody = functionName.trim();
				// Strip any "function(){xxx}" declaration
				if (yada.startsWith(functionName, "function(")) {
					functionBody = functionName.replace(new RegExp("(?:function\\s*\\(\\)\\s*{)?([^}]+)}?"), "$1");
				}
				functionObject = new Function('responseText', 'responseHtml', 'link', functionBody); // Throws error when not a function body
			} catch (error) {
				// console.error(error);
				// yada.log("Function '" + func + "' not found (ignored)");
				return null;
			}
		}
		const returnValue = functionObject?.apply(thisObject, args);
		// null is converted to true (e.g. "keep going")
		return returnValue ?? true;
	}
	
	/**
	 * Changes the browser url when an element is clicked
	 */
	yada.enableHashing = function($element) {
		if ($element==null) {
			$element = $('body');
		}
		$('[data-yadaHash]', $element).not(".yadaHashed").click(function(){
			var hashValue = $(this).attr('data-yadaHash')
			const newUrl = yada.replaceHash(window.location.href, hashValue);
			history.pushState({'yadaHash': true, 'hashValue' : hashValue}, null, newUrl);
		}).addClass("yadaHashed");
	}

	yada.enableTooltip = function($element) {
		if ($element==null) {
			$element = $('body');
		}
	    $('.s_tooltip', $element).tooltip && $('.s_tooltip', $element).tooltip();
	};
	
	yada.enableHelpButton = function($element) {
		if ($element==null) {
			$element = $('body');
		}
		$('.yadaHelpButton', $element).popover && $('.yadaHelpButton', $element).popover();
	};

	/**
	 * Reloads the page.
	 * If the previous call was a non-ajax post and the current address has a #, the post is repeated.
	 * Otherwise posts are not repeated.
	 */
	yada.reload = function() {
		var hashPresent = yada.stringContains(window.location.href, '#');
		if (!hashPresent) {
			window.location.replace(window.location.href);
		} else {
			window.location.reload(true); // Careful that non-ajax POST is repeated
		}
	}
	
	/**
	 * Enables clicking on a refresh button (yadaRefresh) to call an ajax data-loading handler. 
	 * The handler is also called at document-ready
	 */
	yada.enableRefreshButtons = function($element) {
		if ($element==null) {
			$element = $('body');
		}
		$('.yadaRefresh', $element).not('.yadaRefreshed').each(function(){
			var handlerName = $(this).attr('data-yadaRefreshHandler');
			if (handlerName===undefined) {
				handlerName = $(this).attr('data-handler'); // Legacy
			}
			var dataHandler = window[handlerName];
			if (typeof dataHandler === "function") {
				$(this).click(dataHandler);
				// Call it now also
				dataHandler();
			}
			$(this).addClass('yadaRefreshed');
		});
	}
	
	/**
	 * Enable the "scroll to top" button.
	*/
	yada.enableScrollTopButton = function() {
		const $scrollTopButton = $('.yadaScrollTop');
		if ($scrollTopButton.length>0) {
			$scrollTopButton.off().click( function(e){
				e.preventDefault();
				window.scrollTo({ top: 0, left: 0, behavior: 'smooth'});
			});
			$(document).on('scroll', function() {
				const visible = $scrollTopButton.is(":visible");
				var y = $(this).scrollTop();
				if (y > 800) {
					visible || $scrollTopButton.fadeIn();
				} else {
					visible && $scrollTopButton.fadeOut();
				}
			});
		}
	};

	/**
	 * When a function can be called repeatedly but only the last call is useful, previous
	 * calls can be cancelled by next ones if within a given timeout.
	 * When the funcion takes too long to execute, the timeout is increased so that less calls are performed.
	 * Useful when making ajax calls.
	 * A small delay must be tolerated.
	 * @param domElement any dom element on which a flag can be set. Must be the same for repeated calls.
	 * @param functionToCall any function (can be an inline function)
	 */
	yada.dequeueFunctionCall = function(domElement, functionToCall) {
		// TODO see https://css-tricks.com/debouncing-throttling-explained-examples/#aa-debounce
		var callTimeout = 200;
		if (domElement.yadaDequeueFunctionCallRunning!=null) {
			// Ajax call still running, so delay a bit longer before the next one
			callTimeout = 2000;
		}
		clearTimeout(domElement.yadaDequeueFunctionTimeoutHandler);
		domElement.yadaDequeueFunctionTimeoutHandler = setTimeout(function(){
			domElement.yadaDequeueFunctionCallRunning = true;
			functionToCall.bind(domElement)();
			domElement.yadaDequeueFunctionCallRunning = null; // This may clear some other's call flag but don't care
		}, callTimeout);
	}

	/**
	 * Allow only certain characters to be typed into an input field based on a regexp
	 * Taken from https://stackoverflow.com/questions/995183/how-to-allow-only-numeric-0-9-in-html-inputbox-using-jquery/995193#995193
	 * See /YadaWeb/src/main/resources/net/yadaframework/views/yada/formfields/input.html for an example
	 */	
	$.fn.yadaInputFilter = function(inputFilter) {
		return this.on("input keydown keyup mousedown mouseup select contextmenu drop", function() {
			if (inputFilter(this.value)) {
				this.oldValue = this.value;
				this.oldSelectionStart = this.selectionStart;
				this.oldSelectionEnd = this.selectionEnd;
			} else if (this.hasOwnProperty("oldValue")) {
				this.value = this.oldValue;
				this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
			} else {
				this.value = "";
			}
		});
	}
	
	//////////////////////
	/// Url Parameters ///
	//////////////////////
	
	yada.addScrollTop = function(url) {
		return yada.addOrUpdateUrlParameter(url, scrollTopParamName, $(window).scrollTop());
	}
	
	function handleScrollTop() {
		const scrollTopValue = yada.getUrlParameter(window.location.href, scrollTopParamName);
		if (scrollTopValue!=null) {
			$(window).scrollTop(scrollTopValue);
			history.replaceState(null, "", yada.removeUrlParameter(window.location.href, scrollTopParamName));
		}
	}
	
	// Ritorna true se la url contiene il parametro indicato
	yada.hasUrlParameter = function(url, param) {
		return yada.getUrlParameter(url, param) !=null;
	};
	
	
	/**
	 * Rimuove dalla url il parametro di request indicato e la ritorna (non funziona se il parametro è senza valore)
	 * @param url
	 * @param param
	 * @param value the current parameter value, or null for any value
	 */
	yada.removeUrlParameter = function(url, param, value) {
		if (value==null) {
			value="[^&]*";
		}
		var regex = new RegExp("[?|&]" + param + "=" + value + "&?", 'g');
		url = url.replace(regex, '&');
		if (yada.endsWith(url, '&')) {
			url = url.substring(0, url.length-1);
		}
		// '?' needs to be reinserted if first param was removed
		var pos = url.indexOf('&');
		if (pos>-1 && url.indexOf('?')==-1) {
			url = yada.replaceAt(url, pos, '?');
		}
	 return url;
	};
	
	// Rimuove dalla url tutti i parametri di request indicati e la ritorna (forse non funziona se il parametro è senza valore)
	yada.removeUrlParameters = function(url, param) {
		var regex = new RegExp("[?|&]" + param + "=[^&]+&?", 'g');
		url = url.replace(regex, '&');
		if (yada.endsWith(url, '&')) {
			url = url.substring(0, url.length-1);
		}
		// '?' needs to be reinserted if first param was removed
		var pos = url.indexOf('&');
		if (url.indexOf('?')==-1 && pos>-1) {
			url = yada.replaceAt(url, pos, '?');
		}
		return url;
	};
	
	/**
	 * Aggiunge un parametro di request alla url, anche se già presente. La url può anche essere solo la location.search e può essere vuota.
	 * Ritorna la url modificata. Se value==null, il parametro è aggiunto senza valore.
	 * @param url la url o la query string o il segmento da modificare
	 * @param param nome del parametro da aggiungere
	 * @param value valore da aggiungere, can be null
	 * @param addQuestionMark (optional) if explicitly false, do not add a question mark when missing
	 */
	yada.addUrlParameter = function(url, param, value, addQuestionMark) {
		addQuestionMark = addQuestionMark==false?false:true;
		var anchor="";
		if (url) {
			var anchorPos = url.indexOf('#');
			anchor = anchorPos>-1?url.substring(anchorPos):"";
			url = anchorPos>-1?url.substring(0, anchorPos):url;
			if (url.indexOf('?')==-1 && addQuestionMark==true) {
				url = url + '?';
			} else {
				url = url + '&';
			}
		} else if (addQuestionMark==true) {
			url = "?";
		}
		url = url + encodeURIComponent(param);
		if (value!=null) {
			url = url + '=' + encodeURIComponent(value);
		}
		return url+anchor;
	};
	
	// Modifica o aggiunge un parametro di request alla url. La url può anche essere solo la location.search e può essere vuota.
	// if papram is null or empty, the url is unchanged.
	yada.addOrUpdateUrlParameter = function(url, param, value, addQuestionMark) {
		if (param==null || param=="") {
			return url;
		}
		if (yada.hasUrlParameter(url, param)) {
			return yada.updateUrlParameter(url, param, value);
		}
		return yada.addUrlParameter(url, param, value, addQuestionMark);
	};
	
	// Aggiunge un parametro di request alla url, solo se NON già presente. La url può anche essere solo la location.search e può essere vuota
	yada.addUrlParameterIfMissing = function(url, param, value, addQuestionMark) {
		if (yada.hasUrlParameter(url, param)) {
			return url;
		}
		return yada.addUrlParameter(url, param, value, addQuestionMark);
	};
	
	// Cambia il valore di un parametro di request, ritornando la url nuova
	// Adattato da http://stackoverflow.com/questions/5413899/search-and-replace-specific-query-string-parameter-value-in-javascript
	yada.updateUrlParameter = function(url, param, value) {
	 var regex = new RegExp("([?|&]" + param + "=)[^\&]+");
	 return url.replace(regex, '$1' + value);
	};
	
	/**
	 * Returns an url parameter when found, null when not found or empty
	 * Adapted from http://stackoverflow.com/questions/2090551/parse-query-string-in-javascript
	 * @param url can be a url, a query string or even part of it, or null; everything before "?" or "&" will be skipped.
	 */
	yada.getUrlParameter = function(url, varName){
		 var queryStr = url + '&';
		 var regex = new RegExp('.*?[&\\?]' + varName + '=(.*?)[&#].*');
		 var val = queryStr.replace(regex, "$1");
		 return val == queryStr ? null : unescape(val);
	};

	/**
	 * Returns an URLSearchParams object: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams.
	 * It can be iterated upon with:
	 * for (var nameValue of yada.getUrlParameters(url).entries()) {
	 * 		const name = nameValue[0];
	 * 		const value = nameValue[1];
	 * }
	 * @param url can be a url, a query string or even part of it, or null
	 */
	yada.getUrlParameters = function(url) {
		// Keep query string only
		url = yada.getAfter(url, "?", 0);
		url = yada.removeHash(url);
		return new URLSearchParams(url);
	};

	//Rimpiazza un singolo carattere
	yada.replaceAt = function(str, index, character) {
	 return str.substr(0, index) + character + str.substr(index+character.length);
	};

	// Ritorna l'url passata senza la query string
	yada.removeQuery = function(url) {
		return url.replace(/\?.*/, '');
	}

	//////////////////////
	/// Path Variables ///
	//////////////////////

	//Rimuove la pathVariable che segue il precedingSegment. Può anche non esistere.
	yada.removePathVariable = function(url, precedingSegment) {
		var regex = new RegExp("/" + precedingSegment + "(/[^/?]*)?"); // Match di /site/racconti, /site/racconti/, /site/racconti/123, /site/racconti/123?aaa
		return url.replace(regex, "/"+precedingSegment);
	}
	
	// Setta la pathVariable al valore indicato, aggiungendola se non esiste. La query string rimane inalterata.
	// Il precedingSegment può stare ovunque, e il valore che segue viene sempre settato al nuovo valore.
	// Però se il precedingSegment non è seguito da un valore, il metodo funziona solo se precedingSegment non è seguito da altri segmenti (vedi primo e ultimo esempio).
	// Esempi:
	// yada.setPathVariable('http://localhost:8080/site/racconti', 'racconti', 410); --> "http://localhost:8080/site/racconti/410"
	// yada.setPathVariable('http://localhost:8080/site/racconti/123', 'racconti', 410); --> "http://localhost:8080/site/racconti/410"
	// yada.setPathVariable('http://localhost:8080/site/racconti/567/belli/123', 'racconti', 410); --> "http://localhost:8080/site/racconti/410/belli/123"
	// yada.setPathVariable('http://localhost:8080/site/racconti/belli/123', 'racconti', 410); --> "http://localhost:8080/site/racconti/410/123" !!!!ERROR!!!!!
	yada.setPathVariable = function(url, precedingSegment, newValue) {
		var regex = new RegExp("/" + precedingSegment + "(/[^/?]*)?"); // Match di /site/racconti, /site/racconti/, /site/racconti/123, /site/racconti/123?aaa
		return url.replace(regex, "/"+precedingSegment+"/" + newValue);
	}
	
	// Ritorna true se il precedingSegment è seguito da un valore.
	// Quindi se url = "http://localhost:8080/site/racconti/410?page=2&size=36&sort=publishDate,DESC"
	// e precedingSegment = "racconti", il risultato è true; 
	// invece è false per url = "http://localhost:8080/site/racconti/" oppure url = "http://localhost:8080/site/register"
	yada.hasPathVariableWithValue = function(url, precedingSegment) {
		var value = yada.getPathVariable(url, precedingSegment);
		return value != null && value.length>0;
	}
	
	// Ritorna true se il precedingSegment esiste nell'url, anche se non seguito da un valore.
	// Quindi se url = "http://localhost:8080/site/racconti/410?page=2&size=36&sort=publishDate,DESC"
	// oppure url = "http://localhost:8080/site/racconti"
	// e precedingSegment = "racconti", il risultato è true; 
	// invece è false per url = "http://localhost:8080/site/register"
	yada.hasPathVariable = function(url, precedingSegment) {
		var value = yada.getPathVariable(url, precedingSegment);
		return value != null;
	}
	
	// Ritorna il valore che nell'url segue il precedingSegment.
	// Per esempio se url = "http://localhost:8080/site/racconti/410?page=2&size=36&sort=publishDate,DESC"
	// e precedingSegment = "racconti", il risultato è "410".
	// Se precedingSegment non è seguito da un valore, ritorna stringa vuota.
	// Se precedingSegment non c'è, ritorna null
	// The anchor is stripped
	yada.getPathVariable = function(url, precedingSegment) {
		var segments = yada.removeQuery(yada.removeHash(url)).split('/');
		var found=false;
		for (var i=1; i0) {
			var segments = hashString.split(';'); // ['story=132', 'command=message']
			for (var i = 0; i < segments.length; i++) {
				var parts = segments[i].split('='); // ['story', '132']
				result[parts[0]]=parts[1];
			}
		}
		return result;
	}
	
	/**
	 * Convert a location hash value to a map. For example, if propertyList=['id', 'name'] and windowLocationHash='#123-joe' and separator='-',
	 * the result is {'id':'123', 'name':'joe'}
	 * When there are more properties than values, the extra properties are set to the empty string.
	 * When there are more values than properties, the extra values are ignored.
	 * @param propertyList an array of map keys
	 * @param windowLocationHash the value of location.hash (starts with #)
	 * @param separator the separator for the values in the hash
	 * @return an object where to each property in the list corresponds a value from the hash
	 */
	yada.hashPathToMap = function(propertyList, windowLocationHash, separator) {
		var result = {};
		var segments = [];
		var hashString = yada.getHashValue(windowLocationHash); // 834753/myslug
		if (hashString!=null && hashString.length>0) {
			segments = hashString.split(separator);
		}
		for (var i = 0; i < propertyList.length; i++) {
			var name = propertyList[i];
			if (i0;
	}

	/**
	 * Converts a sentence to title case: each first letter of a word is uppercase, the rest lowercase.
	 * It will also convert "u.s.a" to "U.S.A" and "jim-joe" to "Jim-Joe"
	 * Adapted from https://stackoverflow.com/a/196991/587641
	 */
	yada.titleCase = function(sentence) {
		 return sentence.replace(
			 /\w[^\s-.]*/g, // The regex means "word char up to a space or - or dot": https://www.w3schools.com/jsref/jsref_obj_regexp.asp
			 	function(txt) {
	                return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
	            }
	     );
	}

	/**
	 * Replaces a template like "My name is ${name}" with its value. The value can be a string or a number or an array of strings/numbers.
	 * @param template the template string
	 * @param replacements an object whose attributes have to be searched and replaced in the string, e.g. replacements.name="Joe"
	 * @returns
	 */
	 yada.templateReplace = function(template, replacements) {
		for (name in replacements) {
			if (name!=null) {
				var placeholder = '\\$\\{'+name+'\\}';
				var value = replacements[name];
				if (typeof value != 'object') {
					template = template.replace(new RegExp(placeholder, 'g'), value);
				} else {
					for (var i=0; i=0) {
			return str.substring(pos+toFind.length);
		}
		return str;
	}
	
	/**
	 * Split a comma-separated string into an array. Commas can be followed by spaces.
	 * If the input is null, return an empty array. If the value is numeric, return an array with the value converted to string
	 */
	yada.listToArray = function(str) {
		if (str==null) {
			return [];
		}
		if (typeof str != "string") {
			return [str.toString()];
		}
		return str.split(/, */);
	}
	
	// Ritorna true se str contiene toFind
	yada.stringContains = function(str, toFind) {
		return str!=null && typeof str=="string" && str.indexOf(toFind) >= 0;
	}
	
	// Returns the last element of a delimiter-separated list of elements in a string.
	// Spaces around the delimiter are ignored.
	// Examples: 
	// yada.getLast("a, b, c", ",") --> "c"
	// yada.getLast("a", ",") --> "a"
	// yada.getLast("", ",") --> ""
	yada.getLast = function(source, separatorChar) {
		var regexp = new RegExp("\\s*" + separatorChar + "\\s*");
		return source.split(regexp).pop();
	}
	
	// Ritorna true se str inizia con prefix
	// http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string
	yada.startsWith = function(str, prefix) {
		return str!=null && typeof str=="string" && str.lastIndexOf(prefix, 0) === 0;
	}

	/**
	 * Returns true if the string ends with the suffix
	 * http://stackoverflow.com/a/2548133/587641
	 */
	yada.endsWith = function(str, suffix) {
		return str!=null && typeof str=="string" && str.substr(-suffix.length) === suffix;
	}
	
	/**
	 * Returns the smallest portion of the string inside the prefix and suffix, if found, otherwise return the empty string.
	 */
	yada.extract = function(str, prefix, suffix) {
		const regex = new RegExp(escapeRegExp(prefix) + "(.*?)" + escapeRegExp(suffix));
		const matched = regex.exec(str);
		if (matched!=null && matched.length>1 && matched[1]!=null) {
			return matched[1];
		}
		return "";
	}
	
	function escapeRegExp(string) {
		// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
  		return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
	}
	
	/**
	 * Increment a numeric value.
	 * @param elementSelector the jquery selector that identifies the element(s) to increment 
	 */
	yada.increment = function(elementSelector) {
		return yada.numberAdd(elementSelector, 1);
	}
	
	/**
	 * Aggiunge al valore numerico di un elemento una quantità algebrica eventualmente limitandola a un minimo o massimo
	 * Esempio: yada.numberAdd('#totMessaggiCounter', -1, 0, true);
	 * @param elementSelector id dell'elemento incluso l'hash, e.g. #myCounter, oppure un suo selector qualsiasi come ".myElement > div"
	 * @param toAdd valore numerico da aggiungere algebricamente, può essere positivo o negativo
	 * @param limit valore opzionale oltre il quale il valore non deve passare
	 * @param removeAtLimit se true, quando il limit viene raggiunto, il valore viene rimosso totalmente invece che inserito
	 * @return the number of elements that have been modified (0 if no element found or limit reached)
	 */
	yada.numberAdd = function(elementSelector, toAdd, limit, removeAtLimit) {
		var result = 0;
		$(elementSelector).each(function(){
			var element = $(this);
			var text = element.text();
			var val = parseInt(text, 10);
			if (isNaN(val)) {
				val=0;
			}
			val = val + toAdd;
			var remove=false;
			if (limit != null) {
				if ((toAdd>0 && val>limit) || (toAdd<0 && val
	 **/
	yada.makeCustomPopover = function($element) {
		if (typeof bootstrap != 'object' || typeof bootstrap.Popover != 'function' ) {
			return yada.handleCustomPopoverB3($element); // Bootstrap 3
		}
		$element = $element || $('body');
		$("[data-yadaPopover]", $element).not('.yadaPopovered').each(function(){
			const trigger = this;
			const $trigger = $(trigger);
			$trigger.addClass('yadaPopovered');
			const htmlIdWithHash = yada.getIdWithHash(trigger, "data-yadaPopover"); // id of the HTML for the popover
			if (htmlIdWithHash!=null && htmlIdWithHash!="" && htmlIdWithHash!="#") {
				//const htmlId = yada.getHashValue(htmlIdWithHash);
				const contentInstanceId = yada.getRandomId("yada");
				// Using "content" because the HTML is supposed to be in a