fix history (#1009)

* fix history

* tweak withBatchedUpdates typing
This commit is contained in:
David Luzar 2020-03-19 14:51:05 +01:00 committed by GitHub
parent ca5f37850e
commit 82ce068972
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 86 additions and 80 deletions

View File

@ -14,7 +14,10 @@ import { newElementWith } from "../element/mutateElement";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor", name: "changeViewBackgroundColor",
perform: (_, appState, value) => { perform: (_, appState, value) => {
return { appState: { ...appState, viewBackgroundColor: value } }; return {
appState: { ...appState, viewBackgroundColor: value },
commitToHistory: true,
};
}, },
PanelComponent: ({ appState, updateData }) => { PanelComponent: ({ appState, updateData }) => {
return ( return (
@ -28,18 +31,17 @@ export const actionChangeViewBackgroundColor = register({
</div> </div>
); );
}, },
commitToHistory: () => true,
}); });
export const actionClearCanvas = register({ export const actionClearCanvas = register({
name: "clearCanvas", name: "clearCanvas",
commitToHistory: () => true,
perform: elements => { perform: elements => {
return { return {
elements: elements.map(element => elements: elements.map(element =>
newElementWith(element, { isDeleted: true }), newElementWith(element, { isDeleted: true }),
), ),
appState: getDefaultAppState(), appState: getDefaultAppState(),
commitToHistory: true,
}; };
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
@ -81,6 +83,7 @@ export const actionZoomIn = register({
...appState, ...appState,
zoom: getNormalizedZoom(appState.zoom + ZOOM_STEP), zoom: getNormalizedZoom(appState.zoom + ZOOM_STEP),
}, },
commitToHistory: false,
}; };
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
@ -107,6 +110,7 @@ export const actionZoomOut = register({
...appState, ...appState,
zoom: getNormalizedZoom(appState.zoom - ZOOM_STEP), zoom: getNormalizedZoom(appState.zoom - ZOOM_STEP),
}, },
commitToHistory: false,
}; };
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
@ -133,6 +137,7 @@ export const actionResetZoom = register({
...appState, ...appState,
zoom: 1, zoom: 1,
}, },
commitToHistory: false,
}; };
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (

View File

@ -20,12 +20,11 @@ export const actionDeleteSelected = register({
elementType: "selection", elementType: "selection",
multiElement: null, multiElement: null,
}, },
commitToHistory: isSomeElementSelected(elements, appState),
}; };
}, },
contextItemLabel: "labels.delete", contextItemLabel: "labels.delete",
contextMenuOrder: 3, contextMenuOrder: 3,
commitToHistory: (appState, elements) =>
isSomeElementSelected(elements, appState),
keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE, keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton

View File

@ -23,6 +23,7 @@ export const actionDuplicateSelection = register({
}, },
[], [],
), ),
commitToHistory: true,
}; };
}, },
contextItemLabel: "labels.duplicateSelection", contextItemLabel: "labels.duplicateSelection",

View File

