UNPKG

@eclipse-scout/core

Version:
962 lines (872 loc) 33.8 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {arrays, BaseDoEntity, dates, ObjectFactory, Primitive, scout, Widget} from '../index'; import $ from 'jquery'; const CONST_REGEX = /\${const:([^}]*)}/; export const objects = { /** * Uses Object.create(null) to create an object without a prototype. This is different to use the literal {} which links the object to Object.prototype. * <p> * Not using the literal has the advantage that the object does not contain any inherited properties like `toString` so it is not necessary to use `o.hasOwnProperty(p)` * instead of `p in o` to check for the existence. * * @param [properties] optional initial properties to be set on the new created object */ createMap(properties?: object): any { let map = Object.create(null); if (properties) { $.extend(map, properties); } return map; }, /** * Copies all the properties (including the ones from the prototype.) from dest to source * @param [filter] an array of property names. * @returns the destination object (the destination parameter will be modified as well) */ copyProperties<D>(source: object, dest: D, filter?: string[] | string): D { let propertyName; filter = arrays.ensure(filter); for (propertyName in source) { if (filter.length === 0 || filter.indexOf(propertyName) !== -1) { dest[propertyName] = source[propertyName]; } } return dest; }, /** * Creates a dynamic proxy which can be used e.g. to initialize a constant. * The proxy wraps an instance created on first use using the given constructor function. * All calls to the proxy are forwarded to this lazy instance. * The instance can only be created after the {@link ObjectFactory} has been initialized. * @param constr The constructor to lazily create the instance on first use. * @returns A proxy that delegates calls to the lazy instance. */ createSingletonProxy<T extends object>(constr: new() => T): T { return new Proxy({/* target obj for the lazy instance */}, { get(target: { instance?: T }, prop: string | symbol, proxy) { if (!target.instance) { if (prop === 'prototype') { // variable has no prototype: directly return undefined so that no instance is created yet. return undefined; } if (!ObjectFactory.get()?.initialized) { // only allow singleton creation after ObjectFactory has been set up. Otherwise, the class cannot be customized/replaced. throw Error('Singleton cannot be created yet as the ObjectFactory is not initialized.'); } target.instance = scout.create(constr); } let requestedProperty = target.instance[prop]; if (typeof requestedProperty === 'function') { return requestedProperty.bind(target.instance); } return Reflect.get(target.instance, prop, target.instance); }, set(target: { instance?: T }, prop: string | symbol, value: any) { return Reflect.set(target.instance, prop, value, target.instance); } }) as T; }, /** * Copies the own properties (excluding the ones from the prototype) from source to dest. * If a filter is specified, only the properties matching the ones in the filter are copied. * @param [filter] an array of property names. * @returns the destination object (the destination parameter will be modified as well) */ copyOwnProperties<D>(source: object, dest: D, filter?: string[] | string): D { let propertyName; filter = arrays.ensure(filter); for (propertyName in source) { if (Object.prototype.hasOwnProperty.call(source, propertyName) && (filter.length === 0 || filter.indexOf(propertyName) !== -1)) { dest[propertyName] = source[propertyName]; } } return dest; }, /** * Counts and returns the properties of a given object or map (see #createMap). */ countOwnProperties(obj: object): number { // map objects don't have a prototype if (!Object.getPrototypeOf(obj)) { return Object.keys(obj).length; } // regular objects may inherit a property through their prototype // we're only interested in own properties let count = 0; for (let prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { count++; } } return count; }, /** * Copies the specified properties (including the ones from the prototype) from source to dest. * Properties that already exist on dest are NOT overwritten. */ extractProperties<D>(source: object, dest: D, properties: string[]): D { properties.forEach(propertyName => { if (dest[propertyName] === undefined) { dest[propertyName] = source[propertyName]; } }); return dest; }, /** * returns * - true if the obj has at least one of the given properties. * - false if the obj has none of the given properties. */ someOwnProperties(obj: object, properties: string[] | string): boolean { let propArr = arrays.ensure(properties); return propArr.some(prop => { return Object.prototype.hasOwnProperty.call(obj, prop); }); }, /** * returns * - true if the obj or its prototypes have at least one of the given properties. * - false if the obj or its prototypes have none of the given properties. */ someProperties(obj: object, properties: string[] | string): boolean { let propArr = arrays.ensure(properties); return propArr.some(prop => { return prop in obj; }); }, /** * Creates a copy of the given object with the properties in alphabetic order. The characters in the property names are compared * individually (no alphanumeric sorting). The names are first sorted by ascending length and then by character class: special * characters (punctuation etc.), then numbers (0-9), then lowercase letters (a-z), then uppercase letters (A-Z). * * The order of elements in an array is preserved. Values that are not plain objects are left as is. This method detects cyclic * references and does not throw an error. Instead, the cyclic reference is reassigned to the corresponding copy. * * @param recursive whether to recursively sort property names in nested objects. The default value is `true`. */ sortProperties<T>(obj: T, recursive = true): T { return sortPropertiesImpl(obj, recursive, new Map()); function sortPropertiesImpl(obj: any, recursive: boolean, seen: Map<any, any>): any { // Check for cyclic references if (seen.has(obj)) { return seen.get(obj); } if (objects.isArray(obj)) { let copy = []; seen.set(obj, copy); return obj.reduce((acc, elem) => { if (recursive) { acc.push(sortPropertiesImpl(elem, recursive, seen)); } else { acc.push(elem); } return acc; }, copy); } if (objects.isObject(obj)) { let copy = {}; seen.set(obj, copy); return Object.keys(obj) .sort((k1, k2) => k1.localeCompare(k2)) .reduce((acc, key) => { if (recursive) { acc[key] = sortPropertiesImpl(obj[key], recursive, seen); } else { acc[key] = obj[key]; } return acc; }, copy); } return obj; } }, /** * Creates deep clones of given value. * The following types are supported: * * Pojo * * `Array` * * `Date` * * `Map` * * `Set` * * All objects (except Widget) having a `clone` function (expected to create a deep clone without taking any arguments). These are classes like `BaseDoEntity`, `Dimension`, `GridData`, `Insets`, `Point`, `Status`, `URL`, ... * @param val The value to deep clone. * @returns The deep clone if supported, the input value otherwise. */ valueCopy<T>(val: T): T { if (objects.isPojo(val)) { const pojoCopy = {}; for (const [k, v] of Object.entries(val)) { pojoCopy[k] = objects.valueCopy(v); } return pojoCopy as T; } return deepCloneClass(val); // handles all class types }, /** * Returns the first object with the given property and propertyValue or null if there is no such object within parentObj. * @param property property to search for * @param propertyValue value of the property */ findChildObjectByKey(parentObj: any, property: string, propertyValue: any): any { if (parentObj === undefined || parentObj === null || typeof parentObj !== 'object') { return null; } if (parentObj[property] === propertyValue) { return parentObj; } let child; if (Array.isArray(parentObj)) { for (let i = 0; i < parentObj.length; i++) { child = objects.findChildObjectByKey(parentObj[i], property, propertyValue); if (child) { return child; } } } for (let prop in parentObj) { if (Object.prototype.hasOwnProperty.call(parentObj, prop)) { child = objects.findChildObjectByKey(parentObj[prop], property, propertyValue); if (child) { return child; } } } return null; }, /** * This function returns the value of a property from the provided object specified by the second path parameter. * The path consists of a dot separated series of property names (e.g. foo, foo.bar, foo.bar.baz). * In addition, traversing into array properties is possible by specifying a suitable filter for the element's id property in square brackets (e.g. foo[bar], foo.bar[baz]). * * Example: * * let obj = { * foo: { * bar: { * foobar: 'val1' * } * }, * baz: [ * { * id: 'baz1', * value: 'val2' * }, * { * id: 'baz2', * value: 'val3' * } * ] * } * * objects.getByPath(obj, 'foo') === obj.foo; * objects.getByPath(obj, 'foo.bar') === obj.foo.bar; * objects.getByPath(obj, 'baz[baz1]') → { id: 'baz1', value: 'val2' } * objects.getByPath(obj, 'baz[baz2].value') → 'val3' * * @param object The object to select a property from. * @param path The path for the selection. * @returns Object Returns the selected object. * @throws Throws an error, if the provided parameters are malformed, or a property could not be found/a id property filter does not find any elements. */ getByPath(object: object, path: string): any { scout.assertParameter('object', object, Object); scout.assertParameter('path', path); const pathElementRegexString = '(\\w+)(?:\\[((?:\\w|\\.|-)+)\\])?'; const pathValidationRegex = new RegExp('^' + pathElementRegexString + '(?:\\.' + pathElementRegexString + ')*$'); if (!pathValidationRegex.test(path)) { throw new Error('Malformed path expression "' + path + '"'); } const pathElementRegex = new RegExp(pathElementRegexString); let pathMatchedSoFar = ''; let currentContext = object; // Split by dot, but only if the dot is not followed by a string containing a ] that is not preceded by a [. // That excludes dots, that are part of an array filter (e.g. foo[foo.bar]). // Explanation: The regular expression matches dots literally, (\.), that are not followed (negative lookahead: (?!...) // by any mount of "not opening square brackets" ([^[]*) followed by a closing square bracket (last closing square bracket: ]) path.split(/\.(?![^[]*])/).forEach(pathElement => { // After the first iteration, the current context may be null or undefined. In this case, further traversal is not possible. if (objects.isNullOrUndefined(currentContext)) { throw new Error('Value selected by matched path "' + pathMatchedSoFar + '" is null or undefined. Further traversal not possible.'); } // match path element to retrieve property name and optional array property index let pathElementMatch = pathElementRegex.exec(pathElement); let propertyName = pathElementMatch[1]; let arrayPropertyFilter = pathElementMatch[2]; let pathMatchedErrorContext = pathMatchedSoFar.length === 0 ? 'root level of the provided object.' : 'matched path "' + pathMatchedSoFar + '".'; // check if property 'propertyName' exists if (!currentContext.hasOwnProperty(propertyName)) { throw new Error('Property "' + propertyName + '" does not exist at the ' + pathMatchedErrorContext); } let property = currentContext[propertyName]; // check if we are trying to match an array property or not if (arrayPropertyFilter) { // check for correct type of property if (!Array.isArray(property)) { throw new Error('Path element "' + pathElement + '" contains array filter but property "' + propertyName + '" does not contain an array at the ' + pathMatchedErrorContext); } // find elements matching criteria and make sure that exactly one object was found let matchedElements = property.filter(element => { return element['id'] === arrayPropertyFilter; }); if (matchedElements.length === 0) { throw new Error('No object found with id property "' + arrayPropertyFilter + '" in array property "' + propertyName + '" at the ' + pathMatchedErrorContext); } else if (matchedElements.length > 1) { throw new Error('More than one object found with id property "' + arrayPropertyFilter + '" in array property "' + propertyName + '" at the ' + pathMatchedErrorContext); } // reassign current context to found element currentContext = matchedElements[0]; } else { // reassign current context to found property currentContext = property; } if (pathMatchedSoFar) { pathMatchedSoFar += '.'; } pathMatchedSoFar += pathElement; }); return currentContext; }, /** * @deprecated The method was renamed to {@link isObject}. Use the new name or consider using {@link isPojo} instead. */ isPlainObject<T>(obj: T): obj is Exclude<typeof obj, Primitive | undefined | null | T[]> { return objects.isObject(obj); }, /** * @returns true if the given object is an object: no primitive type (number, string, boolean, bigint, symbol), no array, not null and not undefined. */ isObject<T>(obj: T): obj is Exclude<typeof obj, Primitive | undefined | null | T[]> { return typeof obj === 'object' && !objects.isNullOrUndefined(obj) && !Array.isArray(obj); }, /** * Checks if the given object is a plain old JavaScript object, which is an object created by the object literal notation (`{}`), `Object.create` or `new Object()`. * * Note: objects without prototype (e.g. created using `Object.create(null)` or {@link objects.createMap}) return true here. * So methods typically <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#instance_methods">inherited from Object</a> may not be available for a pojo! * @returns true if it is a pojo, false otherwise. */ isPojo<T>(obj: T): obj is Exclude<typeof obj, Primitive | undefined | null | T[]> { if (!objects.isObject(obj)) { return false; } let prototype = Object.getPrototypeOf(obj); return prototype === Object.prototype || prototype === null; }, /** * Null-safe access the property of objects. Instead of using this function consider using conditional chaining with the elvis operator: obj?.foo?.bar. * Examples: * <ul> * <li><code>optProperty(obj, 'value');</code> try to access and return obj.value</li> * <li><code>optProperty(obj, 'foo', 'bar');</code> try to access and return obj.foo.bar</li> * </ul> * * @returns the value of the requested property or undefined if the property does not exist on the object */ optProperty(obj: object, ...properties: string[]): any { if (!obj) { return null; } let numArgs = properties.length; if (numArgs === 0) { return obj; } if (numArgs === 1) { return obj[properties[0]]; } for (let i = 0; i < numArgs - 1; i++) { obj = obj[properties[i]]; if (!obj) { return null; } } return obj[properties[numArgs - 1]]; }, /** * Returns true if: * - obj is not undefined or null * - obj not isNaN * - obj isFinite * * This method is handy in cases where you want to check if a number is set. Since you cannot write: * if (myNumber) { ... * * Because when myNumber === 0 would also resolve to false. In that case use instead: * if (isNumber(myNumber)) { ... */ isNumber(obj: any): obj is number { return obj !== null && !isNaN(obj) && isFinite(obj) && !isNaN(parseFloat(obj)); }, isString(obj: any): obj is string { return typeof obj === 'string' || obj instanceof String; }, isNullOrUndefined(obj: any): obj is null | undefined { return obj === null || obj === undefined; }, // eslint-disable-next-line @typescript-eslint/ban-types isFunction(obj: any): obj is Function { return $.isFunction(obj); }, /** * Returns true if the given object is {@link isNullOrUndefined null or undefined} or {@link isEmpty empty}. */ isNullOrUndefinedOrEmpty(obj: any): boolean { if (objects.isNullOrUndefined(obj)) { return true; } return objects.isEmpty(obj); }, isArray(obj: any): obj is Array<any> { return Array.isArray(obj); }, /** * Checks whether the provided value is a promise or not. * @param value The value to check. * @returns true, in case the provided value is a thenable, false otherwise. * * Note: This method checks whether the provided value is a "thenable" (see https://promisesaplus.com/#terminology). * Checking for promise would require to check the behavior which is not possible. So you could provide an object * with a "then" function that does not conform to the Promises/A+ spec but this method would still return true. */ isPromise(value: any): value is PromiseLike<any> { return !!value && typeof value === 'object' && typeof value.then === 'function'; }, /** * Returns values from the given (map) object. By default, only values of 'own' properties are returned. * * @param obj * @param all can be set to true to return all properties instead of own properties * @returns an Array with values */ values<K extends PropertyKey, V>(obj: Record<K, V>, all?: boolean): V[] { let values: V[] = []; if (obj) { if (typeof obj.hasOwnProperty !== 'function') { all = true; } for (let key in obj) { if (all || obj.hasOwnProperty(key)) { values.push(obj[key]); } } } return values; }, /** * @returns the key (name) of a property with given value */ keyByValue<V>(obj: Record<string, V>, value: V): string { return Object.keys(obj)[objects.values(obj).indexOf(value)]; }, /** * Java-like equals method. * * The two values are considered equal if one of the following rules applies: * * * They are the same objects (===). * * They are Dates having the same value. * * They are both zero-length collections (Array, Map or Set). * * They have both an `equals` method, are of the same Class type and this `equals` method returns `true`. * * @returns true if both values are equal. */ equals(objA: any, objB: any): boolean { return !!equalsImpl(objA, objB); // equalsImpl might return null which means false. }, /** * Compares two objects and all its child elements recursively using value equality as defined by {@link #equals}. Order of the property keys is ignored. * * @param objA The first value to compare. * @param objB The second value to compare. * @param skipRootEquals An optional boolean indicating if the equals method should be ignored for the given two objects. Default is false. * It might be handy to set this to true if it is called from within an equals method to prevent stack overflows. * @returns true if both objects and all child elements are equals by value or implemented equals method. * @see objects.equals */ equalsRecursive(objA: any, objB: any, skipRootEquals = false): boolean { const equalsResult = equalsImpl(objA, objB, !skipRootEquals); if (equalsResult !== null) { return equalsResult; } // Map if (objA instanceof Map && objB instanceof Map) { return objects.equalsMap(objA, objB); } // Set if (objA instanceof Set && objB instanceof Set) { return objects.equalsSet(objA, objB, true); } // Objects if (objects.isObject(objA) && objects.isObject(objB)) { const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (!arrays.equalsIgnoreOrder(keysA, keysB)) { return false; } for (const key of keysA) { if (!objects.equalsRecursive(objA[key], objB[key])) { return false; } } return true; } // Arrays if (objects.isArray(objA) && objects.isArray(objB)) { if (objA.length !== objB.length) { return false; } for (let i = 0; i < objA.length; i++) { if (!objects.equalsRecursive(objA[i], objB[i])) { return false; } } return true; } return false; }, /** * Compares a list of properties of two objects by using the equals method for each property. */ propertiesEquals(objA: object, objB: object, properties: string[]): boolean { let i, property; for (i = 0; i < properties.length; i++) { property = properties[i]; if (!objects.equals(objA[property], objB[property])) { return false; } } return true; }, /** * @returns the function identified by funcName from the given object. The function will return an error * if that function does not exist. Use this function if you modify an existing framework function * to find problems after refactoring / renaming as soon as possible. */ // eslint-disable-next-line mandatoryFunction(obj: object, funcName: string): Function { let func = obj[funcName]; if (!func || typeof func !== 'function') { throw new Error('Function \'' + funcName + '\' does not exist on object. Check if it has been renamed or moved. Object: ' + obj); } return func; }, /** * Use this method to replace a function on a prototype of an object. It checks if that function exists * by calling <code>mandatoryFunction</code>. */ // eslint-disable-next-line replacePrototypeFunction(obj: any, funcOrName: string | ((...args) => any), func: Function, rememberOrig: boolean) { let proto = obj.prototype; let funcName; if (typeof funcOrName === 'string') { funcName = funcOrName; } else { funcName = funcOrName.name; } objects.mandatoryFunction(proto, funcName); if (rememberOrig) { proto[funcName + 'Orig'] = proto[funcName]; } proto[funcName] = func; }, /** * @returns a real Array for the pseudo-array 'arguments'. */ argumentsToArray(args: IArguments): any[] { return args ? Array.prototype.slice.call(args) : []; }, /** * Used to loop over 'arguments' pseudo-array with forEach. */ forEachArgument(args: IArguments, func: (value: any, index: number, args: any[]) => void) { return objects.argumentsToArray(args).forEach(func); }, /** * @param value text which contains a constant reference like '${const:FormField.LabelPosition.RIGHT}'. * @returns the resolved constant value or the unchanged input value if the constant could not be resolved. */ resolveConst(value: string, constType?: any): any { if (!objects.isString(value)) { return value; } let result = CONST_REGEX.exec(value); if (result && result.length === 2) { // go down the object hierarchy starting on the given constType-object or on 'window' let objectHierarchy = result[1].split('.'); let obj = constType || window; for (let i = 0; i < objectHierarchy.length; i++) { obj = obj[objectHierarchy[i]]; if (obj === undefined) { window.console.log('Failed to resolve constant \'' + result[1] + '\', object is undefined'); return value; } } return obj; } return value; }, resolveConstProperty(object: object, config: { property: string; constType: any }) { scout.assertProperty(config, 'property'); scout.assertProperty(config, 'constType'); let value = object[config.property]; let resolvedValue = objects.resolveConst(value, config.constType); if (value !== resolvedValue) { object[config.property] = resolvedValue; } }, resolveConstProperties(object: object, configs: { property: string; constType: any }[]) { arrays.ensure(configs).forEach(config => { objects.resolveConstProperty(object, config); }); }, /** * Cleans the given object, i.e. removes all top-level properties with values that are null, undefined or * consist of an empty array or an empty object. This is useful to have a minimal data object. * * This method is *not* recursive. * * The object is modified *in-place* and is also returned. * * If the given object is set but not a {@link isObject plain object}, an error is thrown. * * @see isNullOrUndefinedOrEmpty */ removeEmptyProperties(object: any): any { if (objects.isNullOrUndefined(object)) { return object; } if (!objects.isObject(object)) { throw new Error('Not an object: ' + object); } // Attributes in DOs should not be removed but set to undefined so that they look the same as a new instance. Important when comparing DOs. const removeAttribute = object instanceof BaseDoEntity ? (key: string) => { object[key] = undefined; } : (key: string) => delete object[key]; Object.keys(object).forEach(key => { if (objects.isNullOrUndefinedOrEmpty(object[key])) { removeAttribute(key); } }); return object; }, /** * Empty if the argument is: * - `null` * - `undefined` * - an empty {@link Array} * - an empty {@link Map} * - an empty {@link Set} * - or an object without keys (except {@link Date} which is never empty). * * @returns `true` if *obj* is empty, `false` if *obj* is not empty, `undefined` if *obj* is no object (e.g. a primitive). */ isEmpty(obj: any): boolean | undefined { if (objects.isNullOrUndefined(obj)) { return true; } if (objects.isArray(obj)) { return arrays.empty(obj); } if (!objects.isObject(obj)) { return undefined; } if (obj instanceof Date) { return false; } if (obj instanceof Map) { return obj.size === 0; } if (obj instanceof Set) { return obj.size === 0; } return Object.keys(obj).length === 0; }, /** * @returns true if the first parameter is the same or a subclass of the second parameter. */ isSameOrExtendsClass<TClass2>(class1: any, class2: abstract new() => TClass2): class1 is new() => TClass2 { if (typeof class1 !== 'function' || typeof class2 !== 'function') { return false; } return class1 === class2 || class2.isPrototypeOf(class1); }, /** * Converts any non-string argument to a string that can be used as an object property name. * Complex objects are converted to their JSON representation (instead of returning something * non-descriptive such as '[Object object]'). */ ensureValidKey(key: any): string { if (key === undefined) { return 'undefined'; } if (objects.isString(key)) { return key; } return JSON.stringify(key); }, /** * Receives the value for the given key from the map, if the value is not null or undefined. * If the key has no value associated, the value will be computed with the given function and added to the map, unless it is null or undefined. * * @returns the value associated with the given key or the computed value returned by the given mapping function. */ getOrSetIfAbsent<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, computeValue: (key: TKey) => TValue): TValue { let value = map.get(key); if (!objects.isNullOrUndefined(value)) { return value; } value = computeValue(key); if (!objects.isNullOrUndefined(value)) { map.set(key, value); } return value; }, /** * Compares two Sets for equality using {@link equals}. * * @param setA First Set. * @param setB Second Set. * @param deep Specifies if a deep comparison should be performed (recursively). If the Set value is a non-primitive type the equals steps into these structures (Objects, Arrays, Maps, Sets). Default is false. */ equalsSet(setA: Set<any>, setB: Set<any>, deep = false) { if (setA === setB) { return true; } if (!setA || !setB) { return false; } if (setA.size !== setB.size) { return false; } return equalsIterable(setA, setB, deep ? objects.equalsRecursive : objects.equals); }, /** * Compares two Maps for equality using {@link equals} on keys and values. * * @param setA First Map. * @param setB Second Map. * @param deep Specifies if a deep comparison should be performed (recursively). If the Map key or value is a non-primitive type the equals steps into these structures (Objects, Arrays, Maps, Sets). Default is false. */ equalsMap(mapA: Map<any, any>, mapB: Map<any, any>, deep = false): boolean { if (mapA === mapB) { return true; } if (!mapA || !mapB) { return false; } if (mapA.size !== mapB.size) { return false; } const equalsEntriesFunc = deep ? equalsMapEntriesRecursive : equalsMapEntries; return equalsIterable(mapA.entries(), mapB.entries(), equalsEntriesFunc); } }; /** * Deep clone implementation for special classes. The following classes are supported: * * `Array` * * `Date` * * `Map` * * `Set` * * All objects having a `clone` function (expected to create a deep clone without taking any arguments). Widgets are excluded. * These are classes like `BaseDoEntity`, `Dimension`, `GridData`, `Insets`, `Point`, `Status`, `URL`, ... */ function deepCloneClass(val: any): any { if (!val) { return val; } // Array if (objects.isArray(val)) { return val.map(e => objects.valueCopy(e)); } // Date if (val instanceof Date) { return new Date(val.getTime()); } // Map if (val instanceof Map) { const mapCopy = new Map(); for (const [key, value] of val) { mapCopy.set(objects.valueCopy(key), objects.valueCopy(value)); } return mapCopy; } // Set if (val instanceof Set) { const setCopy = new Set(); for (const item of val) { setCopy.add(objects.valueCopy(item)); } return setCopy; } if (!(val instanceof Widget)) { // clone for widgets makes no sense here so their clone() function is ignored // with clone() function. E.g. for BaseDoEntity, Dimension, GridData, Insets, Point, Status, URL, etc. const cloneFunction = val['clone']; if (objects.isFunction(cloneFunction)) { return cloneFunction.call(val); // expected to work without arguments and to create a deep clone. } } return val; } function equalsImpl(objA: any, objB: any, useEqualsFunc = true): boolean | null { if (objA === objB) { return true; } // both values are of the same type (which may be null) if (protoTypeOf(objA) !== protoTypeOf(objB)) { return false; // cannot be equal if different type } // dates if (objA instanceof Date) { return dates.equals(objA, objB); } // two empty arrays are equal if (objects.isArray(objA) && !objA.length && !objB.length) { return true; } // two empty maps/sets are equal if ((objA instanceof Map || objA instanceof Set) && !objA.size && !objB.size) { return true; } // both objects have an equals() function if (useEqualsFunc && objects.isFunction(objA?.equals) && objects.isFunction(objB?.equals)) { return objA.equals(objB); } return null; // = false } function equalsMapEntries(entryA: [any, any], entryB: [any, any]): boolean { const keyA = entryA[0]; const valueA = entryA[1]; const keyB = entryB[0]; const valueB = entryB[1]; return objects.equals(keyA, keyB) && objects.equals(valueA, valueB); } function equalsMapEntriesRecursive(entryA: [any, any], entryB: [any, any]): boolean { const keyA = entryA[0]; const valueA = entryA[1]; const keyB = entryB[0]; const valueB = entryB[1]; return objects.equalsRecursive(keyA, keyB) && objects.equalsRecursive(valueA, valueB); } function protoTypeOf(obj: any): any { return objects.isNullOrUndefined(obj) ? null : Object.getPrototypeOf(obj); } function equalsIterable<T>(setA: Iterable<T>, setB: Iterable<T>, equalsFunction: (a: T, b: T) => boolean): boolean { const copyB = Array.from(setB); for (const entry of setA) { const foundAt = arrays.findIndex(copyB, e => equalsFunction(entry, e)); if (foundAt < 0) { return false; } copyB.splice(foundAt, 1); // remove item found } return true; }