Skip to main content

Utility Functions

function encodeString(str, encode) {
return encode ? encodeURI(str).replace(/\'/g, '%27').replace(/\;/g, '%3B').replace(/\_/g, '%5F').replace(/\./g, '%2E').replace(/\!/g, '%21').replace(/\~/g, '%7E').replace(/\*/g, '%2A').replace(/\(/g, '%28').replace(/\)/g, '%29') : decodeURI(str);
}

/**
* Called to determine if a particular value is present
* in the supplied filter listing.
* @param {string} targetValue - value to search for in list
* @param {Array} list - list to search through for value
* @returns {boolean} - whether or not match was found
*/
function valueInList(targetValue, list, rule = self.filterRules.Match) {
var matchFound = false;
if (targetValue && angular.isArray(list)) {
list.forEach(function (listItem) {
let item = typeof listItem === 'string' ? listItem.toLowerCase() : listItem;




targetValue = typeof targetValue === 'string' ? targetValue.toLowerCase() : targetValue;




if (rule === self.filterRules.Contains && targetValue.indexOf(item) > -1) {
matchFound = true;
} else if (targetValue === item) {
matchFound = true;
}
});
}
return matchFound;
}

/**
* Alphabetical sort
* @param {string} item1 - first item
* @param {string} item2 - second item
* @returns {number} - 0, 1, or -1
*/
self.alphabeticSort = function (item1, item2) {
//because Javascript sorts by Unicode, convert strings in lowerCase before comparing
var i1 = item1, i2 = item2;
if (typeof i1 === 'string' && typeof i2 === 'string') {
i1 = i1.toLowerCase();
i2 = i2.toLowerCase();
}
//if the values are undefined, we treat them as if they were empty string, which would result in proper sort (otherwise JS does not sort undefined properly)
else if (i1 === undefined) {
i1 = '';
}
else if (i2 === undefined) {
i2 = '';
}
if (i1 < i2) return -1;
if (i1 > i2) return 1;
return 0;
};

/**
* Numeric sort.
* @param {number} item1 - first item
* @param {number} item2 - second item
* @param {string} direction - sort direction string
* @returns {number} - difference between items
*/
self.numericSort = function (item1, item2, direction) {
if (direction === self.sortDirections.Desc) {
return item2 - item1;
}




return item1 - item2;
};

/**
* Alphanumeric sort
* @param {string} item1 - first item
* @param {string} item2 - second item
* @returns {number} - 0, 1, or -1
*/
self.alphanumericSort = function (item1, item2) {
if (item1 && item2) {
return item1.localeCompare(item2, undefined, { numeric: true, sensitivity: 'base' });
}




return self.alphabeticSort(item1, item2);
};

/**
* Copy the contents of an Array
* @param {Array} source - The original array to copy
* @param {boolean} useAngularCopy - Whether or not to use angular.copy or angular.extend when copying the source Array
* @returns {Array} A copy of the original source array
*/
self.copyArray = function (source, useAngularCopy) {
if (!Array.isArray(source)) {
return source;
}

var copy = [];
source.forEach(function (item) {
copy.push(useAngularCopy ? angular.copy(item) : angular.extend({}, item));
});

return copy;
};


/**
* Call to create a universally unique identifier based on the current time.
* Site Ref: https://www.w3resource.com/javascript-exercises/javascript-math-exercise-23.php
* @returns {string}
*/
self.createUuid = function () {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});

return uuid;
};


