undo-redo foundation + first use case with instance insertion #8

This commit is contained in:
Oleg Isonen committed 2022-03-25 17:54:49 +00:00
1 parent 199b86fd7b
commit 63e8a225ce
9 files changed
+120 -43

No files matched your search

+9 -1
View File
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { DndProvider } from "react-dnd";
import { TouchBackend } from "react-dnd-touch-backend";
import {
@@ -28,6 +28,7 @@ import {
} from "./shared/publish";
import { useActiveElementTracking } from "./shared/active-element";
import { WrapperComponentDev } from "./features/wrapper-component";
import { initUndoRedo } from "./shared/undo-redo";
const useElementsTree = (
rootInstance: Instance,
@@ -68,6 +69,12 @@ const useIsPreviewMode = () => {
return isPreviewMode;
};
const useUndoRedo = () => {
useEffect(() => {
initUndoRedo();
}, []);
};
const PreviewMode = ({ rootInstance }: { rootInstance: Instance }) => {
return createElementsTree({
instance: rootInstance,
@@ -100,6 +107,7 @@ const DesignMode = ({
usePublishRootInstance(rootInstance);
useActiveElementTracking({ rootInstance });
useShortcuts({ rootInstance });
useUndoRedo();
const elements = useElementsTree(
rootInstance,
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useRef } from "react";
import { useDrag } from "react-dnd";
import { type Instance } from "@webstudio-is/sdk";
import { type DragData } from "~/shared/component";
@@ -11,6 +11,8 @@ type UseDraggable = {
};
export const useDraggable = ({ instance, isDisabled }: UseDraggable) => {
const isDraggingRef = useRef(false);
//const updatePointerOutline = usePointerOutline();
const [{ isDragging, currentOffset }, dragRefCallback] = useDrag(
() => ({
@@ -33,9 +35,12 @@ export const useDraggable = ({ instance, isDisabled }: UseDraggable) => {
if (isDragging === true) {
publish<"dragStartInstance">({ type: "dragStartInstance" });
} else {
// Initial state is false, don't fire event if there was no dragging
//if (isDraggingRef.current === false) return;
// Ended
publish<"dragEndInstance", Instance["id"]>({ type: "dragEndInstance" });
}
isDraggingRef.current = isDragging;
}, [isDragging, instance.id]);
useEffect(() => {
+2 -2
View File
@@ -90,8 +90,8 @@ export const useDeleteInstance = ({
setRootInstance: (instance: Instance) => void;
}) => {
const [, setSelectedInstance] = useSelectedInstance();
useSubscribe<"deleteSelectedInstance", { id: Instance["id"] }>(
"deleteSelectedInstance",
useSubscribe<"deleteInstace", { id: Instance["id"] }>(
"deleteInstace",
({ id }) => {
const newRootInstance = deleteInstance(rootInstance, id);
setRootInstance(newRootInstance);
+49
View File
@@ -0,0 +1,49 @@
import { emitter } from "@webstudio-is/sdk";
import { publish } from "~/canvas/shared/pubsub";
const types = ["insertInstance"] as const;
const maxLength = 100;
type Action = {
type: typeof types[number];
payload: unknown;
};
const current: Array<Action> = [];
const undone: Array<Action> = [];
export const add = (action: Action) => {
current.push(action);
if (history.length >= maxLength) {
current.shift();
}
// Undone items can't be redone if user adds another change to the current list.
// It's artificial, but that's how we know it.
undone.splice(0);
};
export const initUndoRedo = () => {
for (const type of types) {
emitter.on(type, (payload) => {
add({ type, payload });
});
}
};
export const undo = () => {
const action = current.pop();
if (action === undefined) return;
undone.push(action);
publish({
type: action.type + "Undo",
payload: action.payload,
});
};
export const redo = () => {
const action = undone.pop();
if (action === undefined) return;
current.push(action);
publish(action);
};
+24 -30
View File
@@ -39,25 +39,6 @@ export const useDragDropHandlers = ({
const [dropData, setDropData] = useDropData();
const [dragData, setDragData] = useState<DragData>();
const insert = ({
instance,
dropData,
}: {
instance: Instance;
dropData: DropData;
}) => {
const instanceInsertionSpec = {
instance,
parentId: dropData.instance.id,
position: dropData.position,
} as const;
setInstanceInsertionSpec(instanceInsertionSpec);
publish<"syncInstanceInsertion", InstanceInsertionSpec>({
type: "syncInstanceInsertion",
payload: instanceInsertionSpec,
});
};
useSubscribe<"dragStartInstance">("dragStartInstance", () => {
setSelectedInstance(undefined);
});
@@ -82,12 +63,13 @@ export const useDragDropHandlers = ({
findInstanceById(rootInstance, dragData.instance.id) === undefined;
if (isNew) {
insert({
instance: dragData.instance,
dropData,
publish<"insertInstance", Instance>({
type: "insertInstance",
payload: dragData.instance,
});
return;
}
const instanceReparentingSpec = {
parentId: dropData.instance.id,
position: dropData.position,
@@ -141,17 +123,29 @@ export const useDragDropHandlers = ({
});
useSubscribe<"insertInstance", Instance>("insertInstance", (instance) => {
if (selectedInstance === undefined) {
setSelectedInstance(rootInstance);
}
insert({
const instanceInsertionSpec = {
instance,
dropData: {
position: "end",
instance: selectedInstance ?? rootInstance,
},
parentId: selectedInstance?.id ?? rootInstance.id,
position: dropData?.position || "end",
};
setInstanceInsertionSpec(instanceInsertionSpec);
publish<"syncInstanceInsertion", InstanceInsertionSpec>({
type: "syncInstanceInsertion",
payload: instanceInsertionSpec,
});
});
useSubscribe<"insertInstanceUndo", Instance>(
"insertInstanceUndo",
(instance) => {
publish<"deleteInstace", { id: Instance["id"] }>({
type: "deleteInstace",
payload: {
id: instance.id,
},
});
}
);
return { instanceInsertionSpec, instanceReparentingSpec };
};
+17 -2
View File
@@ -2,6 +2,7 @@ import { useHotkeys } from "react-hotkeys-hook";
import { type Instance } from "@webstudio-is/sdk";
import { useSelectedInstance } from "./nano-values";
import { publish } from "./pubsub";
import { redo, undo } from "./undo-redo";
export const useShortcuts = ({ rootInstance }: { rootInstance: Instance }) => {
const [instance, setSelectedInstance] = useSelectedInstance();
@@ -10,8 +11,8 @@ export const useShortcuts = ({ rootInstance }: { rootInstance: Instance }) => {
() => {
// @todo tell user they can't delete root
if (instance === undefined || instance.id === rootInstance.id) return;
publish<"deleteSelectedInstance", { id: Instance["id"] }>({
type: "deleteSelectedInstance",
publish<"deleteInstace", { id: Instance["id"] }>({
type: "deleteInstace",
payload: {
id: instance.id,
},
@@ -33,4 +34,18 @@ export const useShortcuts = ({ rootInstance }: { rootInstance: Instance }) => {
},
[instance]
);
useHotkeys(
// Undo
"cmd+z, ctrl+z",
undo,
[]
);
useHotkeys(
// Redo
"cmd+shift+z, ctrl+shift+z",
redo,
[]
);
};
+2 -2
View File
@@ -77,8 +77,8 @@ export const useSync = ({ project }: { config: Config; project: Project }) => {
}
);
useSubscribe<"deleteSelectedInstance", { id: Instance["id"] }>(
"deleteSelectedInstance",
useSubscribe<"deleteInstace", { id: Instance["id"] }>(
"deleteInstace",
({ id }) => {
enqueue(() =>
fetch(`/rest/delete-instance/${project.devTreeId}`, {
+1 -1
View File
@@ -43,7 +43,7 @@
"@remix-run/vercel": "^1.2.3",
"@stitches/react": "^1.2.6",
"@stitches/stringify": "^1.2.6",
"@webstudio-is/sdk": "^0.1.18",
"@webstudio-is/sdk": "^0.1.19",
"bson-objectid": "^2.0.2",
"camelcase": "^6.3.0",
"hyphenate-style-name": "^1.0.4",
+10 -4
View File
@@ -1820,14 +1820,15 @@
dependencies:
web-streams-polyfill "^3.1.1"
"@webstudio-is/sdk@^0.1.18":
version "0.1.18"
resolved "https://registry.yarnpkg.com/@webstudio-is/sdk/-/sdk-0.1.18.tgz#86fa61b508c0293f0ed9baca6e6b89585f44e320"
integrity sha512-rEgIG4qZMKhyudqVhQGlc7LB1ibMQ6KA9Jr+pjMTAhv3o/Jgqk4YA4eXyui8ZFZvC5ibIakdzqsv+4O6QTXGSQ==
"@webstudio-is/sdk@^0.1.19":
version "0.1.19"
resolved "https://registry.yarnpkg.com/@webstudio-is/sdk/-/sdk-0.1.19.tgz#6075c3095d6f145ee45f3830cbb40044a2f464c5"
integrity sha512-OnhDlhP9EneFCg5p96saEeRKiGdgcq/ngEq78YrVCQF15AvuLpDrCZ5/P9KLEhvusEteNK0SmUYRKqZ2+IQ5NQ==
dependencies:
"@prisma/client" "^3.10.0"
"@stitches/react" "^1.2.7"
immer "^9.0.12"
mitt "^3.0.0"
prisma "^3.10.0"
react-nano-state "^0.4.0"
@@ -6067,6 +6068,11 @@ minizlib@^2.1.1:
minipass "^3.0.0"
yallist "^4.0.0"
mitt@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"