Skip to content

Commit

Permalink
Merge pull request #510 from Lemoncode/feature/#495-allow-the-user-du…
Browse files Browse the repository at this point in the history
…plicate-page

Feature/#495 allow the user duplicate page also closes #503 Allow the user delete a page
  • Loading branch information
brauliodiez authored Oct 31, 2024
2 parents 0f3451d + aff357c commit c842073
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 29 deletions.
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

0 comments on commit c842073

Please sign in to comment.