diff --git a/src/pages/editor/components/canvas/useKeyboardMovement.ts b/src/pages/editor/components/canvas/useKeyboardMovement.ts index aea763e..7408c6e 100644 --- a/src/pages/editor/components/canvas/useKeyboardMovement.ts +++ b/src/pages/editor/components/canvas/useKeyboardMovement.ts @@ -7,6 +7,8 @@ export const useKeyboardMovement = () => { objects, updateObject, deleteObject, + copySelectedObjects, + pasteCopiedObjects, tool, layerRef, groupObjects, @@ -57,6 +59,20 @@ export const useKeyboardMovement = () => { } } + if ((e.metaKey || e.ctrlKey) && e.key === "c") { + if (selectedObjectIds.length > 0) { + e.preventDefault(); + copySelectedObjects(); + } + return; + } + + if ((e.metaKey || e.ctrlKey) && e.key === "v") { + e.preventDefault(); + pasteCopiedObjects(); + return; + } + if (selectedObjectIds.length === 0) { return; } @@ -122,6 +138,8 @@ export const useKeyboardMovement = () => { objects, updateObject, deleteObject, + copySelectedObjects, + pasteCopiedObjects, tool, layerRef, groupObjects, diff --git a/src/pages/editor/store/editorStore.ts b/src/pages/editor/store/editorStore.ts index 5241543..bc67e96 100644 --- a/src/pages/editor/store/editorStore.ts +++ b/src/pages/editor/store/editorStore.ts @@ -38,6 +38,8 @@ type EditorStoreType = { updateObject: (id: string, updates: Partial) => void; deleteObject: (id: string) => void; duplicateObject: (id: string) => void; + copySelectedObjects: () => void; + pasteCopiedObjects: () => void; reorderObjects: (draggedId: string, targetId: string) => void; selectedObjectId: string | null; setSelectedObjectId: (id: string | null) => void; @@ -104,10 +106,50 @@ type EditorStoreType = { groupObjects: (objectIds: string[]) => void; ungroupObjects: (groupId: string) => void; loadPages: (pages: Page[]) => void; + clipboardObjects: EditorObject[]; }; export const useEditorStore = create((set, get) => { const initialPage = createInitialPage("جلد"); + const cloneObjectsForPaste = ( + sourceObjects: EditorObject[], + offset = 20, + ): EditorObject[] => { + if (sourceObjects.length === 0) return []; + + const sourceIds = new Set(sourceObjects.map((obj) => obj.id)); + const idMap = new Map(); + sourceObjects.forEach((obj) => { + idMap.set(obj.id, `${obj.id}-${crypto.randomUUID()}`); + }); + + return sourceObjects.map((objectToClone) => ({ + ...objectToClone, + id: idMap.get(objectToClone.id)!, + x: (objectToClone.x || 0) + offset, + y: (objectToClone.y || 0) + offset, + groupId: + objectToClone.groupId && sourceIds.has(objectToClone.groupId) + ? idMap.get(objectToClone.groupId) + : undefined, + ...(objectToClone.tableData && { + tableData: { + ...objectToClone.tableData, + id: `table-${crypto.randomUUID()}`, + cells: Object.fromEntries( + Object.entries(objectToClone.tableData.cells).map(([key, cell]) => [ + key, + { + ...cell, + id: `cell-${crypto.randomUUID()}`, + }, + ]), + ), + }, + }), + })); + }; + return { tool: "select", setTool: (tool) => @@ -117,6 +159,7 @@ export const useEditorStore = create((set, get) => { })), objects: [], guides: [], + clipboardObjects: [], addObject: (object) => { const state = get(); if (state.currentPageId) { @@ -206,29 +249,7 @@ export const useEditorStore = create((set, get) => { const objectToDuplicate = state.objects.find((obj) => obj.id === id); if (!objectToDuplicate) return; - const duplicatedObject: EditorObject = { - ...objectToDuplicate, - id: `${objectToDuplicate.id}-${crypto.randomUUID()}`, - x: (objectToDuplicate.x || 0) + 20, - y: (objectToDuplicate.y || 0) + 20, - ...(objectToDuplicate.tableData && { - tableData: { - ...objectToDuplicate.tableData, - id: `table-${crypto.randomUUID()}`, - cells: Object.fromEntries( - Object.entries(objectToDuplicate.tableData.cells).map( - ([key, cell]) => [ - key, - { - ...cell, - id: `cell-${crypto.randomUUID()}`, - }, - ], - ), - ), - }, - }), - }; + const duplicatedObject = cloneObjectsForPaste([objectToDuplicate])[0]; if (state.currentPageId) { set((state) => ({ @@ -247,6 +268,57 @@ export const useEditorStore = create((set, get) => { selectedObjectId: duplicatedObject.id, })); }, + copySelectedObjects: () => { + const state = get(); + const selectedIds = + state.selectedObjectIds.length > 0 + ? state.selectedObjectIds + : state.selectedObjectId + ? [state.selectedObjectId] + : []; + + if (selectedIds.length === 0) return; + + const selectedIdSet = new Set(selectedIds); + const groupsToCopy = new Set( + state.objects + .filter((obj) => selectedIdSet.has(obj.id) && obj.type === "group") + .map((obj) => obj.id), + ); + const objectsToCopy = state.objects.filter( + (obj) => selectedIdSet.has(obj.id) || groupsToCopy.has(obj.groupId || ""), + ); + + if (objectsToCopy.length === 0) return; + set({ clipboardObjects: objectsToCopy }); + }, + pasteCopiedObjects: () => { + const state = get(); + if (state.clipboardObjects.length === 0) return; + + const pastedObjects = cloneObjectsForPaste(state.clipboardObjects); + const pastedIds = pastedObjects.map((obj) => obj.id); + + if (state.currentPageId) { + set((state) => ({ + objects: [...state.objects, ...pastedObjects], + pages: state.pages.map((p) => + p.id === state.currentPageId + ? { ...p, objects: [...p.objects, ...pastedObjects] } + : p, + ), + selectedObjectId: pastedIds[0] ?? null, + selectedObjectIds: pastedIds, + })); + return; + } + + set((state) => ({ + objects: [...state.objects, ...pastedObjects], + selectedObjectId: pastedIds[0] ?? null, + selectedObjectIds: pastedIds, + })); + }, reorderObjects: (draggedId, targetId) => { if (draggedId === targetId) return;