@ -10,7 +10,7 @@ import { register } from "./register";
export const actionChangeProjectName = register({ export const actionChangeProjectName = register({
name: "changeProjectName", name: "changeProjectName",
perform: (_elements, appState, value) => { perform: (_elements, appState, value) => {
return { appState: { ...appState, name: value } }; return { appState: { ...appState, name: value }, commitToHistory: false };
}, },
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData }) => (
<ProjectName <ProjectName
@ -24,7 +24,10 @@ export const actionChangeProjectName = register({
export const actionChangeExportBackground = register({ export const actionChangeExportBackground = register({
name: "changeExportBackground", name: "changeExportBackground",
perform: (_elements, appState, value) => { perform: (_elements, appState, value) => {
return { appState: { ...appState, exportBackground: value } }; return {
appState: { ...appState, exportBackground: value },
commitToHistory: false,
};
}, },
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData }) => (
<label> <label>
@ -42,7 +45,7 @@ export const actionSaveScene = register({
name: "saveScene", name: "saveScene",
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
saveAsJSON(elements, appState).catch(error => console.error(error)); saveAsJSON(elements, appState).catch(error => console.error(error));
return {}; return { commitToHistory: false };
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
@ -63,7 +66,11 @@ export const actionLoadScene = register({
appState, appState,
{ elements: loadedElements, appState: loadedAppState }, { elements: loadedElements, appState: loadedAppState },
) => { ) => {
return { elements: loadedElements, appState: loadedAppState }; return {
elements: loadedElements,
appState: loadedAppState,
commitToHistory: false,
};
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton

View File

@ -52,6 +52,7 @@ export const actionFinalize = register({
editingElement: null, editingElement: null,
selectedElementIds: {}, selectedElementIds: {},
}, },
commitToHistory: false,
}; };
}, },
keyTest: (event, appState) => keyTest: (event, appState) =>

View File

@ -1,4 +1,4 @@
import { Action } from "./types"; import { Action, ActionResult } from "./types";
import React from "react"; import React from "react";
import { undo, redo } from "../components/icons"; import { undo, redo } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
@ -13,8 +13,12 @@ import { newElementWith } from "../element/mutateElement";
const writeData = ( const writeData = (
prevElements: readonly ExcalidrawElement[], prevElements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
updater: () => { elements: ExcalidrawElement[]; appState: AppState } | null, updater: () => {
) => { elements: ExcalidrawElement[];
appState: AppState;
} | null,
): ActionResult => {
const commitToHistory = false;
if ( if (
!appState.multiElement && !appState.multiElement &&
!appState.resizingElement && !appState.resizingElement &&
@ -23,7 +27,7 @@ const writeData = (
) { ) {
const data = updater(); const data = updater();
if (data === null) { if (data === null) {
return {}; return { commitToHistory };
} }
const prevElementMap = getElementMap(prevElements); const prevElementMap = getElementMap(prevElements);
@ -47,9 +51,10 @@ const writeData = (
), ),
), ),
appState: { ...appState, ...data.appState }, appState: { ...appState, ...data.appState },
commitToHistory,
}; };
} }
return {}; return { commitToHistory };
}; };
const testUndo = (shift: boolean) => (event: KeyboardEvent) => const testUndo = (shift: boolean) => (event: KeyboardEvent) =>

View File

@ -12,6 +12,7 @@ export const actionToggleCanvasMenu = register({
...appState, ...appState,
openMenu: appState.openMenu === "canvas" ? null : "canvas", openMenu: appState.openMenu === "canvas" ? null : "canvas",
}, },
commitToHistory: false,
}), }),
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData }) => (
<ToolButton <ToolButton
@ -31,6 +32,7 @@ export const actionToggleEditMenu = register({
...appState, ...appState,
openMenu: appState.openMenu === "shape" ? null : "shape", openMenu: appState.openMenu === "shape" ? null : "shape",
}, },
commitToHistory: false,
}), }),
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton

View File

@ -52,9 +52,9 @@ export const actionChangeStrokeColor = register({
}), }),
), ),
appState: { ...appState, currentItemStrokeColor: value }, appState: { ...appState, currentItemStrokeColor: value },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<> <>
<h3 aria-hidden="true">{t("labels.stroke")}</h3> <h3 aria-hidden="true">{t("labels.stroke")}</h3>
@ -83,9 +83,9 @@ export const actionChangeBackgroundColor = register({
}), }),
), ),
appState: { ...appState, currentItemBackgroundColor: value }, appState: { ...appState, currentItemBackgroundColor: value },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<> <>
<h3 aria-hidden="true">{t("labels.background")}</h3> <h3 aria-hidden="true">{t("labels.background")}</h3>
@ -114,9 +114,9 @@ export const actionChangeFillStyle = register({
}), }),
), ),
appState: { ...appState, currentItemFillStyle: value }, appState: { ...appState, currentItemFillStyle: value },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<fieldset> <fieldset>
<legend>{t("labels.fill")}</legend> <legend>{t("labels.fill")}</legend>
@ -151,9 +151,9 @@ export const actionChangeStrokeWidth = register({
}), }),
), ),
appState: { ...appState, currentItemStrokeWidth: value }, appState: { ...appState, currentItemStrokeWidth: value },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<fieldset> <fieldset>
<legend>{t("labels.strokeWidth")}</legend> <legend>{t("labels.strokeWidth")}</legend>
@ -186,9 +186,9 @@ export const actionChangeSloppiness = register({
}), }),
), ),
appState: { ...appState, currentItemRoughness: value }, appState: { ...appState, currentItemRoughness: value },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<fieldset> <fieldset>
<legend>{t("labels.sloppiness")}</legend> <legend>{t("labels.sloppiness")}</legend>
@ -221,9 +221,9 @@ export const actionChangeOpacity = register({
}), }),
), ),
appState: { ...appState, currentItemOpacity: value }, appState: { ...appState, currentItemOpacity: value },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<label className="control-label"> <label className="control-label">
{t("labels.opacity")} {t("labels.opacity")}
@ -281,9 +281,9 @@ export const actionChangeFontSize = register({
appState.currentItemFont.split("px ")[1] appState.currentItemFont.split("px ")[1]
}`, }`,
}, },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<fieldset> <fieldset>
<legend>{t("labels.fontSize")}</legend> <legend>{t("labels.fontSize")}</legend>
@ -328,9 +328,9 @@ export const actionChangeFontFamily = register({
appState.currentItemFont.split("px ")[0] appState.currentItemFont.split("px ")[0]
}px ${value}`, }px ${value}`,
}, },
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<fieldset> <fieldset>
<legend>{t("labels.fontFamily")}</legend> <legend>{t("labels.fontFamily")}</legend>

