mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 13:05:08 +09:00
undo-redo foundation + first use case with instance insertion #8
This commit is contained in:
1 parent
199b86fd7b
commit
63e8a225ce
9 files changed
+120
-43
No files matched your search
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user