import "./utils.css";
/**
* A collection of useful tools.
*
* @author rkoppe <roland.koppe@awi.de>
* @author rhess <robin.hess@awi.de>
*/
/**
* Method to check if a value is null or undefined
* @memberof vef.utils
*
* @param {*} value
* @returns {boolean}
*/
export function isNullorUndefined(value) {
return (value === null) || (value === undefined);
}
/**
* replaces URLs starting with http or https and replaces them
* with a link tag leading to their location
*
* @memberof vef.utils
*
* @param {string} text
* @returns {string} text with added link tags
*/
export function createLinks(text) {
// return original text if it contains link tags
if (text.includes('<a') && text.includes('a>') && text.includes("href=")) return text;
const URL_PATTERN = /(https?:\/\/[a-z0-9-]{2,}(\.[a-z0-9-]{2,})+(\/[a-z0-9-_\.]+)*\/?[a-z0-9-_\\?&@\.=:~\/]+)/gi;
return text.replace(URL_PATTERN, `<a href="$1" target="_blank">$1</a>`);
}
/**
* Creates the elements defined by the given HTML string and returns
* these elements as array or the element, if it is only one.
*
* @memberof vef.utils
*
* @param {string} html
*/
export function createElement(html) {
const template = document.createElement('template');
template.innerHTML = html;
const fragment = template.content;
if (fragment.children.length > 1) {
return fragment.children;
} else {
return fragment.children[0];
}
}
/**
* Returns true if fn is a function.
*
* @memberof vef.utils
*
* @param {object} fn
*/
export function isFunction(fn) {
return fn && Object.prototype.toString.call(fn) === '[object Function]';
}
/**
* Get query params from current document.location or optional URI
*
* @memberof vef.utils
*
* @param {string} uri (optional)
* @returns {object} URI params
*/
export function getQueryParams(uri) {
const regex = /[(\?|\&|\#)]([^=]+)\=([^\&#]+)/g;
const obj = {};
let match;
if (uri === undefined) uri = document.location.href;
uri = uri.replace(/&&/g, '&');
while (!!(match = regex.exec(uri))) {
if (!!obj[match[1]]) {
obj[match[1]].push(decodeURIComponent(match[2]));
} else {
obj[match[1]] = [decodeURIComponent(match[2])];
}
}
return obj;
}
/**
* Create a query params string from a given object
*
* @memberof vef.utils
*
* @param {object} params
* @returns {string} query string
*/
export function setQueryParams(params) {
let query = "";
const add = (k, v) => {
let segment = encodeURIComponent(k) + "=" + encodeURIComponent(v);
query += (query.length) ? ("&" + segment) : segment;
}
for (let k in params) {
if (Array.isArray(params[k])) {
for (let i = 0; i < params[k].length; ++i) {
add(k, params[k][i]);
}
} else {
add(k, params[k]);
}
}
return (query.length) ? ("?" + query) : "";
}
/**
* Download a Blob as a file
* Triggers the browsers "save file" dialouge
*
* @memberof vef.utils
*
* @param {Blob} blob
* @param {string} fileName
*/
export function saveAsFile(blob, fileName) {
const a = document.createElement('a');
a.download = fileName;
a.rel = 'noopener';
a.href = URL.createObjectURL(blob);
a.dispatchEvent(new MouseEvent("click"));
// Invalidate Object URL after 60 Seconds
setTimeout(() => URL.revokeObjectURL(a.href), 60000);
}
/**
* iterates through the object and sets
* the mathing CSS properties.
*
* The prefix "--" will automaticalle be added
* to the property names
*
* @memberof vef.utils
*
* @param {object} properties
*/
export function setCSSProperties(properties) {
const root = document.documentElement;
for (let name in properties) {
if (typeof properties[name] == "string") {
root.style.setProperty('--' + name, properties[name]);
}
}
}
/**
* Method for capitalizing a string
*
* @memberof vef.utils
*
* @param {string} str
*/
export function capitalizeString(str) {
str = str.replaceAll("_", " ");
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
/**
* round a number to the given decimals
*
* @memberof vef.utils
*
* @param {number} num
* @param {number} decimals
*/
export function roundNumber(num, decimals, trailingZeros) {
// Used approach from https://www.htmlgoodies.com/javascript/round-in-javascript/ (2022-09-26)
decimals = decimals || 0;
const p = Math.pow(10, decimals);
const n = (num * p) * (1 + Number.EPSILON);
num = Math.round(n) / p;
return (trailingZeros) ? num.toFixed(decimals) : num;
}
/**
* Helper method to only execute a callback after the event has not
* been dispatched for the given amount of milliseconds
*
* @memberof vef.utils
*
* @param {function} callback
* @param {number} milliseconds
*
* @returns {function} add this as event listener
*/
export function debounceCallback(callback, milliseconds) {
let timeout = null;
return function (event) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
callback(event);
}, milliseconds);
}
}
/**
* Display a temporary message banner on top of the page
* @memberof vef.utils
*/
export function displayMessage(message, duration, type = "message") {
duration = (Number.isFinite(duration)) ? duration : 5000;
let bannerContainer = document.getElementsByClassName("vef-message-banner")[0];
if (!bannerContainer) {
bannerContainer = document.createElement("div");
bannerContainer.classList.add("vef-message-banner", "vef-ui", `level-${type}`);
}
const bannerMessage = document.createElement("div");
bannerMessage.classList.add("message-banner-inner");
bannerMessage.innerHTML = `
<div class="message-content">${message}</div>
<i class="fas fa-times btn-close"></i>
`;
const hide = () => {
if (bannerMessage.classList.contains("hiding"))
return;
bannerMessage.classList.add("hiding");
setTimeout(() => {
bannerMessage.remove();
if (bannerContainer.childElementCount == 0)
bannerContainer.remove()
}, 500);
}
bannerMessage.querySelector(".btn-close").addEventListener("click", hide);
setTimeout(hide, duration);
bannerContainer.appendChild(bannerMessage);
document.body.appendChild(bannerContainer);
}
/**
* Method to remove script tags to prevent some forms of XSS attacks.
* This method might note be safe to cover all possible XSS attacks.
*
* Inspired by: https://gomakethings.com/how-to-sanitize-html-strings-with-vanilla-js-to-reduce-your-risk-of-xss-attacks/ (2022-10-29)
*
* @memberof vef.utils
* @param {string} html
* @param {boolean} returnAsString
* @returns {HTMLElement} parsed element
*/
export function sanitizeHTML(html, returnAsString) {
// parse string to DOM
const parser = new DOMParser();
const element = parser.parseFromString(html, 'text/html').body || document.createElement('body');
// remove scripts
const scripts = element.querySelectorAll('script');
for (let i = 0; i < scripts.length; ++i) {
scripts[i].remove();
}
// recursive cleanup of malicious attributes in every node
const cleanNode = node => {
// remove dangerous attributes
const attributes = node.attributes;
for (let i = 0; i < attributes.length; ++i) {
const name = attributes[i].name;
if (name.startsWith("on")) {
node.removeAttribute(name);
} else if (['src', 'href', 'xlink:href'].includes(name)) {
const value = attributes[i].value.replace(/\s+/g, '').toLowerCase();
if (value.includes('javascript:') || value.includes('data:text/html')) {
node.removeAttribute(name);
}
}
}
// recursively iterate through child nodes
const children = node.children;
for (let i = 0; i < children.length; ++i) {
cleanNode(children[i]);
}
}
cleanNode(element);
return (returnAsString) ? element.innerHTML : element.childNodes;
}
/**
* Function to get a hash for a list of strings.
* If the list argument contains only one string, the hash calculation is applied directly to the string without using the separator.
* For multiple strings in the list, the strings are merged into one string via the separator, and the hash is calculated.
*
* Inspired by: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js (2023-02-01, public domain license)
*
* @memberof vef.utils
* @param {Array.<String>} list list containing one or multiple strings
* @param {string} separator delimiter to join strings in list
* @param {Number} seed parameter to randomize the hash function
* @returns {string} hash
*/
export function getHash(list, separator = '|', seed = 0) {
const str = list.join(separator)
const cyrb53 = (str, seed) => {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
return cyrb53(str)
}
/**
* Generates an identifier based on time and a random number
*
* @memberof vef.utils
* @returns {string}
*/
export function generateUniqueId() {
return new Date().getTime().toString() + Math.floor(Math.random() * 1000000).toString();
}