Skip to content

Instantly share code, notes, and snippets.

@js2me
Created March 22, 2021 13:03
Show Gist options
  • Save js2me/08d37d751723ea915a671197e79144a6 to your computer and use it in GitHub Desktop.
Save js2me/08d37d751723ea915a671197e79144a6 to your computer and use it in GitHub Desktop.
Undo redo effecto draft
import { combine, createEvent, createStore, Event, guard, sample, StoreValue } from "effector";
export const createUndoRedo = <S extends unknown>({ defaultState, filter, limit, name }: UndoRedoOptions<S>) => {
const storeName = name || "unknown-store";
const $present = createStore(defaultState, { name: `history/${storeName}/present` });
const limitPerTime = Math.round(limit / 2);
const $past = createStore<S[]>([], { name: `history/${storeName}/past` });
const $future = createStore<S[]>([], { name: `history/${storeName}/future` });
const $history = combine({
past: $past,
present: $present,
future: $future,
});
type HistoryType = StoreValue<typeof $history>;
const undo = createEvent(`history/${storeName}/undo`);
const redo = createEvent(`history/${storeName}/redo`);
const $canUndo = $past.map(s => !!s.length);
const $canRedo = $future.map(s => !!s.length);
$past.watch(fff => console.info("$past", fff));
$canUndo.watch(fff => console.info("can undo", fff));
const resetHistory = createEvent(`history/${storeName}/reset-history`);
const enum TimeTravel {
ToPast = -1,
ToFuture = 1,
}
const timeTravel = createEvent<TimeTravel>(`history/${storeName}/time-travel`);
$past.reset(resetHistory);
$future.reset(resetHistory);
guard({
source: $canUndo,
clock: undo,
filter: source => !!source,
target: timeTravel.prepend(() => TimeTravel.ToPast),
});
guard({
source: $canRedo,
clock: redo,
filter: source => !!source,
target: timeTravel.prepend(() => TimeTravel.ToFuture),
});
const travelToPast = ({ past, present, future }: HistoryType): HistoryType | null => {
const newPast = past.slice();
const newFuture = future.slice();
const newPresent = newPast.pop();
if (filter && !filter(newPresent, present)) return null;
newFuture.unshift(present);
if (newFuture.length >= limitPerTime) {
newFuture.pop();
}
return {
past: newPast,
present: newPresent,
future: newFuture,
};
};
const travelToFuture = ({ past, present, future }: HistoryType): HistoryType | null => {
const newPast = past.slice();
const newFuture = future.slice();
const newPresent = newFuture.shift();
if (filter && !filter(newPresent, present)) return null;
newPast.push(present);
if (newPast.length >= limitPerTime) {
newPast.shift();
}
return {
past: newPast,
present: newPresent,
future: newFuture,
};
};
const historyUpdate = guard({
source: sample({
source: $history,
clock: timeTravel,
fn: (history, travelTo): HistoryType | null =>
travelTo === TimeTravel.ToPast ? travelToPast(history) : travelToFuture(history),
}),
filter: (source): source is HistoryType => source !== null,
});
sample({
source: historyUpdate,
fn: ({ present }) => present,
target: $present,
});
sample({
source: historyUpdate,
fn: ({ past }) => past,
target: $past,
});
sample({
source: historyUpdate,
fn: ({ future }) => future,
target: $future,
});
const $store = createStore(defaultState, { name });
let isInternalUpdate = false;
$present.watch(() => {
isInternalUpdate = true;
});
const clearFuture = createEvent();
const clearFutureUpdate = sample({
source: [$history, $store],
clock: clearFuture,
fn: ([history, currState]): HistoryType => {
const newPast = history.past.slice();
newPast.push(history.present);
if (newPast.length >= limitPerTime) {
newPast.shift();
}
return {
past: newPast,
present: currState,
future: [],
};
},
});
clearFutureUpdate.watch(() => {
isInternalUpdate = true;
});
sample({
source: $present,
target: $store,
}).watch(() => {
isInternalUpdate = false;
});
sample({
source: clearFutureUpdate,
fn: ({ present }) => present,
target: $present,
});
sample({
source: clearFutureUpdate,
fn: ({ past }) => past,
target: $past,
});
sample({
source: clearFutureUpdate,
fn: ({ future }) => future,
target: $future,
});
guard({
source: sample({
source: $store,
fn: () => $future.defaultState,
}),
filter: () => !isInternalUpdate,
target: $future,
});
$store.watch(() => {
if (!isInternalUpdate) {
clearFuture();
}
});
return {
canUndo: $canUndo,
canRedo: $canRedo,
undo,
redo,
history: $history,
store: $store,
reset: resetHistory,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment