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 = () => {
  • +
  • + +