/*
* Your installation or use of this SugarCRM file is subject to the applicable
* terms available at
* http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
* If you do not agree to all of the applicable terms or do not have the
* authority to bind the entity as an authorized representative, then do not
* install or use this SugarCRM file.
*
* Copyright (C) SugarCRM Inc. All rights reserved.
*/
const User = require('core/user');
const Language = require('core/language');
/**
* The Utils module provides several utility methods, such as those for
* number formatting.
*
* @module Utils/Utils
*/
var _doWhenStack = [];
var _doWhenLocked = false;
var _doWhenInterval = false;
var _doWhenretryCount = 0;
var _startDoWhenInterval = function () {
if (!_doWhenInterval) {
_doWhenInterval = window.setInterval(Utils._doWhenCheck, 50);
}
};
/**
* For cookie handling.
* @class
* @name Utils/Utils.Cookie
*/
const Cookie = {
/**
* Sets a cookie.
*
* @param {string} cName Cookie name.
* @param {string} value Cookie value.
* @param {number} exdays Days until expiration.
* @param {string} path Cookie path.
* @memberOf Utils/Utils.Cookie
*/
setCookie: function(cName, value, exdays, path) {
var exdate = new Date(), c_value;
exdate.setDate(exdate.getDate() + exdays);
c_value = encodeURIComponent(value);
if (exdays) {
c_value += "; expires=" + exdate.toUTCString();
}
if (path) {
c_value += "; path=" + path;
}
document.cookie = cName + "=" + c_value;
},
/**
* Gets a cookie.
*
* @param {string} cName Cookie name.
* @return {string|undefined} The cookie value associated with `cName`, or
* `undefined` if not found.
* @memberOf Utils/Utils.Cookie
*/
getCookie: function(cName) {
var i, x, y, ARRcookies = document.cookie.split(";");
for (i = 0; i < ARRcookies.length; i++) {
x = ARRcookies[i].substr(0, ARRcookies[i].indexOf("="));
y = ARRcookies[i].substr(ARRcookies[i].indexOf("=") + 1);
x = x.replace(/^\s+|\s+$/g, "");
if (x === cName) {
return decodeURIComponent(y);
}
}
}
};
/**
* @alias module:Utils/Utils
*/
const Utils = {
/**
* Capitalizes a string.
*
* @param {string} s The string to capitalize.
* @return {string} `s` capitalized, or an empty string if `s` is
* `undefined` or `null`.
*/
capitalize: s => s ? (s.charAt(0).toUpperCase() + (s.length > 1 ? s.substr(1) : '')) : '',
/**
* Capitalizes a hyphenated string and removes the hyphens.
* The first letter, and all letters after a hyphen, are capitalized,
* so `"my-string"` becomes `"MyString"`.
*
* @param {string} s The string to capitalize.
* @return {string} `s` capitalized or an empty string if `s` is
* `undefined` or `null`.
*/
capitalizeHyphenated: function(s) {
return this._classify(s, '-');
},
/**
* Capitalizes an underscored string and removes the underscores.
* The first letter, and all letters after an underscore, are capitalized,
* so `"my_string"` becomes `"MyString"`.
*
* @param {string} s The string to capitalize.
* @return {string} `s` capitalized or an empty string if `s` is
* `undefined` or `null`.
*/
classify: function(s) {
return this._classify(s, '_');
},
/**
* Capitalizes a delimited string.
*
* `"my_string"` becomes `"MyString"`.
* @param {string} s The string to capitalize.
* @param {string} [delimiter='_'] Delimiter string.
* @return {string} Capitalized string or an empty string if `s` is
* `undefined` or `null`.
* @private
*/
_classify: function(s, delimiter) {
var self = this, result = '';
delimiter = delimiter || '_';
if (!s || s.lastIndexOf(delimiter) === -1) {
result = self.capitalize(s);
} else {
var words = s.split(delimiter);
_.each(words, function(word) {
result += self.capitalize(word);
});
}
return result;
},
/**
* Extends a Class based on the given controller.
*
* If the controller has an `extendsFrom` property, it will be used to
* define its parent class. It should be defined as a string in order
* for the system to detect if there is any customization on that
* parent (normally prefixed with `Custom` like `Custom<ClassName>`).
*
* If the parent class defined in the `extendsFrom` property doesn't
* exist or the controller isn't specifying one, it will fallback to
* the supplied `defaultBase` param which has it's own set of fallback
* strategy defined by the components that call this method. See
* {@link View.ViewManager} and {@link Data.DataManager}.
*
* @param {Object} cache Object cache to add controller to.
* @param {Object} defaultBase Class to be extended from if no override
* (`extendsFrom`) is defined in the controller.
* @param {string} className Class name to be used for new Class.
* @param {Object} controller Properties for new Class.
* @param {string} platformNamespace Platform name.
* @return {Object} The new extended class.
*/
extendClass: function(cache, defaultBase, className, controller, platformNamespace) {
var klass;
if (!_.isObject(controller)) {
// nothing to extend from, use default
klass = cache[className] = defaultBase;
return klass;
}
if (_.isObject(controller.extendsFrom)) {
//should be avoided due to chain breakage - aka custom fallback
klass = cache[className] = controller.extendsFrom.extend(controller);
return klass;
}
if (!controller.extendsFrom) {
// follow the default fallback flow
klass = cache[className] = defaultBase.extend(controller);
return klass;
}
// try to find the base using the name and following the fallback flow
var base = cache[platformNamespace + 'Custom' + controller.extendsFrom] ||
cache[platformNamespace + controller.extendsFrom] ||
cache['Custom' + controller.extendsFrom] ||
cache[controller.extendsFrom];
if (!base) {
SUGAR.App.logger.warn('The "' + controller.extendsFrom + '" component was not found to be used as ' +
'the "' + className + '"\'s parent. Please update your code to point to an existing class.');
base = defaultBase;
}
klass = cache[className] = base.extend(controller);
return klass;
},
/**
* Formats a number.
*
* @param {number} value Number to be formatted eg 2.134.
* @param {number} round number of digits to right of decimal to round at.
* @param {number} precision number of digits to right of decimal to take
* precision at.
* @param {string} numberGroupSeparator Character separator for number
* groups of 3 digits to the left of the decimal to add.
* @param {string} decimalSeparator Character to replace decimal in arg
* number with.
* @param {boolean} toStringOnly Flag for integer type field if `true`,
* convert integer to string value, else rounds it.
* @return {string} Formatted number string OR original value if it is not
* a number.
*/
formatNumber: function(value, round, precision, numberGroupSeparator, decimalSeparator, toStringOnly) {
round = round || precision;
var original = value;
if (_.isNaN(value) || !_.isFinite(value)) {
return original;
}
if (_.isString(value)) {
value = parseFloat(value, 10);
if(_.isNaN(value)){
return original;
}
}
// Return original value if it is not a number
if(!_.isNumber(value)) {
if(!_.isNull(value) && !_.isUndefined(value)) {
// invalid variable type
SUGAR.App.logger.warn('formatNumber: invalid variable type ('+typeof(original)+')');
}
return original;
}
// make sure that the precision variable is an integer and not a string
// Big.js doesn't like it as a string
if (_.isString(precision)) {
precision = parseInt(precision);
}
// stop rounding if integer type field
value = toStringOnly === true ? value.toString() : Big(value).toFixed(precision);
return (_.isString(numberGroupSeparator) && _.isString(decimalSeparator)) ?
this.addNumberSeparators(value, numberGroupSeparator, decimalSeparator) : value;
},
/**
* Formats a number according to the current user locale.
*
* @param {number} value Value to format.
* @return {string} Formatted number.
*/
formatNumberLocale:function (value) {
// use user locale, or decent defaults otherwise
return this.formatNumber(
value,
User.getPreference('decimal_precision') || 2,
User.getPreference('decimal_precision') || 2,
User.getPreference('number_grouping_separator') || ',',
User.getPreference('decimal_separator') || '.'
);
},
/**
* Formats a full name with the provided locale format.
*
* @param {Object} params Name property values.
* @param {string} params.first_name First name.
* @param {string} params.last_name Last name.
* @param {string} params.salutation Salutation.
* @param {string} format Locale format (i.e. [f l s], [s l, f]).
* @return {string} Formatted string.
*/
formatName: function(params, format) {
return format.replace(/(f)|(l)|(s)/g, function(str, firstName, lastName, salutation) {
if (firstName) {
return params.first_name || '';
}
if (lastName) {
return params.last_name || '';
}
if (salutation && (params.last_name || params.first_name)) {
return params.salutation || '';
}
return '';
})
//Remove comma when last name is empty
.replace(/^( )?,/g, '')
//Remove comma when last name is provided but first name is empty
.replace(/, $/g, '')
//Remove extra spaces when middle part is missing
.replace(/ /g, ' ')
//trim spaces
.trim();
},
/**
* Formats a record's name (ie. full name) according to the name format
* passed in parameters or defined in the user preferences.
*
* Format you can pass:
* ```
* 'f l s' will output: `FirstName LastName Salutation`,
* 's l, f' will output: `Salutation LastName FirstName`.
* ```
*
* The module defines a `nameFormat` object that maps a letter to a
* field.
* ```
* {
* s: 'salutation',
* f: 'first_name',
* l: 'last_name'
* }
* ```
*
* @param {string} module The module name the record belongs to.
* @param {Object} data The record attributes.
* @param {string} [format=User.getPreference('default_locale_name_format')]
* The format definition.
* @return {string} The formatted full name string.
*/
formatNameModel: function(module, data, format) {
format = format || User.getPreference('default_locale_name_format');
data = data || {};
var metadata = SUGAR.App.metadata.getModule(module) || {fields: {}};
var formatMap = metadata.nameFormat || {};
return _.reduce(format.split(''), function(formattedString, letter) {
// only letters a-z may be significant in the format,
// everything else is translated verbatim
if (letter < 'a' || letter > 'z') {
return formattedString + letter;
}
if (!formatMap[letter]) {
return formattedString;
}
var enumLabel,
isEnum = metadata.fields[formatMap[letter]] &&
metadata.fields[formatMap[letter]].type === 'enum' &&
!_.isEmpty(metadata.fields[formatMap[letter]].options);
if (isEnum) {
var list = Language.getAppListStrings(metadata.fields[formatMap[letter]].options);
enumLabel = list[data[formatMap[letter]]] || data[formatMap[letter]] || '';
}
return formattedString + (enumLabel || data[formatMap[letter]] || '');
}, '')
//Remove leading comma - i.e. ", John"
.replace(/^( )?,/g, '')
//Remove trailing comma - i.e., "John, ", "John, ", etc.
.replace(/, +$/g, '')
//Remove extra spaces when middle part is missing
.replace(/ /g, ' ')
//trim spaces
.trim();
},
/**
* Formats a full name according to the user's locale format.
*
* @param {Object} params Name property values.
* @param {string} params.first_name First name.
* @param {string} params.last_name Last name.
* @param {string} params.salutation Salutation.
* @return {string} Formatted string.
*/
formatNameLocale: function(params) {
return this.formatName(params, User.getPreference('default_locale_name_format'));
},
/**
* Adds number separators to a number string.
*
* @param {string} numberString String of number to be modified of the
* format nn.nnn.
* @param {string} numberGroupSeparator Character separator for number
* groups of 3 digits to the left of the decimal to add.
* @param {string} decimalSeparator Character to replace decimal in arg
* number with.
* @return {string} `numberString` with the appropriate separators added.
*/
addNumberSeparators: function(numberString, numberGroupSeparator, decimalSeparator) {
var numberArray = numberString.split(".");
var regex = /(\d+)(\d{3})/;
while (numberGroupSeparator !== '' && regex.test(numberArray[0])) {
numberArray[0] = numberArray[0].toString().replace(regex, '$1' + numberGroupSeparator + '$2');
}
return numberArray[0] + (numberArray.length > 1 &&
numberArray[1] !== '' ? decimalSeparator + numberArray[1] : '');
},
/**
* Unformats number strings.
*
* @param {string} numberString The number string to unformat.
* @param {string} numberGroupSeparator The thousands separator.
* @param {string} decimalSeparator The string between number and decimals.
* @param {boolean} [toFloat=false] If `true`, convert string to float value.
* @return {string} Formatted number string.
*/
unformatNumberString: function(numberString, numberGroupSeparator, decimalSeparator, toFloat) {
toFloat = toFloat || false;
if (typeof numberGroupSeparator === 'undefined' || typeof decimalSeparator === 'undefined') {
return numberString;
}
// if number is not as string, make it a string
if (!_.isString(numberString)) {
if (_.isFinite(numberString)) {
// valid number, convert to string
numberString = numberString.toString();
} else {
// invalid value: null, undefined, NaN, etc.
// set to empty string
numberString = '';
}
}
// parse out number group separators
if (numberGroupSeparator !== '') {
var num_grp_sep_re = new RegExp('\\' + numberGroupSeparator, 'g');
numberString = numberString.replace(num_grp_sep_re, '');
}
// parse out decimal separators
numberString = numberString.replace(decimalSeparator, '.');
// remove any invalid chars
//numberString = numberString.replace(/[^0-9\.\+\-\%]/g, '');
// convert to float
if (numberString.length > 0 && toFloat) {
var float = parseFloat(numberString);
if (float == numberString) {
return float;
}
}
return numberString;
},
/**
* Unformats a number string based on the current user's locale.
*
* @param {string} value The number string to unformat.
* @param {boolean} [toFloat=false] If `true`, convert string to float
* value.
* @return {string} The formatted value.
*/
unformatNumberStringLocale: function(value, toFloat) {
return this.unformatNumberString(
value,
User.getPreference('number_grouping_separator') || ',',
User.getPreference('decimal_separator') || '.',
toFloat
);
},
/**
* Replaces tokens like {0}, {1}, etc. with the provided arguments.
*
* @param {string} format String to format.
* @param {string} args Arguments to replace.
* @return {string} Formatted string.
*/
formatString: function(format, args){
for(var idx in args){
format = format.replace('{'+idx+'}',args[idx]);
}
return format;
},
/**
* Escapes a given string for use in a JavaScript regex.
*
* @param {string} string The string to escape.
* @return {string} `string` escaped.
*/
regexEscape: function regexEscape(string) {
if( typeof regexEscape.specialRegExp == 'undefined' ) {
var specials = [
'/', '.', '*', '+', '?', '|',
'(', ')', '[', ']', '{', '}', '\\',
'-', ',', '^', '$', '#'
];
regexEscape.specialRegExp = new RegExp(
'(\\' + specials.join('|\\') + ')', 'g'
);
}
return string.replace(regexEscape.specialRegExp, '\\$1');
},
/**
* Generates and returns a UUID according to RFC 4122.
*
* @return {string} A UUID.
*/
generateUUID: function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
/*jshint -W016 */ // "Unexpected use of '|'.",
/*jshint -W116 */ // "Expected '===' and instead saw '=='."
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
/**
* @type {Utils/Utils.Cookie}
* @name cookie
*/
cookie: Cookie,
/**
* Checks if an email address is valid.
*
* Only performs a very basic validation because the complexity of the
* server-side regular expression is too great to mirror on the client,
* both in terms of maintenance and difficulty in porting to a different
* engine. Even if the light-weight validation passes, the server-side
* validation may fail.
*
* @param {string} address The email address to check.
* @return {boolean} `false` if this is definitely an invalid email
* address. A return value of `true` does not guarantee that this is a
* valid email address.
*/
isValidEmailAddress: function(address) {
return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@\S+$/.test(address);
},
/**
* Based on {@link https://yui.github.io/yui2/docs/yui_2.9.0_full/event/index.html#onavailable|YUI's onAvailable},
* but will use any boolean function instead of an ID. Once the given
* condition is met, the callback function will be executed.
*
* ```
* // Execute a callback once an Object is defined
* Utils.doWhen('SUGAR.ObjectToWaitFor', function(){
* // Use the object here
* console.log(SUGAR.ObjectToWaitFor);
* });
*
* // Use a function for condition and set parameters for the callback
* var el = $('#myId');
* var cond = function(){return el.hasClass('foo')};
* var callback = function(params) {
* this.log(params.msg);
* el.html(params.html);
* };
* Utils.doWhen(cond, callback, {
* msg: 'Hello World',
* html: '<h1>Exists!</h1>'
* }, console);
* ```
*
* @param {Function|string} condition Function/evaluatable string which
* must return a boolean value.
* @param {Function} callback Function to execute when `condition` is met.
* @param {Object} [params] Object to pass to `callback`.
* @param {Object} [scope] Object to use as `this` when executing
* `callback`.
*/
doWhen : function(condition, callback, params, scope){
_doWhenStack.push({
check: condition,
fn: callback,
obj: params,
overrideContext: scope
});
_doWhenretryCount = 50;
_startDoWhenInterval();
},
/**
* The guts of `doWhen`. Runs through the stack checking all the conditions
* and fires the callbacks when the conditions are met.
* @private
*/
// FIXME: we cannot make this truly private because it is used by the metadata-manager test (ugh...)
_doWhenCheck : function () {
if (_doWhenStack.length === 0) {
_doWhenretryCount = 0;
if (_doWhenInterval) {
clearInterval(_doWhenInterval);
_doWhenInterval = null;
}
return;
}
if (_doWhenLocked) {
return;
}
_doWhenLocked = true;
// keep trying until after the page is loaded. We need to
// check the page load state prior to trying to bind the
// elements so that we can be certain all elements have been
// tested appropriately
var tryAgain = $.isReady;
if (!tryAgain) {
tryAgain = (_doWhenretryCount > 0 && _doWhenStack.length > 0);
}
// onAvailable
var notAvail = [];
var executeItem = function (context, item) {
if (item.overrideContext) {
if (item.overrideContext === true) {
context = item.obj;
} else {
context = item.overrideContext;
}
}
if (item.fn) {
item.fn.call(context, item.obj);
}
};
var i, len, item, test;
// onAvailable onContentReady
for (i = 0, len = _doWhenStack.length; i < len; i = i + 1) {
item = _doWhenStack[i];
if (item) {
test = item.check;
if ((typeof(test) == 'string' &&
eval(test)) || // jshint ignore:line
(typeof(test) == 'function' && test())
) {
executeItem(this, item);
_doWhenStack[i] = null;
}
else {
notAvail.push(item);
}
}
}
_doWhenretryCount--;
if (tryAgain) {
for (i = _doWhenStack.length - 1; i > -1; i--) {
item = _doWhenStack[i];
if (!item || !item.check) {
_doWhenStack.splice(i, 1);
}
}
_startDoWhenInterval();
} else {
if (_doWhenInterval) {
clearInterval(_doWhenInterval);
_doWhenInterval = null;
}
}
_doWhenLocked = false;
},
/**
* Compares two version strings.
*
* Example:
* ```
* Utils.versionCompare('8.2.5rc', '8.2.5a'); // 1
* Utils.versionCompare('8.2.50', '8.2.52', '<') // true
* Utils.versionCompare('5.3.0-dev', '5.3.0') === -1
* Utils.versionCompare('4.1.0.52','4.01.0.51') === 1
* ```
*
* @param {string} v1 First version.
* @param {string} v2 Second version.
* @param {string} [operator] Operator argument, if specified, test for
* a particular relationship. The possible operators are:
* `<`, `lt`, `<=`, `le`, `>`, `gt`, `>=`, `ge`, `==`, `=`, `eq`,
* `!=`, `<>`, and `ne`.
* This parameter is case-sensitive, values should be lowercase.
*
* @return {number|boolean} By default, returns -1 if the first version
* is lower than the second, 0 if they are equal, and 1 if the second
* is lower. When using the optional operator argument, the function
* will return `true` if the relationship is the one specified by the
* operator, `false` otherwise.
*/
versionCompare: function(v1, v2, operator) {
// Use php.js implementation
// http://phpjs.org/functions/version_compare/
return version_compare(v1, v2, operator);
},
/**
* Forces one class to extend from another and optionally
* overrides specific properties.
*
* @param {Function} subc Constructor for the subclass.
* @param {Function} superc Constructor for the superclass.
* @param {Object} overrides Properties to override on `subc`'s prototype.
*/
extendFrom: function(subc, superc, overrides) {
subc.prototype = new superc(); // set the superclass
// overrides
_.extend(subc.prototype, overrides);
},
/**
* Creates a deep clone of an object.
*
* @param {*} obj The object to clone.
* @return {*} A value of the same type as the input.
*/
deepCopy: function(obj) {
return _.isObject(obj) ? JSON.parse(JSON.stringify(obj)) : obj;
},
/**
* Compare field values in the first bean with the second bean and return
* the field names that have different values.
*
* @param {Data/Bean} beanA The first bean.
* @param {Data/Bean} beanB The second bean.
* @return {string[]} A list of names of fields whose values differ between
* `beanA` and `beanB`.
*/
compareBeans: function(beanA, beanB) {
var changedFields;
if (beanA.module !== beanB.module) {
throw Error('Only beans of the same module can be compared.');
}
changedFields = _.reduce(beanA.attributes, function(memo, value, attribute) {
// skip id field and all fields that start with an underscore
if ((attribute !== 'id') && (attribute.indexOf('_') !== 0)) {
if (
!this.areBeanValuesEqual(value, beanB.get(attribute)) &&
this.hasDefaultValueChanged(attribute, beanA)
) {
memo.push(attribute);
}
}
return memo;
}, [], this);
return changedFields;
},
/**
* Checks to see if values in beans are equal to each other.
*
* @param {*} value1 The first value.
* @param {*} value2 The second value.
* @return {boolean} `true` if the values are equal; `false` otherwise.
*/
areBeanValuesEqual: function(value1, value2) {
var getValueToCompare = function(value) {
if (_.isObject(value) && _.isEmpty(value)) {
return '';
} else if (!_.isObject(value) && (_.isUndefined(value) || _.isNull(value))) {
return '';
} else {
return value;
}
};
value1 = getValueToCompare(value1);
value2 = getValueToCompare(value2);
return ((_.isObject(value1) && _.isEqual(value1, value2)) || (!_.isObject(value1) && (value1 === value2)));
},
/**
* Checks to see if the default value has changed.
*
* @param {string} attribute The bean attribute you are interested in.
* @param {Data/Bean} bean The bean to check the value of.
* @return {boolean} `true` if the value of `attribute` for `bean`
* is different than its default value; `false` otherwise.
*/
hasDefaultValueChanged: function(attribute, bean) {
var defaultValue = bean._defaults ? bean._defaults[attribute] : '';
return !this.areBeanValuesEqual(defaultValue, bean.get(attribute));
},
/**
* Checks if the AJAX error is a network connectivity error: timeout, DNS,
* etc.
*
* @param {Api.HttpError} ajaxError AJAX error.
* @return {boolean} `true` if the error is a network error; `false`
* otherwise.
*/
isConnectivityError: function(ajaxError) {
// There can be situations when the status is not zero but the actual request times out.
return ((ajaxError.status === 0) ||
(ajaxError.textStatus === "timeout"));
},
/**
* Returns ISO8601 timestamp in UTC time.
*
* @param {string|number} [dateValue=new Date()] Date string or raw msec
* number.
* @param {Object} [options={}] Extra parameters.
* @param {boolean} [options.msecPrecision=false] If `true`, include
* milliseconds in the output.
* @return {string} Passed date or current date converted to UTC timezone.
*/
getTimestamp: function(dateValue, options) {
options = options || {};
var date = dateValue ? new Date(dateValue) : new Date(),
dateString = date.toISOString();
if (!options.msecPrecision) {
dateString = dateString.replace(/\.\d{3}/, '');
}
//Z is replaced with +00:00 for unification with server
return dateString.replace(/Z/, '+00:00');
},
/**
* Builds a good url based on `siteUrl` from configuration.
*
* It is ready for the several use cases that `siteUrl` can have:
*
* - relative path (aka context);
* - full path;
* - empty path.
*
* @param {string} url The full url or a relative url without the
* prepended `/`.
* @return {string} The constructed URL.
*/
buildUrl: function(url) {
// Adjust relative URL: prepend it with site URL
if (url.indexOf("http") !== 0 && !_.isEmpty(SUGAR.App.config.siteUrl)) {
// Strip trailing forward slashes from site URL just in case
url = SUGAR.App.config.siteUrl.replace(/\/+$/, "") + "/" + url;
}
return url;
},
/**
* Gets the diff between `data1` and `data2`.
* Intended for comparing objects with the same properties.
*
* @param {Object} data1 Changed object
* @param {Object} data2 Original object
* @param {boolean} [strict=false] By default values are compared property
* by property via non-strict comparison.
* @return {Object} Hash of fields from `data1` which are different from `data2`.
*/
getChangedProps: function (data1, data2, strict) {
/**
* Determines the type of a given value.
*
* @param {*} value The value to check.
* @return {string} 'array' if `value` is an array, 'object' if it is
* and object, and 'other' in all other cases.
* @private
*/
function getType(value) {
if (_.isObject(value)) {
if (_.isArray(value)) {
return 'array';
}
return 'object';
}
return 'other';
}
function isEqualProp (value1, value2) {
if (strict) return _.isEqual(value1, value2);
var value1Type = getType(value1),
value2Type = getType(value2);
if (value1Type === value2Type && _.contains(['object', 'array'], value1Type)) {
if (value1Type === 'array' && value1.length !== value2.length) {
return false;
}
var keys1 = _.keys(value1),
keys2 = _.keys(value2);
if (keys1.length !== keys2.length || !_.isEqual(keys1.sort(), keys2.sort())) {
return false;
}
return !_.any(value1, function (val, key) {
return !isEqualProp(value1[key], value2[key]);
});
}
else {
return strict ? (value1 === value2) : (value1 == value2);
}
}
var diff = {};
_.each(data1, function (value, key) {
if (!isEqualProp(data1[key], data2[key])) {
diff[key] = value;
}
});
return diff;
},
/**
* Gets the layout container for the given component.
*
* @param {View/Component} component The component whose layout you want.
* @return {View/Layout} The layout container for the given `component`.
*/
getParentLayout: function(component) {
var parent;
if (component.view && component.view.layout) {
parent = component.view.layout;
} else {
parent = component.layout;
}
return parent;
},
/**
* Determines if a given module is sortable on a certain field when in a
* particular view.
*
* @param {string} module The module to check.
* @param {Object} fieldViewdef Viewdef for the field to consider.
* @param {string} fieldViewdef.name Name of the field; used to look up
* vardefs.
* @param {boolean} [fieldViewdef.sortable] If not `undefined`, determines
* whether the module is sortable or not, regardless of vardefs.
* @return {boolean} `true` if `module` can be sorted by the field given by
* `fieldViewdef`; `false` otherwise.
*/
isSortable: function(module, fieldViewdef) {
var fieldVardef = SUGAR.App.metadata.getModule(module).fields[fieldViewdef.name],
isSortable = true;
if (fieldVardef && !_.isUndefined(fieldVardef.sortable)) {
isSortable = fieldVardef.sortable;
}
if (fieldViewdef && !_.isUndefined(fieldViewdef.sortable)) {
isSortable = fieldViewdef.sortable;
}
return isSortable;
},
/**
* Returns a URL without the http(s):// prefix.
*
* @param {string} url Input url.
* @return {string} `url` without the http(s):// prefix.
*/
stripHttpPrefix: function(url) {
return url.replace(/^https?:\/\//, '');
},
/**
* Performs a hard refresh of the current page.
*/
hardRefresh: function() {
window.location.reload(true);
},
/**
* Checks if the value should be rendered in RTL.
*
* @param {string} value The value to check.
* @return {boolean} `true` if the string is in a RTL language.
*/
isDirectionRTL: function(value) {
var ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' +
'\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF',
rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC',
isRTL = new RegExp('^[^' + ltrChars + ']*[' + rtlChars + ']');
return isRTL.test(value);
},
/**
* Converts provided value to provided type. Intended to convert numbers
* stored as strings using the appropriate conversion function.
*
* @param {string} value The value to convert
* @param {string} type Abbreviated type to be converted to
* @returns {number} Value converted to provided type
*/
convertNumericType: function (value, type) {
switch (type) {
case 'int':
return parseInt(value);
case 'float':
case 'decimal':
return parseFloat(value);
}
return Number(value);
}
};
module.exports = Utils;