View File

@ -12,6 +12,7 @@ export const actionSelectAll = register({
return map; return map;
}, {} as any), }, {} as any),
}, },
commitToHistory: true,
}; };
}, },
contextItemLabel: "labels.selectAll", contextItemLabel: "labels.selectAll",

View File

@ -17,7 +17,9 @@ export const actionCopyStyles = register({
if (element) { if (element) {
copiedStyles = JSON.stringify(element); copiedStyles = JSON.stringify(element);
} }
return {}; return {
commitToHistory: false,
};
}, },
contextItemLabel: "labels.copyStyles", contextItemLabel: "labels.copyStyles",
keyTest: event => keyTest: event =>
@ -30,7 +32,7 @@ export const actionPasteStyles = register({
perform: (elements, appState) => { perform: (elements, appState) => {
const pastedElement = JSON.parse(copiedStyles); const pastedElement = JSON.parse(copiedStyles);
if (!isExcalidrawElement(pastedElement)) { if (!isExcalidrawElement(pastedElement)) {
return { elements }; return { elements, commitToHistory: false };
} }
return { return {
elements: elements.map(element => { elements: elements.map(element => {
@ -53,9 +55,9 @@ export const actionPasteStyles = register({
} }
return element; return element;
}), }),
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
contextItemLabel: "labels.pasteStyles", contextItemLabel: "labels.pasteStyles",
keyTest: event => keyTest: event =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === "V", event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === "V",

View File

@ -26,11 +26,11 @@ export const actionSendBackward = register({
getSelectedIndices(elements, appState), getSelectedIndices(elements, appState),
), ),
appState, appState,
commitToHistory: true,
}; };
}, },
contextItemLabel: "labels.sendBackward", contextItemLabel: "labels.sendBackward",
keyPriority: 40, keyPriority: 40,
commitToHistory: () => true,
keyTest: event => keyTest: event =>
event[KEYS.CTRL_OR_CMD] && !event.shiftKey && event.code === "BracketLeft", event[KEYS.CTRL_OR_CMD] && !event.shiftKey && event.code === "BracketLeft",
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
@ -54,11 +54,11 @@ export const actionBringForward = register({
getSelectedIndices(elements, appState), getSelectedIndices(elements, appState),
), ),
appState, appState,
commitToHistory: true,
}; };
}, },
contextItemLabel: "labels.bringForward", contextItemLabel: "labels.bringForward",
keyPriority: 40, keyPriority: 40,
commitToHistory: () => true,
keyTest: event => keyTest: event =>
event[KEYS.CTRL_OR_CMD] && !event.shiftKey && event.code === "BracketRight", event[KEYS.CTRL_OR_CMD] && !event.shiftKey && event.code === "BracketRight",
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData }) => (
@ -82,10 +82,10 @@ export const actionSendToBack = register({
getSelectedIndices(elements, appState), getSelectedIndices(elements, appState),
), ),
appState, appState,
commitToHistory: true,
}; };
}, },
contextItemLabel: "labels.sendToBack", contextItemLabel: "labels.sendToBack",
commitToHistory: () => true,
keyTest: event => { keyTest: event => {
return isDarwin return isDarwin
? event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === "BracketLeft" ? event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === "BracketLeft"
@ -118,9 +118,9 @@ export const actionBringToFront = register({
getSelectedIndices(elements, appState), getSelectedIndices(elements, appState),
), ),
appState, appState,
commitToHistory: true,
}; };
}, },
commitToHistory: () => true,
contextItemLabel: "labels.bringToFront", contextItemLabel: "labels.bringToFront",
keyTest: event => { keyTest: event => {
return isDarwin return isDarwin

View File

@ -50,24 +50,12 @@ export class ActionManager implements ActionsManagerInterface {
} }
event.preventDefault(); event.preventDefault();
const commitToHistory = this.updater(data[0].perform(this.getElements(), this.getAppState(), null));
data[0].commitToHistory &&
data[0].commitToHistory(this.getAppState(), this.getElements());
this.updater(
data[0].perform(this.getElements(), this.getAppState(), null),
commitToHistory,
);
return true; return true;
} }
executeAction(action: Action) { executeAction(action: Action) {
const commitToHistory = this.updater(action.perform(this.getElements(), this.getAppState(), null));
action.commitToHistory &&
action.commitToHistory(this.getAppState(), this.getElements());
this.updater(
action.perform(this.getElements(), this.getAppState(), null),
commitToHistory,
);
} }
getContextMenuItems(actionFilter: ActionFilterFn = action => action) { getContextMenuItems(actionFilter: ActionFilterFn = action => action) {
@ -82,12 +70,8 @@ export class ActionManager implements ActionsManagerInterface {
.map(action => ({ .map(action => ({
label: action.contextItemLabel ? t(action.contextItemLabel) : "", label: action.contextItemLabel ? t(action.contextItemLabel) : "",
action: () => { action: () => {
const commitToHistory =
action.commitToHistory &&
action.commitToHistory(this.getAppState(), this.getElements());
this.updater( this.updater(
action.perform(this.getElements(), this.getAppState(), null), action.perform(this.getElements(), this.getAppState(), null),
commitToHistory,
); );
}, },
})); }));
@ -98,12 +82,8 @@ export class ActionManager implements ActionsManagerInterface {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
const updateData = (formState?: any) => { const updateData = (formState?: any) => {
const commitToHistory =
action.commitToHistory &&
action.commitToHistory(this.getAppState(), this.getElements());
this.updater( this.updater(
action.perform(this.getElements(), this.getAppState(), formState), action.perform(this.getElements(), this.getAppState(), formState),
commitToHistory,
); );
}; };

View File

@ -5,6 +5,7 @@ import { AppState } from "../types";
export type ActionResult = { export type ActionResult = {
elements?: readonly ExcalidrawElement[] | null; elements?: readonly ExcalidrawElement[] | null;
appState?: AppState | null; appState?: AppState | null;
commitToHistory: boolean;
}; };
type ActionFn = ( type ActionFn = (
@ -32,10 +33,6 @@ export interface Action {
) => boolean; ) => boolean;
contextItemLabel?: string; contextItemLabel?: string;
contextMenuOrder?: number; contextMenuOrder?: number;
commitToHistory?: (
appState: AppState,
elements: readonly ExcalidrawElement[],
) => boolean;
} }
export interface ActionsManagerInterface { export interface ActionsManagerInterface {

View File

@ -105,11 +105,14 @@ import { isLinearElement } from "../element/typeChecks";
import { rescalePoints } from "../points"; import { rescalePoints } from "../points";
import { actionFinalize } from "../actions"; import { actionFinalize } from "../actions";
/**
* @param func handler taking at most single parameter (event).
*/
function withBatchedUpdates< function withBatchedUpdates<
TFunction extends ((event: any) => void) | (() => void) TFunction extends ((event: any) => void) | (() => void)
>(func: TFunction) { >(func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never) {
return (event => { return (event => {
unstable_batchedUpdates(func, event); unstable_batchedUpdates(func as TFunction, event);
}) as TFunction; }) as TFunction;
} }
@ -164,20 +167,19 @@ export class App extends React.Component<any, AppState> {
this.actionManager.registerAction(createRedoAction(history)); this.actionManager.registerAction(createRedoAction(history));
} }
private syncActionResult = withBatchedUpdates( private syncActionResult = withBatchedUpdates((res: ActionResult) => {
(res: ActionResult, commitToHistory: boolean = true) => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
if (res.elements) { if (res.elements) {
globalSceneState.replaceAllElements(res.elements); globalSceneState.replaceAllElements(res.elements);
if (commitToHistory) { if (res.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
} }
if (res.appState) { if (res.appState) {
if (commitToHistory) { if (res.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
this.setState(state => ({ this.setState(state => ({
@ -186,8 +188,7 @@ export class App extends React.Component<any, AppState> {
collaborators: state.collaborators, collaborators: state.collaborators,
})); }));
} }
}, });
);
private onCut = withBatchedUpdates((event: ClipboardEvent) => { private onCut = withBatchedUpdates((event: ClipboardEvent) => {
if (isWritableElement(event.target)) { if (isWritableElement(event.target)) {
@ -917,7 +918,11 @@ export class App extends React.Component<any, AppState> {
) { ) {
loadFromBlob(file) loadFromBlob(file)
.then(({ elements, appState }) => .then(({ elements, appState }) =>
this.syncActionResult({ elements, appState }), this.syncActionResult({
elements,
appState,
commitToHistory: false,
}),
) )
.catch(error => console.error(error)); .catch(error => console.error(error));
} }

View File

@ -357,5 +357,6 @@ export async function loadScene(id: string | null, privateKey?: string) {
return { return {
elements: data.elements, elements: data.elements,
appState: data.appState && { ...data.appState }, appState: data.appState && { ...data.appState },
commitToHistory: false,
}; };
} }

View File

@ -83,7 +83,7 @@ export class SceneHistory {
} }
undoOnce(): Result | null { undoOnce(): Result | null {
if (this.stateHistory.length === 0) { if (this.stateHistory.length === 1) {
return null; return null;
} }