@kwiz/common
Version:
KWIZ common utilities and helpers for M365 platform
275 lines (252 loc) • 10.9 kB
text/typescript
import { makeUniqueArray } from "./collections.base";
import { jsonParse } from "./json";
import { isDate, isFunction, isNotEmptyArray, isNullOrEmptyString, isNullOrUndefined, isObject, isPrimitiveValue, isString, primitiveTypes } from "./typecheckers";
/** global window, safe for testing and environments without a browser */
export var $w = typeof window === "undefined" ? {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
location: {
href: "", host: ""
}
} as any as Window : window;
/** wrapper for hasOwnProperty that satisfies https://eslint.org/docs/latest/rules/no-prototype-builtins */
export function hasOwnProperty(obj: any, prop: string) {
if (!isNullOrUndefined(obj)) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
return false;
}
/** empty async function */
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
export var noop = async () => { };
/** empty sync function */
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
export var noops = () => { };
/** get or create kwizcom object from top window or current window, set allowFromTop if you want to try to get from window.top */
export function getKWizComGlobal(allowFromTop?: boolean) {
if (allowFromTop) {
try {
$w.top["kwizcom"] = $w.top["kwizcom"] || {};
return $w.top["kwizcom"] as IKWizComGlobals;
} catch (ex) { }
}
$w["kwizcom"] = $w["kwizcom"] || {};
return $w["kwizcom"] as IKWizComGlobals;
}
/** get or create kwizcom.globals dictionary from top window or current window. Add or return key:name initialize as defaults or blank object if does not already exist */
export function getGlobal<T>(name: string, defaults?: T, notFromTop?: boolean) {
var kGlobal = getKWizComGlobal(notFromTop !== true);
kGlobal.globals = kGlobal.globals || {};
if (!kGlobal.globals[name]) {
if (isObject(defaults)) {
// eslint-disable-next-line @typescript-eslint/ban-types
kGlobal.globals[name] = { ...(defaults as object) };
} else {
kGlobal.globals[name] = {};
}
}
return kGlobal.globals[name] as T;
}
/**
* Automatically bind all functions of instance to instance
* Note: if you use knockout, you should skip ko.isObservable
* @param instance
*/
//eslint-disable-next-line @typescript-eslint/ban-types
export function autoBind(instance: any, skip?: (name: string, f: Function) => boolean) {
let funcitonNames = getAllFunctionNames(instance, 1);
funcitonNames.forEach(prop => {
const val = instance[prop];
if (!isFunction(skip) || !skip(prop, val))
instance[prop] = val.bind(instance);
});
}
/** Implements Object.assign which does not exist in IE
* Copies properties over from overrides into original object
* Merge default and override settings: var merged = assign({},defaults,props)
* Create deep copy of object by var copy = assign({},obj) */
export function assign<T>(original: Partial<T>, ...overrides: Partial<T>[]): T {
if (original === undefined || original === null) {
throw new TypeError('Cannot convert first argument to object');
}
var to = Object(original);
if (overrides && overrides.length > 0)
overrides.forEach(o => {
if (!isNullOrUndefined(o)) {
var keysArray = Object.keys(Object(o));
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
var nextKey = keysArray[nextIndex];
var desc = Object.getOwnPropertyDescriptor(o, nextKey);
if (typeof (desc.value) === "undefined" || desc.value === null) {
to[nextKey] = desc.value;
}
else if (typeof (desc.value) === "object") {
//reference type, call assign recursively. fixed problem that object value types from overrides were modified when the returnd object was modified.
if (!Array.isArray(desc.value)) {
to[nextKey] = assign({}, desc.value);
} else {
to[nextKey] = assign([], desc.value);
}
} else {
if (desc !== undefined && desc.enumerable) {
to[nextKey] = desc.value;//this will make a shallow copy, altering the override object o[nextKey];
}
}
}
}
});
return to as T;
}
export function primitivesEqual(o1: primitiveTypes, o2: primitiveTypes) {
let normalize = (oo: any) => {
if (isNullOrUndefined(oo))
return null;
else if (isDate(oo))
return oo.getTime();
return oo;
};
o1 = normalize(o1);
o2 = normalize(o2);
return o1 === o2;
}
export function objectsEqual<T extends object>(o1: T, o2: T, ignoreKeys?: string[]) {
let isPrimitive1 = isPrimitiveValue(o1);
let isPrimitive2 = isPrimitiveValue(o2);
if (isPrimitive1 !== isPrimitive2) return false;//one primitive other not?
if (isPrimitive1)
return primitivesEqual(o1 as unknown as primitiveTypes, o2 as unknown as primitiveTypes);
if (isFunction(o1) || isFunction(o2)) {
try {
return o1.toString() === o2.toString();
} catch (e) {
return false;
}
}
ignoreKeys = ignoreKeys || [];
let allKeys = makeUniqueArray(Object.keys(o1).concat(Object.keys(o2))).filter(key => !ignoreKeys.includes(key));
for (let i = 0; i < allKeys.length; i++) {
let key = allKeys[i];
if (!objectsEqual(o1[key], o2[key], ignoreKeys))
return false;
}
return true;
}
export function jsonClone<T>(obj: T): T {
//todo: check if assign utility method is faster
if (isNullOrUndefined(obj)) return null;
let result = obj;
try { result = jsonParse<T>(JSON.stringify(obj)); }
catch (e) {
if (isNotEmptyArray(obj))
result = (obj as any).slice();
else
result = obj;
}
//clone date objects
try { cloneDatesOnObjectRecursivily(obj, result); } catch (e) { }
return result;
}
function cloneDatesOnObjectRecursivily(obj1, obj2) {
Object.keys(obj1).forEach(key => {
let v = obj1[key];
if (v === null)
obj2[key] = null;
else if (isDate(v))
obj2[key] = new Date(v.getTime());
else if (isObject(v) && Object.keys(v).length) {
let v2 = obj2[key];
if (isObject(v2))
cloneDatesOnObjectRecursivily(v, v2);
}
});
}
/** if an object in this path doesnt exist under parent - creates it.*/
export function ensureObjectPath(objectPath: string, defaultValue: any = {}, parent: any = $w) {
if (isNullOrEmptyString(objectPath)) return;
let parts = objectPath.split('.');
for (let i = 0; i < parts.length; i++) {
let partName = parts[i];
if (i === parts.length - 1)//last
{
if (isNullOrUndefined(parent[partName]))
parent[partName] = defaultValue;
}
else {
if (isNullOrUndefined(parent[partName]))
parent[partName] = {};
parent = parent[partName];
}
}
}
/** If o has propb and not propa - will copy propb into propa and remove propb */
export function keepOne(o: any, propa: string, propb: string) {
/* using the unkown type doesn't work in modern apps project */
if (isObject(o)) {
if (!hasOwnProperty(o, propa) && hasOwnProperty(o, propb)) {
o[propa] = o[propb];
delete o[propb];
}
}
}
/**return all members and functions of an object, including inherited ones from its base class, excluding the constructor
* send prototypeLevels to limit the number of prototype climbs to get functions from. 0 means unlimited.
*/
export function getAllMemberNames(instance: any, prototypeLevels: number): string[] {
let props: string[] = [];
let obj = instance;
let level = 0;
let unlimitedLevels = prototypeLevels < 0;
do {
props.push(...Object.getOwnPropertyNames(obj));
obj = Object.getPrototypeOf(obj);
level++;
}
while (unlimitedLevels ? !!obj : !!obj && level <= prototypeLevels);
return makeUniqueArray(props.filter(p => p !== 'constructor' && p !== 'dispose'));
}
/**return all functions of an object, including inherited ones from its base class, excluding the constructor
* send prototypeLevels to limit the number of prototype climbs to get functions from. 0 means unlimited.
*/
export function getAllFunctionNames(instance: any, prototypeLevels: number): string[] {
return getAllMemberNames(instance, prototypeLevels).filter(p => isFunction(instance[p]));
}
/** generic implementation of Object.values */
export function objectValues<T = any>(obj: any): T[] {
return Object.keys(obj).map((key) => {
return obj[key];
}) as T[];
}
export class DefaultProp<T> {
private _value: T;
private _defaultValue: T | (() => T);
private isValid: (value: T) => boolean;
public set value(newValue: T) { this._value = newValue; }
public get value() {
if (!this.isValid(this._value)) {
this._value = isFunction(this._defaultValue)
? (this._defaultValue as () => T)()
: this._defaultValue;
}
return this._value;
}
public constructor(defaultValue: T | (() => T), initialValue?: T, isValid?: (value: T) => boolean) {
this._defaultValue = defaultValue;
this._value = initialValue;
this.isValid = isFunction(isValid) ? isValid : v => !isNullOrUndefined(v);
}
}
/** creates a safe property, if the value is null/undefined or empty string - it will return the default value. */
export function GetDefaultProp<T>(defaultValue: T | (() => T), initialValue?: T, isValid?: (value: T) => boolean) {
return new DefaultProp(defaultValue, initialValue, isValid);
}
/** Get string error message from an error object */
export function GetError(error: any, defaultError: string = "Unknown error"): string {
const err = isNullOrUndefined(error)
? defaultError
: isString(error)
? error
: isString((error as Error).message)
? error.message
: defaultError;
return err.length > 0 ? err : defaultError;
}