import path from '../../path.js'; import isArray from '../../is-array.js'; import isObject from '../../is-object.js'; import {MUTABLE_ARRAY_METHODS} from '../methods/array.js'; import {MUTABLE_SET_METHODS} from '../methods/set.js'; import {MUTABLE_MAP_METHODS} from '../methods/map.js'; import {IMMUTABLE_OBJECT_METHODS} from '../methods/object.js'; export default class CloneObject { constructor(value, path, argumentsList, hasOnValidate) { this._path = path; this._isChanged = false; this._clonedCache = new Set(); this._hasOnValidate = hasOnValidate; this._changes = hasOnValidate ? [] : null; this.clone = path === undefined ? value : this._shallowClone(value); } static isHandledMethod(name) { return IMMUTABLE_OBJECT_METHODS.has(name); } _shallowClone(value) { let clone = value; if (isObject(value)) { clone = {...value}; } else if (isArray(value) || ArrayBuffer.isView(value)) { clone = [...value]; } else if (value instanceof Date) { clone = new Date(value); } else if (value instanceof Set) { clone = new Set([...value].map(item => this._shallowClone(item))); } else if (value instanceof Map) { clone = new Map(); for (const [key, item] of value.entries()) { clone.set(key, this._shallowClone(item)); } } this._clonedCache.add(clone); return clone; } preferredThisArg(isHandledMethod, name, thisArgument, thisProxyTarget) { if (isHandledMethod) { if (isArray(thisProxyTarget)) { this._onIsChanged = MUTABLE_ARRAY_METHODS[name]; } else if (thisProxyTarget instanceof Set) { this._onIsChanged = MUTABLE_SET_METHODS[name]; } else if (thisProxyTarget instanceof Map) { this._onIsChanged = MUTABLE_MAP_METHODS[name]; } return thisProxyTarget; } return thisArgument; } update(fullPath, property, value) { const changePath = path.after(fullPath, this._path); if (property !== 'length') { let object = this.clone; path.walk(changePath, key => { if (object?.[key]) { if (!this._clonedCache.has(object[key])) { object[key] = this._shallowClone(object[key]); } object = object[key]; } }); if (this._hasOnValidate) { this._changes.push({ path: changePath, property, previous: value, }); } if (object?.[property]) { object[property] = value; } } this._isChanged = true; } undo(object) { let change; for (let index = this._changes.length - 1; index !== -1; index--) { change = this._changes[index]; path.get(object, change.path)[change.property] = change.previous; } } isChanged(value) { return this._onIsChanged === undefined ? this._isChanged : this._onIsChanged(this.clone, value); } isPathApplicable(changePath) { return path.isRootPath(this._path) || path.isSubPath(changePath, this._path); } }