|
@@ -1,38 +1,40 @@
|
|
|
|
+/** @type {boolean} */
|
|
|
|
+let deepDiff;
|
|
|
|
+
|
|
export function normalizeKeys(key) {
|
|
export function normalizeKeys(key) {
|
|
if (key == null) return [];
|
|
if (key == null) return [];
|
|
if (Array.isArray(key)) return key;
|
|
if (Array.isArray(key)) return key;
|
|
return `${key}`.split('.').filter(Boolean);
|
|
return `${key}`.split('.').filter(Boolean);
|
|
}
|
|
}
|
|
|
|
|
|
-export function objectGet(obj, rawKey, def) {
|
|
|
|
- const keys = normalizeKeys(rawKey);
|
|
|
|
- let res = obj;
|
|
|
|
- keys.every((key) => {
|
|
|
|
- if (res && typeof res === 'object' && (key in res)) {
|
|
|
|
- res = res[key];
|
|
|
|
- return true;
|
|
|
|
- }
|
|
|
|
- res = def;
|
|
|
|
- return false;
|
|
|
|
- });
|
|
|
|
- return res;
|
|
|
|
|
|
+export function objectGet(obj, rawKey) {
|
|
|
|
+ for (const key of normalizeKeys(rawKey)) {
|
|
|
|
+ if (!obj || typeof obj !== 'object') break;
|
|
|
|
+ obj = obj[key];
|
|
|
|
+ }
|
|
|
|
+ return obj;
|
|
}
|
|
}
|
|
|
|
|
|
-export function objectSet(obj, rawKey, val) {
|
|
|
|
- const keys = normalizeKeys(rawKey);
|
|
|
|
- if (!keys.length) return val;
|
|
|
|
- const root = obj || {};
|
|
|
|
- let sub = root;
|
|
|
|
- const lastKey = keys.pop();
|
|
|
|
- keys.forEach((key) => {
|
|
|
|
- sub = sub[key] || (sub[key] = {});
|
|
|
|
- });
|
|
|
|
- if (typeof val === 'undefined') {
|
|
|
|
- delete sub[lastKey];
|
|
|
|
|
|
+/**
|
|
|
|
+ * @param {Object} [obj = {}]
|
|
|
|
+ * @param {string|string[]} [rawKey]
|
|
|
|
+ * @param {?} [val] - if `undefined` or omitted the value is deleted
|
|
|
|
+ * @param {boolean} [retParent]
|
|
|
|
+ * @return {Object} the original object or the parent of `val` if retParent is set
|
|
|
|
+ */
|
|
|
|
+export function objectSet(obj, rawKey, val, retParent) {
|
|
|
|
+ rawKey = normalizeKeys(rawKey);
|
|
|
|
+ let res = obj || {};
|
|
|
|
+ let key;
|
|
|
|
+ for (let i = 0; (key = rawKey[i], i < rawKey.length - 1); i += 1) {
|
|
|
|
+ res = res[key] || (res[key] = {});
|
|
|
|
+ }
|
|
|
|
+ if (val === undefined) {
|
|
|
|
+ delete res[key];
|
|
} else {
|
|
} else {
|
|
- sub[lastKey] = val;
|
|
|
|
|
|
+ res[key] = val;
|
|
}
|
|
}
|
|
- return root;
|
|
|
|
|
|
+ return retParent ? res : obj;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -42,20 +44,30 @@ export function objectSet(obj, rawKey, val) {
|
|
* @returns {{}}
|
|
* @returns {{}}
|
|
*/
|
|
*/
|
|
export function objectPick(obj, keys, transform) {
|
|
export function objectPick(obj, keys, transform) {
|
|
- return keys.reduce((res, key) => {
|
|
|
|
|
|
+ const res = {};
|
|
|
|
+ for (const key of keys) {
|
|
let value = obj?.[key];
|
|
let value = obj?.[key];
|
|
if (transform) value = transform(value, key);
|
|
if (transform) value = transform(value, key);
|
|
- if (value != null) res[key] = value;
|
|
|
|
- return res;
|
|
|
|
- }, {});
|
|
|
|
|
|
+ if (value !== undefined) res[key] = value;
|
|
|
|
+ }
|
|
|
|
+ return res;
|
|
}
|
|
}
|
|
|
|
|
|
-// invoked as obj::mapEntry(([key, value], i, allEntries) => transformedValue)
|
|
|
|
-export function mapEntry(func) {
|
|
|
|
- return Object.entries(this).reduce((res, entry, i, allEntries) => {
|
|
|
|
- res[entry[0]] = func(entry, i, allEntries);
|
|
|
|
- return res;
|
|
|
|
- }, {});
|
|
|
|
|
|
+/**
|
|
|
|
+ * @param {function} [fnValue] - (value, newKey, obj) => newValue
|
|
|
|
+ * @param {function} [fnKey] - (key, val, obj) => newKey (if newKey is falsy the key is skipped)
|
|
|
|
+ * @param {Object} [thisObj] - passed as `this` to both functions
|
|
|
|
+ * @return {Object}
|
|
|
|
+ */
|
|
|
|
+export function mapEntry(fnValue, fnKey, thisObj) {
|
|
|
|
+ const res = {};
|
|
|
|
+ for (let key of Object.keys(this)) {
|
|
|
|
+ const val = this[key];
|
|
|
|
+ if (!fnKey || (key = thisObj::fnKey(key, val, this))) {
|
|
|
|
+ res[key] = fnValue ? thisObj::fnValue(val, key, this) : val;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return res;
|
|
}
|
|
}
|
|
|
|
|
|
// invoked as obj::forEachEntry(([key, value], i, allEntries) => {})
|
|
// invoked as obj::forEachEntry(([key, value], i, allEntries) => {})
|
|
@@ -73,16 +85,12 @@ export function forEachValue(func, thisObj) {
|
|
if (this) Object.values(this).forEach(func, thisObj);
|
|
if (this) Object.values(this).forEach(func, thisObj);
|
|
}
|
|
}
|
|
|
|
|
|
-// Needed for Firefox's browser.storage API which fails on Vue observables
|
|
|
|
export function deepCopy(src) {
|
|
export function deepCopy(src) {
|
|
- return src && (
|
|
|
|
- /* Not using `map` because its result belongs to the `window` of the source,
|
|
|
|
- * so it becomes "dead object" in Firefox after GC collects it. */
|
|
|
|
- Array.isArray(src) && Array.from(src, deepCopy)
|
|
|
|
- // Used in safe context
|
|
|
|
- // eslint-disable-next-line no-restricted-syntax
|
|
|
|
- || typeof src === 'object' && src::mapEntry(([, val]) => deepCopy(val))
|
|
|
|
- ) || src;
|
|
|
|
|
|
+ if (!src || typeof src !== 'object') return src;
|
|
|
|
+ /* Not using `map` because its result belongs to the `window` of the source,
|
|
|
|
+ * so it becomes "dead object" in Firefox after GC collects it. */
|
|
|
|
+ if (Array.isArray(src)) return Array.from(src, deepCopy);
|
|
|
|
+ return src::mapEntry(deepCopy);
|
|
}
|
|
}
|
|
|
|
|
|
// Simplified deep equality checker
|
|
// Simplified deep equality checker
|
|
@@ -91,8 +99,7 @@ export function deepEqual(a, b) {
|
|
if (!a || !b || typeof a !== typeof b || typeof a !== 'object') {
|
|
if (!a || !b || typeof a !== typeof b || typeof a !== 'object') {
|
|
res = a === b;
|
|
res = a === b;
|
|
} else if (Array.isArray(a)) {
|
|
} else if (Array.isArray(a)) {
|
|
- res = a.length === b.length
|
|
|
|
- && a.every((item, i) => deepEqual(item, b[i]));
|
|
|
|
|
|
+ res = a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
} else {
|
|
} else {
|
|
const keysA = Object.keys(a);
|
|
const keysA = Object.keys(a);
|
|
const keysB = Object.keys(b);
|
|
const keysB = Object.keys(b);
|
|
@@ -101,3 +108,38 @@ export function deepEqual(a, b) {
|
|
}
|
|
}
|
|
return res;
|
|
return res;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+/** @return {?} `undefined` if equal */
|
|
|
|
+export function deepCopyDiff(src, sample) {
|
|
|
|
+ if (src === sample) return;
|
|
|
|
+ if (!src || typeof src !== 'object') return src;
|
|
|
|
+ if (!sample || typeof sample !== 'object') return deepCopy(src);
|
|
|
|
+ if ((deepDiff = false, src = deepCopyDiffObjects(src, sample), deepDiff)) return src;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function deepCopyDiffObjects(src, sample) {
|
|
|
|
+ const isArr = Array.isArray(src);
|
|
|
|
+ const arr1 = isArr ? src : Object.keys(src);
|
|
|
|
+ const arr2 = isArr ? sample : Object.keys(sample);
|
|
|
|
+ const res = isArr ? [] : {};
|
|
|
|
+ if (arr1.length !== arr2.length) {
|
|
|
|
+ deepDiff = true;
|
|
|
|
+ }
|
|
|
|
+ for (let i = 0, key, a, b; i < arr1.length; i += 1) {
|
|
|
|
+ key = isArr ? i : arr1[i];
|
|
|
|
+ a = src[key];
|
|
|
|
+ b = isArr || arr2.includes(key) ? sample[key] : !a;
|
|
|
|
+ if (a && typeof a === 'object') {
|
|
|
|
+ if (b && typeof b === 'object') {
|
|
|
|
+ a = deepCopyDiffObjects(a, b);
|
|
|
|
+ } else {
|
|
|
|
+ a = deepCopy(a);
|
|
|
|
+ deepDiff = true;
|
|
|
|
+ }
|
|
|
|
+ } else if (a !== b) {
|
|
|
|
+ deepDiff = true;
|
|
|
|
+ }
|
|
|
|
+ res[key] = a;
|
|
|
|
+ }
|
|
|
|
+ return res;
|
|
|
|
+}
|