diff --git a/public/icons/copy.svg b/public/icons/copy.svg
new file mode 100644
index 00000000..ac87cdf5
--- /dev/null
+++ b/public/icons/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/paste.svg b/public/icons/paste.svg
new file mode 100644
index 00000000..34cfab52
--- /dev/null
+++ b/public/icons/paste.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/common/components/icons/copy-icon.component.tsx b/src/common/components/icons/copy-icon.component.tsx
new file mode 100644
index 00000000..271d38d8
--- /dev/null
+++ b/src/common/components/icons/copy-icon.component.tsx
@@ -0,0 +1,15 @@
+export const CopyIcon = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/common/components/icons/index.ts b/src/common/components/icons/index.ts
index 67ef2461..2bf628b8 100644
--- a/src/common/components/icons/index.ts
+++ b/src/common/components/icons/index.ts
@@ -6,3 +6,5 @@ export * from './send-to-back-icon.component';
export * from './linkedin-icon.component';
export * from './x-icon.component';
export * from './quickmock-logo.component';
+export * from './copy-icon.component';
+export * from './paste-icon.component';
diff --git a/src/common/components/icons/paste-icon.component.tsx b/src/common/components/icons/paste-icon.component.tsx
new file mode 100644
index 00000000..9830f1b9
--- /dev/null
+++ b/src/common/components/icons/paste-icon.component.tsx
@@ -0,0 +1,15 @@
+export const PasteIcon = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts
index 9535fb18..5e2a2528 100644
--- a/src/core/providers/canvas/canvas.model.ts
+++ b/src/core/providers/canvas/canvas.model.ts
@@ -38,7 +38,6 @@ export interface CanvasContextModel {
scale: number;
clearCanvas: () => void;
setScale: React.Dispatch>;
- pasteShape: (shape: ShapeModel) => void;
addNewShape: (
type: ShapeType,
x: number,
@@ -54,6 +53,10 @@ export interface CanvasContextModel {
canRedo: () => boolean;
doUndo: () => void;
doRedo: () => void;
+ canCopy: boolean;
+ canPaste: boolean;
+ copyShapeToClipboard: () => void;
+ pasteShapeFromClipboard: () => void;
}
export interface DocumentModel {
diff --git a/src/core/providers/canvas/canvas.provider.tsx b/src/core/providers/canvas/canvas.provider.tsx
index 139b9251..9197de53 100644
--- a/src/core/providers/canvas/canvas.provider.tsx
+++ b/src/core/providers/canvas/canvas.provider.tsx
@@ -9,6 +9,7 @@ import { createDefaultDocumentModel, DocumentModel } from './canvas.model';
import { v4 as uuidv4 } from 'uuid';
import Konva from 'konva';
import { removeShapeFromList } from './canvas.business';
+import { useClipboard } from './use-clipboard.hook';
interface Props {
children: React.ReactNode;
@@ -37,6 +38,18 @@ export const CanvasProvider: React.FC = props => {
const selectionInfo = useSelection(document, setDocument);
+ const pasteShape = (shape: ShapeModel) => {
+ shape.id = uuidv4();
+
+ setDocument(prevDocument => ({
+ ...prevDocument,
+ shapes: [...prevDocument.shapes, shape],
+ }));
+ };
+
+ const { copyShapeToClipboard, pasteShapeFromClipboard, canCopy, canPaste } =
+ useClipboard(pasteShape, document.shapes, selectionInfo);
+
const clearCanvas = () => {
setDocument({ shapes: [] });
};
@@ -51,15 +64,6 @@ export const CanvasProvider: React.FC = props => {
}));
};
- const pasteShape = (shape: ShapeModel) => {
- shape.id = uuidv4();
-
- setDocument(prevDocument => ({
- ...prevDocument,
- shapes: [...prevDocument.shapes, shape],
- }));
- };
-
// TODO: instenad of x,y use Coord and reduce the number of arguments
const addNewShape = (
type: ShapeType,
@@ -130,13 +134,16 @@ export const CanvasProvider: React.FC = props => {
clearCanvas,
selectionInfo,
addNewShape,
- pasteShape,
updateShapeSizeAndPosition,
updateShapePosition,
canUndo,
canRedo,
doUndo,
doRedo,
+ canCopy,
+ canPaste,
+ copyShapeToClipboard,
+ pasteShapeFromClipboard,
stageRef,
deleteSelectedShape,
}}
diff --git a/src/core/providers/canvas/use-clipboard.hook.tsx b/src/core/providers/canvas/use-clipboard.hook.tsx
new file mode 100644
index 00000000..000f6980
--- /dev/null
+++ b/src/core/providers/canvas/use-clipboard.hook.tsx
@@ -0,0 +1,50 @@
+import { useMemo, useRef, useState } from 'react';
+import { ShapeModel } from '@/core/model';
+import {
+ adjustShapePosition,
+ cloneShape,
+ findShapeById,
+ validateShape,
+} from '../../../pods/canvas/clipboard.utils';
+
+export const useClipboard = (
+ pasteShape: (shape: ShapeModel) => void,
+ shapes: ShapeModel[],
+ selectionInfo: { selectedShapeId: string | null }
+) => {
+ const [clipboardShape, setClipboardShape] = useState(null);
+ const clipboardShapeRef = useRef(null);
+ const copyCount = useRef(1);
+
+ const copyShapeToClipboard = () => {
+ const selectedShape = findShapeById(
+ selectionInfo.selectedShapeId ?? '',
+ shapes
+ );
+ if (selectedShape) {
+ clipboardShapeRef.current = cloneShape(selectedShape);
+ setClipboardShape(clipboardShapeRef.current);
+ copyCount.current = 1;
+ }
+ };
+
+ const pasteShapeFromClipboard = () => {
+ if (clipboardShapeRef.current) {
+ const newShape: ShapeModel = cloneShape(clipboardShapeRef.current);
+ validateShape(newShape);
+ adjustShapePosition(newShape, copyCount.current);
+ pasteShape(newShape);
+ copyCount.current++;
+ }
+ };
+
+ const canCopy: boolean = useMemo(() => {
+ return !!selectionInfo.selectedShapeId;
+ }, [selectionInfo.selectedShapeId]);
+
+ const canPaste: boolean = useMemo(() => {
+ return clipboardShapeRef.current !== null;
+ }, [clipboardShape]);
+
+ return { copyShapeToClipboard, pasteShapeFromClipboard, canCopy, canPaste };
+};
diff --git a/src/pods/canvas/canvas.pod.tsx b/src/pods/canvas/canvas.pod.tsx
index c44f7ea3..2b10e7fa 100644
--- a/src/pods/canvas/canvas.pod.tsx
+++ b/src/pods/canvas/canvas.pod.tsx
@@ -8,7 +8,6 @@ import { useDropShape } from './use-drop-shape.hook';
import { useMonitorShape } from './use-monitor-shape.hook';
import classes from './canvas.pod.module.css';
import { EditableComponent } from '@/common/components/inline-edit';
-import { useClipboard } from './use-clipboard.hook';
import { useSnapIn } from './use-snapin.hook';
import { ShapeType } from '@/core/model';
import { useDropImageFromDesktop } from './use-drop-image-from-desktop';
@@ -25,6 +24,10 @@ export const CanvasPod = () => {
updateShapeSizeAndPosition,
updateShapePosition,
stageRef,
+ canCopy,
+ canPaste,
+ copyShapeToClipboard,
+ pasteShapeFromClipboard,
} = useCanvasContext();
const {
@@ -88,27 +91,6 @@ export const CanvasPod = () => {
updateShapePosition(id, { x, y });
};
- const { copyShape, pasteShapeFromClipboard } = useClipboard();
-
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- const isCtrlOrCmdPressed = e.ctrlKey || e.metaKey;
-
- if (isCtrlOrCmdPressed && e.key === 'c') {
- copyShape();
- }
- if (isCtrlOrCmdPressed && e.key === 'v') {
- pasteShapeFromClipboard();
- }
- };
-
- window.addEventListener('keydown', handleKeyDown);
-
- return () => {
- window.removeEventListener('keydown', handleKeyDown);
- };
- }, [selectedShapeId]);
-
{
/* TODO: add other animation for isDraggerOver */
}
diff --git a/src/pods/canvas/use-clipboard.hook.tsx b/src/pods/canvas/use-clipboard.hook.tsx
deleted file mode 100644
index 9b93ee1d..00000000
--- a/src/pods/canvas/use-clipboard.hook.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useRef } from 'react';
-import { ShapeModel } from '@/core/model';
-import { useCanvasContext } from '@/core/providers';
-import {
- adjustShapePosition,
- cloneShape,
- findShapeById,
- validateShape,
-} from './clipboard.utils';
-
-export const useClipboard = () => {
- const { pasteShape, shapes, selectionInfo } = useCanvasContext();
- const clipboardShapeRef = useRef(null);
- const copyCount = useRef(1);
-
- const copyShape = () => {
- const selectedShape = findShapeById(selectionInfo.selectedShapeId, shapes);
- if (selectedShape) {
- clipboardShapeRef.current = cloneShape(selectedShape);
- copyCount.current = 1;
- }
- };
-
- const pasteShapeFromClipboard = () => {
- if (clipboardShapeRef.current) {
- const newShape: ShapeModel = cloneShape(clipboardShapeRef.current);
- validateShape(newShape);
- adjustShapePosition(newShape, copyCount.current);
- pasteShape(newShape);
- copyCount.current++;
- }
- };
-
- return { copyShape, pasteShapeFromClipboard };
-};
diff --git a/src/pods/toolbar/components/copy-paste-button/copy-paste-button.tsx b/src/pods/toolbar/components/copy-paste-button/copy-paste-button.tsx
new file mode 100644
index 00000000..ad128dd6
--- /dev/null
+++ b/src/pods/toolbar/components/copy-paste-button/copy-paste-button.tsx
@@ -0,0 +1,49 @@
+import { CopyIcon, PasteIcon } from '@/common/components/icons';
+import { ToolbarButton } from '../toolbar-button';
+import classes from '@/pods/toolbar/toolbar.pod.module.css';
+import { useCanvasContext } from '@/core/providers';
+import { SHORTCUTS } from '../../shortcut/shortcut.const';
+
+export const CopyButton = () => {
+ const { canCopy, canPaste, copyShapeToClipboard, pasteShapeFromClipboard } =
+ useCanvasContext();
+
+ const handleCopyClick = () => {
+ if (canCopy) {
+ copyShapeToClipboard();
+ }
+ };
+
+ const handlePasteClick = () => {
+ if (canPaste) {
+ pasteShapeFromClipboard();
+ }
+ };
+
+ return (
+
+
+
+ }
+ label="Copy"
+ onClick={handleCopyClick}
+ className={classes.button}
+ disabled={!canCopy}
+ shortcutOptions={SHORTCUTS.copy}
+ />
+
+
+ }
+ label="Paste"
+ onClick={handlePasteClick}
+ className={classes.button}
+ disabled={!canPaste} // Disable the button if the clipboard is not filled
+ shortcutOptions={SHORTCUTS.paste}
+ />
+
+
+
+ );
+};
diff --git a/src/pods/toolbar/components/copy-paste-button/index.ts b/src/pods/toolbar/components/copy-paste-button/index.ts
new file mode 100644
index 00000000..fe96ca75
--- /dev/null
+++ b/src/pods/toolbar/components/copy-paste-button/index.ts
@@ -0,0 +1 @@
+export * from './copy-paste-button';
diff --git a/src/pods/toolbar/shortcut/shortcut.const.ts b/src/pods/toolbar/shortcut/shortcut.const.ts
index 04d49a1d..ff42b7e5 100644
--- a/src/pods/toolbar/shortcut/shortcut.const.ts
+++ b/src/pods/toolbar/shortcut/shortcut.const.ts
@@ -11,4 +11,16 @@ export const SHORTCUTS: Shortcut = {
targetKey: ['Backspace'],
targetKeyLabel: 'Backspace',
},
+ copy: {
+ description: 'Copy',
+ id: 'copy-button-shortcut',
+ targetKey: ['c'],
+ targetKeyLabel: 'Ctrl + C',
+ },
+ paste: {
+ description: 'Paste',
+ id: 'paste-button-shortcut',
+ targetKey: ['v'],
+ targetKeyLabel: 'Ctrl + V',
+ },
};
diff --git a/src/pods/toolbar/shortcut/shortcut.hook.tsx b/src/pods/toolbar/shortcut/shortcut.hook.tsx
index f4d171ce..568b5b88 100644
--- a/src/pods/toolbar/shortcut/shortcut.hook.tsx
+++ b/src/pods/toolbar/shortcut/shortcut.hook.tsx
@@ -9,12 +9,14 @@ export interface ShortcutHookProps {
export const useShortcut = ({ targetKey, callback }: ShortcutHookProps) => {
const handleKeyPress = (event: KeyboardEvent) => {
const isAltKeyPressed = event.getModifierState('Alt');
- const isCtrlKeyPressed = event.getModifierState('Control');
+ //const isCtrlKeyPressed = event.getModifierState('Control');
+ const isCtrlOrCmdPressed = event.ctrlKey || event.metaKey;
if (
(isWindowsOrLinux() && isAltKeyPressed) ||
- (isMacOS() && isCtrlKeyPressed)
+ (isMacOS() && isCtrlOrCmdPressed)
) {
+ console.log('event.key', event.key);
if (targetKey.includes(event.key)) {
event.preventDefault();
callback();
diff --git a/src/pods/toolbar/toolbar.pod.tsx b/src/pods/toolbar/toolbar.pod.tsx
index 4b10d54c..cc6fc6b4 100644
--- a/src/pods/toolbar/toolbar.pod.tsx
+++ b/src/pods/toolbar/toolbar.pod.tsx
@@ -1,4 +1,6 @@
import { DeleteButton } from './components/delete-button';
+import { CopyButton } from './components/copy-paste-button';
+import { PasteButton } from './components/paste-button';
import {
ZoomInButton,
ZoomOutButton,
@@ -36,6 +38,9 @@ export const ToolbarPod: React.FC = () => {
+
+
+