Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#495 allow the user duplicate page also closes #503 Allow the user delete a page #510

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './x-icon.component';
export * from './quickmock-logo.component';
export * from './copy-icon.component';
export * from './paste-icon.component';
export * from './delete-icon.component';
export * from './pencil-icon.component';
15 changes: 15 additions & 0 deletions src/common/components/icons/pencil-icon.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const PencilIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 256 256"
>
<path
fill="currentColor"
d="m225.9 74.78l-44.69-44.69a14 14 0 0 0-19.8 0L38.1 153.41a13.94 13.94 0 0 0-4.1 9.9V208a14 14 0 0 0 14 14h44.69a13.94 13.94 0 0 0 9.9-4.1L225.9 94.58a14 14 0 0 0 0-19.8M48.49 160L136 72.48L155.51 92L68 179.51ZM46 208v-33.52L81.51 210H48a2 2 0 0 1-2-2m50-.49L76.49 188L164 100.48L183.51 120ZM217.41 86.1L192 111.51L144.49 64l25.41-25.42a2 2 0 0 1 2.83 0l44.68 44.69a2 2 0 0 1 0 2.83"
/>
</svg>
);
};
6 changes: 6 additions & 0 deletions src/core/providers/canvas/canvas.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,11 @@ export interface CanvasContextModel {
setFileName: (fileName: string) => void;
fullDocument: DocumentModel;
addNewPage: () => void;
duplicatePage: (pageIndex: number) => void;
setActivePage: (pageId: string) => void;
deletePage: (pageIndex: number) => void;
isThumbnailContextMenuVisible: boolean;
setIsThumbnailContextMenuVisible: React.Dispatch<
React.SetStateAction<boolean>
>;
}
54 changes: 50 additions & 4 deletions src/core/providers/canvas/canvas.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const CanvasProvider: React.FC<Props> = props => {
const stageRef = React.useRef<Konva.Stage>(null);
const [isInlineEditing, setIsInlineEditing] = React.useState(false);
const [fileName, setFileName] = React.useState<string>('');
const [isThumbnailContextMenuVisible, setIsThumbnailContextMenuVisible] =
React.useState(false);

const {
addSnapshot,
Expand Down Expand Up @@ -53,15 +55,55 @@ export const CanvasProvider: React.FC<Props> = props => {
);
};

const duplicatePage = (pageIndex: number) => {
const newShapes: ShapeModel[] = document.pages[pageIndex].shapes.map(
shape => {
const newShape: ShapeModel = { ...shape };
newShape.id = uuidv4();
return newShape;
}
);

setDocument(lastDocument =>
produce(lastDocument, draft => {
const newPage = {
id: uuidv4(),
name: `Page ${draft.pages.length + 1}`,
shapes: newShapes,
};
draft.pages.push(newPage);
setActivePage(newPage.id);
})
);
};

const deletePage = (pageIndex: number) => {
const newActivePageId =
pageIndex < document.pages.length - 1
? document.pages[pageIndex + 1].id // If it's not the last page, select the next one
: document.pages[pageIndex - 1].id; // Otherwise, select the previous one

setDocument(lastDocument =>
produce(lastDocument, draft => {
draft.pages = draft.pages.filter(
currentPage => document.pages[pageIndex].id !== currentPage.id
);
})
);

setActivePage(newActivePageId);
};

const setActivePage = (pageId: string) => {
selectionInfo.clearSelection();
selectionInfo.shapeRefs.current = {};

setDocument(lastDocument =>
produce(lastDocument, draft => {
draft.activePageIndex = draft.pages.findIndex(
page => page.id === pageId
);
console.log(draft.activePageIndex);
const pageIndex = draft.pages.findIndex(page => page.id === pageId);
if (pageIndex !== -1) {
draft.activePageIndex = pageIndex;
}
})
);
};
Expand Down Expand Up @@ -245,7 +287,11 @@ export const CanvasProvider: React.FC<Props> = props => {
setFileName,
fullDocument: document,
addNewPage,
duplicatePage,
setActivePage,
deletePage,
isThumbnailContextMenuVisible,
setIsThumbnailContextMenuVisible,
}}
>
{children}
Expand Down
5 changes: 5 additions & 0 deletions src/pods/canvas/use-multiple-selection-shape.hook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { calculateScaledCoordsFromCanvasDivCoordinatesNoScroll } from './canvas.
import { Stage } from 'konva/lib/Stage';
import { isUserDoingMultipleSelectionUsingCtrlOrCmdKey } from '@/common/utils/shapes';
import { KonvaEventObject } from 'konva/lib/Node';
import { useCanvasContext } from '@/core/providers';

// There's a bug here: if you make a multiple selectin and start dragging
// inside the selection but on a blank area it won't drag the selection
Expand Down Expand Up @@ -49,6 +50,8 @@ export const useMultipleSelectionShapeHook = (
visible: false,
});

const { setIsThumbnailContextMenuVisible } = useCanvasContext();

const isDraggingSelection = (mouseCoords: Coord) => {
if (!transformerRef.current) {
return false;
Expand Down Expand Up @@ -166,6 +169,8 @@ export const useMultipleSelectionShapeHook = (
height: 0,
visible: true,
});

setIsThumbnailContextMenuVisible(false);
};

