Skip to content

Commit

Permalink
Merge pull request #241 from Lemoncode/#140-Add-Copy-Paste-Buttons
Browse files Browse the repository at this point in the history
#140 - Added Copy/Paste Button
  • Loading branch information
brauliodiez authored Aug 21, 2024
2 parents b6e9c1a + 918abab commit b609d97
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 70 deletions.
1 change: 1 addition & 0 deletions public/icons/copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/icons/paste.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions src/common/components/icons/copy-icon.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const CopyIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 256 256"
>
<path
fill="currentColor"
d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"
/>
</svg>
);
};
2 changes: 2 additions & 0 deletions src/common/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
15 changes: 15 additions & 0 deletions src/common/components/icons/paste-icon.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const PasteIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 256 256"
>
<path
fill="currentColor"
d="M200 32h-36.26a47.92 47.92 0 0 0-71.48 0H56a16 16 0 0 0-16 16v168a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m-72 0a32 32 0 0 1 32 32H96a32 32 0 0 1 32-32m72 184H56V48h26.75A47.9 47.9 0 0 0 80 64v8a8 8 0 0 0 8 8h80a8 8 0 0 0 8-8v-8a47.9 47.9 0 0 0-2.75-16H200Z"
/>
</svg>
);
};
5 changes: 4 additions & 1 deletion src/core/providers/canvas/canvas.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export interface CanvasContextModel {
scale: number;
clearCanvas: () => void;
setScale: React.Dispatch<React.SetStateAction<number>>;
pasteShape: (shape: ShapeModel) => void;
addNewShape: (
type: ShapeType,
x: number,
Expand All @@ -54,6 +53,10 @@ export interface CanvasContextModel {
canRedo: () => boolean;
doUndo: () => void;
doRedo: () => void;
canCopy: boolean;
canPaste: boolean;
copyShapeToClipboard: () => void;
pasteShapeFromClipboard: () => void;
}

export interface DocumentModel {
Expand Down
27 changes: 17 additions & 10 deletions src/core/providers/canvas/canvas.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,6 +38,18 @@ export const CanvasProvider: React.FC<Props> = 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: [] });
};
Expand All @@ -51,15 +64,6 @@ export const CanvasProvider: React.FC<Props> = 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,
Expand Down Expand Up @@ -130,13 +134,16 @@ export const CanvasProvider: React.FC<Props> = props => {
clearCanvas,
selectionInfo,
addNewShape,
pasteShape,
updateShapeSizeAndPosition,
updateShapePosition,
canUndo,
canRedo,
doUndo,
doRedo,
canCopy,
canPaste,
copyShapeToClipboard,
pasteShapeFromClipboard,
stageRef,
deleteSelectedShape,
}}
Expand Down
50 changes: 50 additions & 0 deletions src/core/providers/canvas/use-clipboard.hook.tsx
Original file line number Diff line number Diff line change
@@ -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<ShapeModel | null>(null);
const clipboardShapeRef = useRef<ShapeModel | null>(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 };
};
26 changes: 4 additions & 22 deletions src/pods/canvas/canvas.pod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +24,10 @@ export const CanvasPod = () => {
updateShapeSizeAndPosition,
updateShapePosition,
stageRef,
canCopy,
canPaste,
copyShapeToClipboard,
pasteShapeFromClipboard,
} = useCanvasContext();

const {
Expand Down Expand Up @@ -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 */
}
Expand Down
35 changes: 0 additions & 35 deletions src/pods/canvas/use-clipboard.hook.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 (
<div>
<ul className={classes.buttonGroup}>
<li>
<ToolbarButton
icon={<CopyIcon />}
label="Copy"
onClick={handleCopyClick}
className={classes.button}
disabled={!canCopy}
shortcutOptions={SHORTCUTS.copy}
/>
</li>
<li>
<ToolbarButton
icon={<PasteIcon />}
label="Paste"
onClick={handlePasteClick}
className={classes.button}
disabled={!canPaste} // Disable the button if the clipboard is not filled
shortcutOptions={SHORTCUTS.paste}
/>
</li>
</ul>
</div>
);
};
1 change: 1 addition & 0 deletions src/pods/toolbar/components/copy-paste-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './copy-paste-button';
12 changes: 12 additions & 0 deletions src/pods/toolbar/shortcut/shortcut.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
};
6 changes: 4 additions & 2 deletions src/pods/toolbar/shortcut/shortcut.hook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/pods/toolbar/toolbar.pod.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -36,6 +38,9 @@ export const ToolbarPod: React.FC = () => {
<li>
<RedoButton />
</li>
<li>
<CopyButton />
</li>
<li>
<DeleteButton />
</li>
Expand Down

0 comments on commit b609d97

Please sign in to comment.