Skip to content

Commit

Permalink
✨ feat: support undo/redo for ProEditor
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Jun 13, 2023
1 parent 30dcf24 commit cec5611
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 13 deletions.
16 changes: 15 additions & 1 deletion src/ProEditor/container/StoreUpdater.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isEqual from 'fast-deep-equal';
import { memo, MutableRefObject, useImperativeHandle } from 'react';
import { createStoreUpdater } from 'zustand-utils';

Expand Down Expand Up @@ -27,9 +28,22 @@ const StoreUpdater = memo(
}: StoreUpdaterProps) => {
const storeApi = useStoreApi();
const useStoreUpdater = createStoreUpdater(storeApi);
const { yjsDoc } = storeApi.getState();

// 结合 yjs 进行变更
const useUpdateWithYjs = (key: 'config', value: any) => {
useStoreUpdater(key, value, [value], (partialNewState) => {
// 如果相等,不需要更新
if (isEqual(value, storeApi.getState()[key])) return;

storeApi.setState(partialNewState);

yjsDoc.updateHistoryData(partialNewState);
});
};

useStoreUpdater('mode', mode);
useStoreUpdater('config', config);
useUpdateWithYjs('config', config);
useStoreUpdater('assetAwareness', assetAwareness);
useStoreUpdater('editorAwareness', editorAwareness);

Expand Down
7 changes: 5 additions & 2 deletions src/ProEditor/hooks/useProEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export interface ProEditorInstance<Config = any, Props = any> extends PublicProE
export const useProEditor = <T>(): ProEditorInstance<T> => {
const storeApi = useStoreApi();

const { deselectCanvas, updateConfig, exportConfig, resetConfig } = storeApi.getState();
const { deselectCanvas, updateConfig, exportConfig, resetConfig, undo, redo } =
storeApi.getState();

const getViewport = useMemoizedFn(() => storeApi.getState().editorAwareness.viewport);
const getEditorAwareness = useMemoizedFn(() => storeApi.getState().editorAwareness);
Expand All @@ -28,11 +29,13 @@ export const useProEditor = <T>(): ProEditorInstance<T> => {
deselectCanvas,
exportConfig,
resetConfig,
undo,
redo,
getViewport,
getConfig,
getProps,
getEditorAwareness,
}),
[updateConfig, deselectCanvas, exportConfig],
[],
);
};
34 changes: 27 additions & 7 deletions src/ProEditor/store/slices/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import merge from 'lodash.merge';
import { StateCreator } from 'zustand';

import { ComponentAsset } from '@/ComponentAsset';

import { DocWithHistoryManager, UserActionParams } from '../../utils/yjs';
import { InternalProEditorStore } from '../createStore';

