diff --git a/packages/core/package.json b/packages/core/package.json index 838879f5d..b33db3ed8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,6 +30,7 @@ "url": "https://github.com/GrapesJS/grapesjs.git" }, "dependencies": { + "immer": "^10.1.1", "@types/backbone": "1.4.15", "backbone": "1.4.1", "backbone-undo": "0.2.6", diff --git a/packages/core/src/patch_manager/ModelWithPatches.ts b/packages/core/src/patch_manager/ModelWithPatches.ts new file mode 100644 index 000000000..2a1926490 --- /dev/null +++ b/packages/core/src/patch_manager/ModelWithPatches.ts @@ -0,0 +1,88 @@ +import { enablePatches, produceWithPatches } from 'immer'; +import EditorModel from '../editor/model/Editor'; +import { Model, ObjectHash, SetOptions } from '../common'; +import { serialize } from '../utils/mixins'; +import PatchManager, { PatchChangeProps, PatchPath } from './index'; + +enablePatches(); + +type SetArgs = { + attrs: Partial; + opts: SetOptions; +}; + +const normalizeSetArgs = (args: any[]): SetArgs => { + const [first, second, third] = args; + + if (typeof first === 'string') { + return { + attrs: { [first]: second } as any, + opts: (third || {}) as SetOptions, + }; + } + + return { + attrs: (first || {}) as Partial, + opts: (second || {}) as SetOptions, + }; +}; + +const normalizePatchPaths = (patches: PatchChangeProps[], prefix: PatchPath): PatchChangeProps[] => + patches.map((patch) => ({ + ...patch, + path: [...prefix, ...patch.path], + ...(patch.from ? { from: [...prefix, ...patch.from] } : {}), + })); + +const syncDraftToState = (draft: any, target: any) => { + Object.keys(draft).forEach((key) => { + if (!(key in target)) { + delete draft[key]; + } + }); + + Object.keys(target).forEach((key) => { + draft[key] = target[key]; + }); +}; + +export default class ModelWithPatches extends Model { + em?: EditorModel; + patchObjectType?: string; + + protected get patchManager(): PatchManager | undefined { + const pm = (this.em as any)?.Patches as PatchManager | undefined; + return pm?.isEnabled && this.patchObjectType ? pm : undefined; + } + + protected getPatchObjectId(): string | number | undefined { + const id = (this as any).id ?? (this as any).get?.('id'); + return id ?? (this as any).cid; + } + + set(...args: any[]): this { + const pm = this.patchManager; + const objectId = this.getPatchObjectId(); + + if (!pm || !objectId) { + return (super.set as any).apply(this, args); + } + + const { attrs, opts } = normalizeSetArgs(args); + const beforeState = serialize(this.attributes || {}); + const result = super.set(attrs as any, opts as any); + const afterState = serialize(this.attributes || {}); + const [, patches, inversePatches] = produceWithPatches(beforeState, (draft: any) => { + syncDraftToState(draft, afterState); + }); + + if (patches.length || inversePatches.length) { + const prefix: PatchPath = [this.patchObjectType as string, objectId, 'attributes']; + const activePatch = pm.createOrGetCurrentPatch(); + activePatch.changes.push(...normalizePatchPaths(patches, prefix)); + activePatch.reverseChanges.push(...normalizePatchPaths(inversePatches, prefix)); + } + + return result; + } +} diff --git a/packages/core/src/patch_manager/index.ts b/packages/core/src/patch_manager/index.ts index 7e850a4a6..017d503c8 100644 --- a/packages/core/src/patch_manager/index.ts +++ b/packages/core/src/patch_manager/index.ts @@ -2,11 +2,13 @@ import { createId } from '../utils/mixins'; export type PatchOp = 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test'; +export type PatchPath = Array; + export type PatchChangeProps = { op: PatchOp; - path: string; + path: PatchPath; value?: any; - from?: string; + from?: PatchPath; }; export type PatchProps = {