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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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