// ======== state ======== //
Expand Down Expand Up @@ -30,6 +32,7 @@ export interface ConfigPublicState<Config = any> {
export interface ConfigSliceState extends ConfigPublicState {
/** 组件的 props */
props?: any;
yjsDoc: DocWithHistoryManager<{ config: any }>;
}

const initialConfigState: ConfigSliceState = {
Expand All @@ -40,24 +43,35 @@ const initialConfigState: ConfigSliceState = {
config: null,
onConfigChange: null,
props: {},
yjsDoc: new DocWithHistoryManager<{ config: any }>(),
};

// ======== action ======== //

export interface ActionPayload {
type: string;
payload: any;
}

export interface ActionOptions {
recordHistory?: boolean;
payload?: Partial<UserActionParams>;
}

export interface ConfigPublicAction {
/**
* 导出配置
*/
exportConfig: () => void;
resetConfig: () => void;
updateConfig: <T>(config: Partial<T>) => void;
updateConfig: <T>(config: Partial<T>, options?: ActionOptions) => void;
}

export interface ConfigSlice extends ConfigPublicAction, ConfigSliceState {
/**
* 内部更新配置
**/
internalUpdateConfig: <T>(config: Partial<T>) => void;
internalUpdateConfig: <T>(config: Partial<T>, payload?: ActionPayload) => void;
}

export const configSlice: StateCreator<
Expand All @@ -74,12 +88,12 @@ export const configSlice: StateCreator<
* 内部修改 config 方法
* 传给 ProTableStore 进行 config 同步
*/
internalUpdateConfig: (config) => {
internalUpdateConfig: (config, payload) => {
const { onConfigChange, componentAsset } = get();

const nextConfig = { ...get().config, ...config };
const nextConfig = merge({}, get().config, config);

set({ config: nextConfig }, false, '🕹内部更新:config');
set({ config: nextConfig }, false, payload);

onConfigChange?.({
config: nextConfig,
Expand All @@ -99,7 +113,13 @@ export const configSlice: StateCreator<
document.body.removeChild(eleLink);
},

updateConfig: (config) => {
get().internalUpdateConfig(config);
updateConfig: (config, action) => {
get().internalUpdateConfig(config, { type: '外部 updateConfig 更新', payload: config });

const useAction = merge({}, { recordHistory: true }, action);

if (useAction.recordHistory) {
get().yjsDoc.recordHistoryData({ config }, { ...useAction.payload, timestamp: Date.now() });
}
},
});
26 changes: 23 additions & 3 deletions src/ProEditor/store/slices/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ const initialGeneralState: GeneralSliceState = {

// ======== action ======== //

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GeneralPublicAction {}
export interface GeneralPublicAction {
undo: () => void;
redo: () => void;
}

export interface GeneralSlice extends GeneralPublicAction, GeneralSliceState {}

Expand All @@ -40,6 +42,24 @@ export const generalSlice: StateCreator<
[['zustand/devtools', never]],
[],
GeneralSlice
> = () => ({
> = (set, get) => ({
...initialGeneralState,

undo: () => {
const { yjsDoc, internalUpdateConfig } = get();
const stack = yjsDoc.undo();

const { config } = yjsDoc.getHistoryJSON();

internalUpdateConfig(config, { type: 'history/undo', payload: stack });
},
redo: () => {
const { yjsDoc, internalUpdateConfig } = get();

const stack = yjsDoc.redo();

const { config } = yjsDoc.getHistoryJSON();

internalUpdateConfig(config, { type: 'history/redo', payload: stack });
},
});
65 changes: 65 additions & 0 deletions src/ProEditor/utils/yjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Doc, UndoManager } from 'yjs';
import { DocOpts } from 'yjs/dist/src/utils/Doc';

export interface UserActionParams {
type: string;
name: string;
timestamp: number;
}

class UserAction {
type;
name;
timestamp;
constructor(params: UserActionParams) {
this.type = params.type;
this.name = params.name;
this.timestamp = params.timestamp;
}
}

export class DocWithHistoryManager<T = object> extends Doc {
private _internalHistoryKey = '__INTERNAL_HISTORY_MAP__';

constructor(params?: DocOpts) {
super(params);

this.undoManager = new UndoManager(this.getHistoryMap(), {
trackedOrigins: new Set([UserAction]),
});
}

public undoManager: UndoManager;

updateHistoryData = (value: Partial<T>) => {
const map = this.getMap(this._internalHistoryKey);

Object.entries(value).forEach(([key, value]) => {
map.set(key, value);
});
};

recordHistoryData = (value: Partial<T>, userAction: UserActionParams) => {
this.transact(() => {
this.updateHistoryData(value);
}, new UserAction(userAction));
};

getHistoryMap = () => {
return this.getMap(this._internalHistoryKey);
};

getHistoryJSON = () => {
const map = this.getMap(this._internalHistoryKey);

return map.toJSON() as T;
};

redo = () => {
return this.undoManager.redo();
};

undo = () => {
return this.undoManager.undo();
};
}

0 comments on commit cec5611

Please sign in to comment.