293 lines
8.2 KiB
JavaScript
293 lines
8.2 KiB
JavaScript
/* eslint-disable unicorn/prefer-spread */
|
|
import {TARGET, UNSUBSCRIBE, PATH_SEPARATOR} from './lib/constants.js';
|
|
import {isBuiltinWithMutableMethods, isBuiltinWithoutMutableMethods} from './lib/is-builtin.js';
|
|
import path from './lib/path.js';
|
|
import isArray from './lib/is-array.js';
|
|
import isSymbol from './lib/is-symbol.js';
|
|
import isIterator from './lib/is-iterator.js';
|
|
import wrapIterator from './lib/wrap-iterator.js';
|
|
import ignoreProperty from './lib/ignore-property.js';
|
|
import Cache from './lib/cache.js';
|
|
import SmartClone from './lib/smart-clone/smart-clone.js';
|
|
|
|
const defaultOptions = {
|
|
equals: Object.is,
|
|
isShallow: false,
|
|
pathAsArray: false,
|
|
ignoreSymbols: false,
|
|
ignoreUnderscores: false,
|
|
ignoreDetached: false,
|
|
details: false,
|
|
};
|
|
|
|
const onChange = (object, onChange, options = {}) => {
|
|
options = {
|
|
...defaultOptions,
|
|
...options,
|
|
};
|
|
|
|
const proxyTarget = Symbol('ProxyTarget');
|
|
const {equals, isShallow, ignoreDetached, details} = options;
|
|
const cache = new Cache(equals);
|
|
const hasOnValidate = typeof options.onValidate === 'function';
|
|
const smartClone = new SmartClone(hasOnValidate);
|
|
|
|
// eslint-disable-next-line max-params
|
|
const validate = (target, property, value, previous, applyData) => !hasOnValidate
|
|
|| smartClone.isCloning
|
|
|| options.onValidate(path.concat(cache.getPath(target), property), value, previous, applyData) === true;
|
|
|
|
const handleChangeOnTarget = (target, property, value, previous) => {
|
|
if (
|
|
!ignoreProperty(cache, options, property)
|
|
&& !(ignoreDetached && cache.isDetached(target, object))
|
|
) {
|
|
handleChange(cache.getPath(target), property, value, previous);
|
|
}
|
|
};
|
|
|
|
// eslint-disable-next-line max-params
|
|
const handleChange = (changePath, property, value, previous, applyData) => {
|
|
if (smartClone.isCloning && smartClone.isPartOfClone(changePath)) {
|
|
smartClone.update(changePath, property, previous);
|
|
} else {
|
|
onChange(path.concat(changePath, property), value, previous, applyData);
|
|
}
|
|
};
|
|
|
|
const getProxyTarget = value => value
|
|
? (value[proxyTarget] ?? value)
|
|
: value;
|
|
|
|
const prepareValue = (value, target, property, basePath) => {
|
|
if (
|
|
isBuiltinWithoutMutableMethods(value)
|
|
|| property === 'constructor'
|
|
|| (isShallow && !SmartClone.isHandledMethod(target, property))
|
|
|| ignoreProperty(cache, options, property)
|
|
|| cache.isGetInvariant(target, property)
|
|
|| (ignoreDetached && cache.isDetached(target, object))
|
|
) {
|
|
return value;
|
|
}
|
|
|
|
if (basePath === undefined) {
|
|
basePath = cache.getPath(target);
|
|
}
|
|
|
|
/*
|
|
Check for circular references.
|
|
|
|
If the value already has a corresponding path/proxy,
|
|
and if the path corresponds to one of the parents,
|
|
then we are on a circular case, where the child is pointing to their parent.
|
|
In this case we return the proxy object with the shortest path.
|
|
*/
|
|
const childPath = path.concat(basePath, property);
|
|
const existingPath = cache.getPath(value);
|
|
|
|
if (existingPath && isSameObjectTree(childPath, existingPath)) {
|
|
// We are on the same object tree but deeper, so we use the parent path.
|
|
return cache.getProxy(value, existingPath, handler, proxyTarget);
|
|
}
|
|
|
|
return cache.getProxy(value, childPath, handler, proxyTarget);
|
|
};
|
|
|
|
/*
|
|
Returns true if `childPath` is a subpath of `existingPath`
|
|
(if childPath starts with existingPath). Otherwise, it returns false.
|
|
|
|
It also returns false if the 2 paths are identical.
|
|
|
|
For example:
|
|
- childPath = group.layers.0.parent.layers.0.value
|
|
- existingPath = group.layers.0.parent
|
|
*/
|
|
const isSameObjectTree = (childPath, existingPath) => {
|
|
if (isSymbol(childPath) || childPath.length <= existingPath.length) {
|
|
return false;
|
|
}
|
|
|
|
if (isArray(existingPath) && existingPath.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const childParts = isArray(childPath) ? childPath : childPath.split(PATH_SEPARATOR);
|
|
const existingParts = isArray(existingPath) ? existingPath : existingPath.split(PATH_SEPARATOR);
|
|
|
|
if (childParts.length <= existingParts.length) {
|
|
return false;
|
|
}
|
|
|
|
return !(existingParts.some((part, index) => part !== childParts[index]));
|
|
};
|
|
|
|
const handler = {
|
|
get(target, property, receiver) {
|
|
if (isSymbol(property)) {
|
|
if (property === proxyTarget || property === TARGET) {
|
|
return target;
|
|
}
|
|
|
|
if (
|
|
property === UNSUBSCRIBE
|
|
&& !cache.isUnsubscribed
|
|
&& cache.getPath(target).length === 0
|
|
) {
|
|
cache.unsubscribe();
|
|
return target;
|
|
}
|
|
}
|
|
|
|
const value = isBuiltinWithMutableMethods(target)
|
|
? Reflect.get(target, property)
|
|
: Reflect.get(target, property, receiver);
|
|
|
|
return prepareValue(value, target, property);
|
|
},
|
|
|
|
set(target, property, value, receiver) {
|
|
value = getProxyTarget(value);
|
|
|
|
const reflectTarget = target[proxyTarget] ?? target;
|
|
const previous = reflectTarget[property];
|
|
|
|
if (equals(previous, value) && property in target) {
|
|
return true;
|
|
}
|
|
|
|
const isValid = validate(target, property, value, previous);
|
|
|
|
if (
|
|
isValid
|
|
&& cache.setProperty(reflectTarget, property, value, receiver, previous)
|
|
) {
|
|
handleChangeOnTarget(target, property, target[property], previous);
|
|
|
|
return true;
|
|
}
|
|
|
|
return !isValid;
|
|
},
|
|
|
|
defineProperty(target, property, descriptor) {
|
|
if (!cache.isSameDescriptor(descriptor, target, property)) {
|
|
const previous = target[property];
|
|
|
|
if (
|
|
validate(target, property, descriptor.value, previous)
|
|
&& cache.defineProperty(target, property, descriptor, previous)
|
|
) {
|
|
handleChangeOnTarget(target, property, descriptor.value, previous);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
deleteProperty(target, property) {
|
|
if (!Reflect.has(target, property)) {
|
|
return true;
|
|
}
|
|
|
|
const previous = Reflect.get(target, property);
|
|
const isValid = validate(target, property, undefined, previous);
|
|
|
|
if (
|
|
isValid
|
|
&& cache.deleteProperty(target, property, previous)
|
|
) {
|
|
handleChangeOnTarget(target, property, undefined, previous);
|
|
|
|
return true;
|
|
}
|
|
|
|
return !isValid;
|
|
},
|
|
|
|
apply(target, thisArg, argumentsList) {
|
|
const thisProxyTarget = thisArg[proxyTarget] ?? thisArg;
|
|
|
|
if (cache.isUnsubscribed) {
|
|
return Reflect.apply(target, thisProxyTarget, argumentsList);
|
|
}
|
|
|
|
if (
|
|
(details === false
|
|
|| (details !== true && !details.includes(target.name)))
|
|
&& SmartClone.isHandledType(thisProxyTarget)
|
|
) {
|
|
let applyPath = path.initial(cache.getPath(target));
|
|
const isHandledMethod = SmartClone.isHandledMethod(thisProxyTarget, target.name);
|
|
|
|
smartClone.start(thisProxyTarget, applyPath, argumentsList);
|
|
|
|
let result = Reflect.apply(
|
|
target,
|
|
smartClone.preferredThisArg(target, thisArg, thisProxyTarget),
|
|
isHandledMethod
|
|
? argumentsList.map(argument => getProxyTarget(argument))
|
|
: argumentsList,
|
|
);
|
|
|
|
const isChanged = smartClone.isChanged(thisProxyTarget, equals);
|
|
const previous = smartClone.stop();
|
|
|
|
if (SmartClone.isHandledType(result) && isHandledMethod) {
|
|
if (thisArg instanceof Map && target.name === 'get') {
|
|
applyPath = path.concat(applyPath, argumentsList[0]);
|
|
}
|
|
|
|
result = cache.getProxy(result, applyPath, handler);
|
|
}
|
|
|
|
if (isChanged) {
|
|
const applyData = {
|
|
name: target.name,
|
|
args: argumentsList,
|
|
result,
|
|
};
|
|
const changePath = smartClone.isCloning
|
|
? path.initial(applyPath)
|
|
: applyPath;
|
|
const property = smartClone.isCloning
|
|
? path.last(applyPath)
|
|
: '';
|
|
|
|
if (validate(path.get(object, changePath), property, thisProxyTarget, previous, applyData)) {
|
|
handleChange(changePath, property, thisProxyTarget, previous, applyData);
|
|
} else {
|
|
smartClone.undo(thisProxyTarget);
|
|
}
|
|
}
|
|
|
|
if (
|
|
(thisArg instanceof Map || thisArg instanceof Set)
|
|
&& isIterator(result)
|
|
) {
|
|
return wrapIterator(result, target, thisArg, applyPath, prepareValue);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return Reflect.apply(target, thisArg, argumentsList);
|
|
},
|
|
};
|
|
|
|
const proxy = cache.getProxy(object, options.pathAsArray ? [] : '', handler);
|
|
onChange = onChange.bind(proxy);
|
|
|
|
if (hasOnValidate) {
|
|
options.onValidate = options.onValidate.bind(proxy);
|
|
}
|
|
|
|
return proxy;
|
|
};
|
|
|
|
onChange.target = proxy => proxy?.[TARGET] ?? proxy;
|
|
onChange.unsubscribe = proxy => proxy?.[UNSUBSCRIBE] ?? proxy;
|
|
|
|
export default onChange;
|