/**
* Debounce function by David Walsh - https://davidwalsh.name/javascript-debounce-function
* @param {Function} func The function to execute
* @param {number} wait time
* @param {Boolean} immediate When true, trigger the function on the leading edge, instead of the trailing
* @returns {Function} inner debounce function
*/
self.debounce = function (func, wait, immediate) {
var timeout;
return function () {
var context = this, args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};


/**
* Tests whether document.execCommand('copy'), which provides
* 'copy to clipboard' functionality, is supported by the browser.
* @returns {boolean} Returns true if the document.execCommand('copy') function is supported
*/
self.copyToClipboardSupported = function () {
return angular.isFunction(document.queryCommandSupported) && !!document.queryCommandSupported('copy');
};


/**
* Copy a specific string to the clipboard
* @param {string} valueToCopy - The string to place on the clipboard
* @returns {Object} An object, with successful and message properties, that indicates whether the copy to clipboard action was successful
*/
self.copyToClipboard = function (valueToCopy) {
if (!self.copyToClipboardSupported()) {
return { successful: false, message: kLocalizeFilter('copyToClipboardNotSupported') };
}

var result = undefined,
successMsg = kLocalizeFilter('copyToClipboardSuccess', { targetValue: valueToCopy }),
failMsg = kLocalizeFilter('copyToClipboardFail', { targetValue: valueToCopy });

//Create an input to hold the string that is intended for the clipboard.
var copyEl = document.createElement('input');
//Position the input off-screen so it's not seen.
copyEl.style.cssText = 'position: absolute; left: -1000px; top: -1000px';
//Set the text and select it
copyEl.value = valueToCopy;
document.body.appendChild(copyEl);

if (navigator.userAgent.match(/ipad|ipod|iphone/i)) {
//iOS-specific behavior since simply using .select() didn't work
copyEl.contentEditable = true;
copyEl.readOnly = true; //Keeps the native keyboard from flashing
var range = document.createRange();
range.selectNodeContents(copyEl);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
copyEl.setSelectionRange(0, 9999999); //Not sure of a correct upper limit but this appears to work
} else {
//This works with IE, Chrome (Win, Mac, Android), Safari (Mac)
copyEl.select();
}

try {
//Copy the selected text
var successfulCopy = document.execCommand('copy');
result = successfulCopy ? { successful: true, message: successMsg } : { successful: false, message: failMsg };
} catch (err) {
result = { successful: false, message: failMsg };
}

//Remove the input since it is no longer needed and return the result.
document.body.removeChild(copyEl);
return result;
};

/**
* Recursively walk an object/array, find and accumulate any instances of 'property' into a single flat array
* no early exit, will walk entire object/list, assuming 'property' is not limited in where it can appear
* avoid using on very large object trees.
* example:

parent = {
prop1: {
prop2: [
{ prop3: 'a' },
{ prop3: 'b' },
{ prop3: 'c' }
]
},
prop3: ['d', 'e', 'f']
}

result = gather(parent, 'prop3')
result: ['a', 'b', 'c', 'd', 'e', 'f']

* @param {object|array} parent, the object or array to traverse
* @param {string} property, the name of the property to scan for and accumulate
* @param {array|string} skiplist, optional name of a single property, or array of multiple property names to skip [not recurse]
* use to avoid recursing down objects with circular references,
* like a Resource object containing $promises;
* { Resource -> $promise -> $$state -> value -> Resource ... }
* pass '$promise' in the skiplist
* @returns {array} array of values of any instances of property found within the parent
*/
self.gather = function (parent, property, skiplist) {
var found = [];
skiplist = Array.isArray(skiplist) ? skiplist : skiplist ? [skiplist] : [];


//iterate over items in an array, or properties on an object
if (Array.isArray(parent)) {
parent.forEach(c => {
found = found.concat(self.gather(c, property, skiplist));
});
} else if (typeof (parent) === 'object') {
Object.keys(parent).forEach(key => {
if (skiplist.indexOf(key) >= 0) {
//do nothing
} else if (key === property) {
//if the target property is already an array, concat it with the accumulation array
//otherwise push it onto the accumulation array
if (Array.isArray(parent[key])) {
found = found.concat(parent[key]);
} else {
found.push(parent[key]);
}
} else {
//recurse down into this object
found = found.concat(self.gather(parent[key], property, skiplist));
}
});
}
return found;
};


//from an array, return an object (dictionary) with no duplicate values
// the key of the returning dictionary will be set to the fieldUsedForKey parameter
self.arrayToDict = function (theArray, fieldUsedForKey) {
var dict = {};
if (Array.isArray(theArray)) {
theArray.forEach(item => {
if (!dict[item[fieldUsedForKey]]) {
dict[item[fieldUsedForKey]] = item;
}
});
}
return dict;
};

//from a dictionary, return an Array
self.dictToArray = function (theDict) {
var resultArray = [];
for (var key in theDict) {
resultArray.push(theDict[key]);
}
return resultArray;
};


//takes a date and offsets it by its time zone offset so that when JSON.stringify is ran on it (like before sending it to the API)
//it converts it back to the actual time set for that date
self.convertDateToOffsetTimeZone = function (date) {
//when executing the code below, we set a custom property (timezoneFixApplied) to true so that if by any chance, we are trying to offset that
//date/time timezone again, we don't do it if that property is already set to true, otherwise it would offset it twice, which would mess up the date
if (date && !date.timezoneFixApplied && typeof date.getDate === 'function') {
date.timezoneFixApplied = true;
//this will return the equivalent of date.getTime() but the "setMinutes" affect the date object itself, the return is just so that we can chain this function if needed
return date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
}
return date;
};


//from a dotted date format, assuming d.mmm.yyyy (where d is from 1 to 31 and mmm is the three letters) OR dd.mmm.yyyy (where dd is leading zero from 01 to 31 and mmm is the three letters)
//convert this date into a valid date Object and return it
self.convertInvalidDate = function (stringDate) {
if (typeof stringDate !== 'string') {
//stringDate is not a string, can't do anything, leave
return stringDate;
}


//using a simple RegEx, we are able to know if the date string is in the expected format, case insensitive
var m = stringDate.match(/(\d+)\.(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\.(\d+)/i);
if (!m || m.length !== 4) {
//if there is no match, or the length of the groups found is smaller than what we expect, leave
return stringDate;
}

//with the results in our RegEx match, we should be able to get a valid date
var monthsLowerCase = moment.monthsShort().map(m => m.toLowerCase()),
monthIndex = monthsLowerCase.indexOf(m[2].toLowerCase()); //this is good enough as the RegEx above tells us already if the month is valid, we simply convert it to lower case here and find its index in the list
try {
var newDate = new Date(m[3], monthIndex, m[1]);
//lastly, as another safe check, make sure the date is valid and if so return it
if (Object.prototype.toString.call(newDate) === '[object Date]' && !isNaN(newDate.getTime())) {
return newDate;
}
//if after all that, it is still invalid, simply return the stringDate as the format is simply not the one expected here
return stringDate;
}
catch (e) {
console.info('utilService.convertDottedDate - Could not convert ' + stringDate + ' into a valid date that the browser will recognize.', e);
}


//if we get here, something is probably not right in the expected format
return stringDate;
};


//sort an array by a give sortByFieldName by also NOT taking care of case sensitivity (default array JS sort will sort strings in this order: number, upper case, lower case)
self.caseInsensitiveSort = function (array, sortByFieldName) {
if (array && array.length && typeof sortByFieldName === 'string') {
array.sort(function (a, b) {
var a1 = a[sortByFieldName], b1 = b[sortByFieldName];
if (typeof a1 === 'string') {
a1 = a1.toLowerCase();
}
if (typeof b1 === 'string') {
b1 = b1.toLowerCase();
}
return a1 < b1 ? -1 : (a1 > b1 ? 1 : 0);
});
}


return array;
};