const handleMouseMove = (e: any) => {
Expand Down
6 changes: 5 additions & 1 deletion src/pods/context-menu/use-context-menu.hook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ dropRef }) => {

const handleRightClick = (event: MouseEvent) => {
event.preventDefault();
if (selectionInfo.getSelectedShapeData()) {
if (
selectionInfo.getSelectedShapeData() &&
stageRef.current &&
stageRef.current.container().contains(event.target as Node)
) {
setShowContextMenu(true);
setContextMenuPosition({ x: event.clientX, y: event.clientY });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.context-menu {
position: absolute;
top: 50%;
left: 50%;
width: 80%;
height: auto;
transform: translate(-50%, -50%);
border: 1px solid var(--primary-500);
background-color: var(--primary-100);
opacity: 0.9;
}

.container {
display: flex;
gap: 0.5em;
align-items: center;
font-size: var(--fs-xs);
padding: var(--space-xs) var(--space-md);
border-bottom: 1px solid var(--primary-300);
cursor: pointer;
}

.container :first-child {
flex: 1;
}

.container:hover {
background-color: var(--primary-200);
}

.disabled {
cursor: not-allowed;
opacity: 0.5;
background-color: var(--primary-200);
}

.shortcut {
color: var(--primary-400);
font-weight: 500;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { useCanvasContext } from '@/core/providers';
import classes from './context-menu.component.module.css';
import { CopyIcon, DeleteIcon } from '@/common/components/icons';

interface ThumbPageContextMenuProps {
contextMenuRef: React.RefObject<HTMLDivElement>;
setShowContextMenu: (show: boolean) => void;
pageIndex: number;
}

export const ThumbPageContextMenu: React.FunctionComponent<
ThumbPageContextMenuProps
> = props => {
const { contextMenuRef, setShowContextMenu, pageIndex } = props;
const {
setIsThumbnailContextMenuVisible,
fullDocument,
duplicatePage,
deletePage,
} = useCanvasContext();

enum ContextButtonType {
'Duplicate',
'Rename',
'Delete',
}

const handleClickOnContextButton = (
event: React.MouseEvent,
buttonClicked: ContextButtonType
) => {
event.stopPropagation();
switch (buttonClicked) {
case ContextButtonType.Duplicate:
duplicatePage(pageIndex);
break;
case ContextButtonType.Rename:
console.log('Rename');
break;
case ContextButtonType.Delete:
deletePage(pageIndex);
break;
}
setShowContextMenu(false);
setIsThumbnailContextMenuVisible(false);
};

return (
<div ref={contextMenuRef} className={classes['context-menu']}>
<div
onClick={event =>
handleClickOnContextButton(event, ContextButtonType.Duplicate)
}
className={classes.container}
>
<p>Duplicate</p>
<CopyIcon />
</div>
<div
onClick={event =>
handleClickOnContextButton(event, ContextButtonType.Delete)
}
className={
fullDocument.pages.length === 1
? `${classes.container} ${classes.disabled}`
: `${classes.container}`
}
>
<p>Delete</p>
<DeleteIcon />
</div>
</div>
);
};
69 changes: 45 additions & 24 deletions src/pods/thumb-pages/components/thumb-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { calculateCanvasBounds } from '@/pods/toolbar/components/export-button/e
import { KonvaEventObject } from 'konva/lib/Node';
import { createRef, useRef } from 'react';
import { Layer, Stage } from 'react-konva';
import { ThumbPageContextMenu } from './context-menu/context-menu.component';
import { useContextMenu } from '../use-context-menu-thumb.hook';

interface Props {
pageIndex: number;
Expand All @@ -13,15 +15,12 @@ interface Props {

export const ThumbPage: React.FunctionComponent<Props> = props => {
const { pageIndex, onSetActivePage } = props;

const { fullDocument } = useCanvasContext();

const page = fullDocument.pages[pageIndex];
const shapes = page.shapes;
const fakeShapeRefs = useRef<ShapeRefs>({});

const bounds = calculateCanvasBounds(shapes);

const canvasSize = {
width: bounds.x + bounds.width,
height: bounds.y + bounds.height,
Expand All @@ -30,27 +29,49 @@ export const ThumbPage: React.FunctionComponent<Props> = props => {
const scaleFactorY = 180 / canvasSize.height;
const finalScale = Math.min(scaleFactorX, scaleFactorY);

const {
showContextMenu,
contextMenuRef,
setShowContextMenu,
handleShowContextMenu,
} = useContextMenu();

return (
<div
style={{ width: '250px', height: '180px', border: '1px solid red' }}
onClick={() => onSetActivePage(page.id)}
>
<Stage width={250} height={180} scaleX={finalScale} scaleY={finalScale}>
<Layer>
{shapes.map(shape => {
if (!fakeShapeRefs.current[shape.id]) {
fakeShapeRefs.current[shape.id] = createRef();
}
return renderShapeComponent(shape, {
handleSelected: () => {},
shapeRefs: fakeShapeRefs,
handleDragEnd:
(_: string) => (_: KonvaEventObject<DragEvent>) => {},
handleTransform: () => {},
});
})}
</Layer>
</Stage>
</div>
<>
<div
style={{
width: '250px',
height: '180px',
border: '1px solid red',
position: 'relative',
}}
onClick={() => onSetActivePage(page.id)}
onContextMenu={handleShowContextMenu}
>
<Stage width={250} height={180} scaleX={finalScale} scaleY={finalScale}>
<Layer>
{shapes.map(shape => {
if (!fakeShapeRefs.current[shape.id]) {
fakeShapeRefs.current[shape.id] = createRef();
}
return renderShapeComponent(shape, {
handleSelected: () => {},
shapeRefs: fakeShapeRefs,
handleDragEnd:
(_: string) => (_: KonvaEventObject<DragEvent>) => {},
handleTransform: () => {},
});
})}
</Layer>
</Stage>
{showContextMenu && (
<ThumbPageContextMenu
contextMenuRef={contextMenuRef}
setShowContextMenu={setShowContextMenu}
pageIndex={pageIndex}
/>
)}
</div>
</>
);
};
Loading