@blueprintjs/core
Version:
Core styles & components
209 lines (185 loc) • 6.91 kB
text/typescript
/*
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// we use the empty object {} a lot in this public API
/* eslint-disable @typescript-eslint/no-empty-object-type */
export interface KeyAllowlist<T> {
include: Array<keyof T>;
}
export interface KeyDenylist<T> {
exclude: Array<keyof T>;
}
/**
* Returns true if the arrays are equal. Elements will be shallowly compared by
* default, or they will be compared using the custom `compare` function if one
* is provided.
*/
export function arraysEqual(arrA: any[], arrB: any[], compare = (a: any, b: any) => a === b) {
// treat `null` and `undefined` as the same
if (arrA == null && arrB == null) {
return true;
} else if (arrA == null || arrB == null || arrA.length !== arrB.length) {
return false;
} else {
return arrA.every((a, i) => compare(a, arrB[i]));
}
}
/**
* Shallow comparison between objects. If `keys` is provided, just that subset
* of keys will be compared; otherwise, all keys will be compared.
*
* @returns true if items are equal.
*/
export function shallowCompareKeys<T extends {}>(
objA: T | null | undefined,
objB: T | null | undefined,
keys?: KeyDenylist<T> | KeyAllowlist<T>,
) {
// treat `null` and `undefined` as the same
if (objA == null && objB == null) {
return true;
} else if (objA == null || objB == null) {
return false;
} else if (Array.isArray(objA) || Array.isArray(objB)) {
return false;
} else if (keys != null) {
return shallowCompareKeysImpl(objA, objB, keys);
} else {
// shallowly compare all keys from both objects
const keysA = Object.keys(objA) as Array<keyof T>;
const keysB = Object.keys(objB) as Array<keyof T>;
return (
shallowCompareKeysImpl(objA, objB, { include: keysA }) &&
shallowCompareKeysImpl(objA, objB, { include: keysB })
);
}
}
/**
* Deep comparison between objects. If `keys` is provided, just that subset of
* keys will be compared; otherwise, all keys will be compared.
*
* @returns true if items are equal.
*/
export function deepCompareKeys(objA: any, objB: any, keys?: Array<string | number | symbol>): boolean {
if (objA === objB) {
return true;
} else if (objA == null && objB == null) {
// treat `null` and `undefined` as the same
return true;
} else if (objA == null || objB == null) {
return false;
} else if (Array.isArray(objA) || Array.isArray(objB)) {
return arraysEqual(objA, objB, deepCompareKeys);
} else if (isSimplePrimitiveType(objA) || isSimplePrimitiveType(objB)) {
return objA === objB;
} else if (keys != null) {
return deepCompareKeysImpl(objA, objB, keys);
} else if (objA.constructor !== objB.constructor) {
return false;
} else {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA == null || keysB == null) {
return false;
}
if (keysA.length === 0 && keysB.length === 0) {
return true;
}
return arraysEqual(keysA, keysB) && deepCompareKeysImpl(objA, objB, keysA);
}
}
/**
* Returns a descriptive object for each key whose values are deeply unequal
* between two provided objects. Useful for debugging shouldComponentUpdate.
*/
export function getDeepUnequalKeyValues<T extends {}>(
objA: T = {} as any as T,
objB: T = {} as any as T,
keys?: Array<keyof T>,
) {
const filteredKeys = keys == null ? unionKeys(objA, objB) : keys;
return getUnequalKeyValues(objA, objB, filteredKeys, (a, b, key) => {
return deepCompareKeys(a, b, [key]);
});
}
// Private helpers
// ===============
/**
* Partial shallow comparison between objects using the given list of keys.
*/
function shallowCompareKeysImpl<T extends object>(objA: T, objB: T, keys: KeyDenylist<T> | KeyAllowlist<T>) {
return filterKeys(objA, objB, keys).every(key => {
return objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && objA[key] === objB[key];
});
}
/**
* Partial deep comparison between objects using the given list of keys.
*/
function deepCompareKeysImpl(objA: any, objB: any, keys: Array<string | number | symbol>): boolean {
return keys.every(key => {
return objA.hasOwnProperty(key) === objB.hasOwnProperty(key) && deepCompareKeys(objA[key], objB[key]);
});
}
function isSimplePrimitiveType(value: any) {
return typeof value === "number" || typeof value === "string" || typeof value === "boolean";
}
function filterKeys<T extends object>(objA: T, objB: T, keys: KeyDenylist<T> | KeyAllowlist<T>) {
if (isAllowlist(keys)) {
return keys.include;
} else if (isDenylist(keys)) {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// merge keys from both objects into a big set for quick access
const keySet = arrayToObject(keysA.concat(keysB));
// delete denied keys from the key set
keys.exclude.forEach(key => delete keySet[key]);
// return the remaining keys as an array
return Object.keys(keySet) as Array<keyof T>;
}
return [];
}
function isAllowlist<T>(keys: any): keys is KeyAllowlist<T> {
return keys != null && (keys as KeyAllowlist<T>).include != null;
}
function isDenylist<T>(keys: any): keys is KeyDenylist<T> {
return keys != null && (keys as KeyDenylist<T>).exclude != null;
}
function arrayToObject(arr: any[]) {
return arr.reduce((obj: any, element: any) => {
obj[element] = true;
return obj;
}, {});
}
function getUnequalKeyValues<T extends {}>(
objA: T,
objB: T,
keys: Array<keyof T>,
compareFn: (objA: any, objB: any, key: keyof T) => boolean,
) {
const unequalKeys = keys.filter(key => !compareFn(objA, objB, key));
const unequalKeyValues = unequalKeys.map(key => ({
key,
valueA: objA[key],
valueB: objB[key],
}));
return unequalKeyValues;
}
function unionKeys<T extends {}>(objA: T, objB: T) {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
const concatKeys = keysA.concat(keysB);
const keySet = arrayToObject(concatKeys);
return Object.keys(keySet) as Array<keyof T>;
}