diff --git a/public/icons/book.svg b/public/icons/book.svg new file mode 100644 index 00000000..adab613e --- /dev/null +++ b/public/icons/book.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M208 26H72a30 30 0 0 0-30 30v168a6 6 0 0 0 6 6h144a6 6 0 0 0 0-12H54v-2a18 18 0 0 1 18-18h136a6 6 0 0 0 6-6V32a6 6 0 0 0-6-6m-6 160H72a29.87 29.87 0 0 0-18 6V56a18 18 0 0 1 18-18h130Z"/></svg> \ No newline at end of file diff --git a/public/icons/bookopen.svg b/public/icons/bookopen.svg new file mode 100644 index 00000000..f4af8966 --- /dev/null +++ b/public/icons/bookopen.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M232 50h-72a38 38 0 0 0-32 17.55A38 38 0 0 0 96 50H24a6 6 0 0 0-6 6v144a6 6 0 0 0 6 6h72a26 26 0 0 1 26 26a6 6 0 0 0 12 0a26 26 0 0 1 26-26h72a6 6 0 0 0 6-6V56a6 6 0 0 0-6-6M96 194H30V62h66a26 26 0 0 1 26 26v116.31A37.86 37.86 0 0 0 96 194m130 0h-66a37.87 37.87 0 0 0-26 10.32V88a26 26 0 0 1 26-26h66ZM160 90h40a6 6 0 0 1 0 12h-40a6 6 0 0 1 0-12m46 38a6 6 0 0 1-6 6h-40a6 6 0 0 1 0-12h40a6 6 0 0 1 6 6m0 32a6 6 0 0 1-6 6h-40a6 6 0 0 1 0-12h40a6 6 0 0 1 6 6"/></svg> \ No newline at end of file diff --git a/public/icons/books.svg b/public/icons/books.svg new file mode 100644 index 00000000..f7a9ab7f --- /dev/null +++ b/public/icons/books.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M104 34H56a14 14 0 0 0-14 14v160a14 14 0 0 0 14 14h48a14 14 0 0 0 14-14V48a14 14 0 0 0-14-14M54 78h52v100H54Zm2-32h48a2 2 0 0 1 2 2v18H54V48a2 2 0 0 1 2-2m48 164H56a2 2 0 0 1-2-2v-18h52v18a2 2 0 0 1-2 2m125.7-15L196.51 37.16a14 14 0 0 0-16.63-10.85l-46.81 10.06A14.09 14.09 0 0 0 122.3 53l33.19 157.81a14 14 0 0 0 6.1 8.9a13.85 13.85 0 0 0 7.57 2.26a13.6 13.6 0 0 0 3-.32l46.81-10.05A14.09 14.09 0 0 0 229.7 195m-82.81-83.32l50.73-10.9l14.12 67.16L161 178.81Zm-6.63-31.56L191 69.19L195.15 89l-50.73 10.9Zm-4.66-32l46.8-10.05a2 2 0 0 1 .42 0a1.9 1.9 0 0 1 1.05.32a2 2 0 0 1 .89 1.31l3.75 17.82l-50.72 10.82l-3.74-17.78a2.07 2.07 0 0 1 1.55-2.46Zm80.81 151.8L169.6 210a1.92 1.92 0 0 1-1.47-.27a2 2 0 0 1-.89-1.31l-3.75-17.82l50.72-10.9l3.79 17.73a2.07 2.07 0 0 1-1.59 2.47Z"/></svg> \ No newline at end of file diff --git a/public/icons/calendarplus.svg b/public/icons/calendarplus.svg new file mode 100644 index 00000000..feb4033b --- /dev/null +++ b/public/icons/calendarplus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M208 34h-26V24a6 6 0 0 0-12 0v10H86V24a6 6 0 0 0-12 0v10H48a14 14 0 0 0-14 14v160a14 14 0 0 0 14 14h160a14 14 0 0 0 14-14V48a14 14 0 0 0-14-14M48 46h26v10a6 6 0 0 0 12 0V46h84v10a6 6 0 0 0 12 0V46h26a2 2 0 0 1 2 2v34H46V48a2 2 0 0 1 2-2m160 164H48a2 2 0 0 1-2-2V94h164v114a2 2 0 0 1-2 2m-50-58a6 6 0 0 1-6 6h-18v18a6 6 0 0 1-12 0v-18h-18a6 6 0 0 1 0-12h18v-18a6 6 0 0 1 12 0v18h18a6 6 0 0 1 6 6"/></svg> \ No newline at end of file diff --git a/public/icons/chalkboard.svg b/public/icons/chalkboard.svg new file mode 100644 index 00000000..3b6ba032 --- /dev/null +++ b/public/icons/chalkboard.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M216 42H40a14 14 0 0 0-14 14v144a14 14 0 0 0 14 14h13.39a6 6 0 0 0 5.42-3.43a50 50 0 0 1 90.38 0a6 6 0 0 0 5.42 3.43H216a14 14 0 0 0 14-14V56a14 14 0 0 0-14-14M78 144a26 26 0 1 1 26 26a26 26 0 0 1-26-26m140 56a2 2 0 0 1-2 2h-57.73a62.34 62.34 0 0 0-31.48-27.61a38 38 0 1 0-45.58 0A62.34 62.34 0 0 0 49.73 202H40a2 2 0 0 1-2-2V56a2 2 0 0 1 2-2h176a2 2 0 0 1 2 2ZM198 80v96a6 6 0 0 1-6 6h-16a6 6 0 0 1 0-12h10V86H70v10a6 6 0 0 1-12 0V80a6 6 0 0 1 6-6h128a6 6 0 0 1 6 6"/></svg> \ No newline at end of file diff --git a/public/icons/clockuser.svg b/public/icons/clockuser.svg new file mode 100644 index 00000000..a00a5289 --- /dev/null +++ b/public/icons/clockuser.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M134 72v46.29l39.32-19.66a6 6 0 0 1 5.36 10.74l-48 24A6 6 0 0 1 122 128V72a6 6 0 0 1 12 0m-6 146a90 90 0 1 1 90-90a6 6 0 0 0 12 0a102 102 0 1 0-102 102a6 6 0 0 0 0-12m101.8 4.46a6 6 0 0 1-11.6 3.08C215.14 214 204.37 206 192 206s-23.14 8-26.2 19.54A6 6 0 0 1 160 230a6.3 6.3 0 0 1-1.54-.2a6 6 0 0 1-4.26-7.34A38.1 38.1 0 0 1 172.72 199a30 30 0 1 1 38.56 0a38.1 38.1 0 0 1 18.52 23.46M174 176a18 18 0 1 0 18-18a18 18 0 0 0-18 18"/></svg> \ No newline at end of file diff --git a/public/icons/minuscircle.svg b/public/icons/minuscircle.svg new file mode 100644 index 00000000..0d5fde54 --- /dev/null +++ b/public/icons/minuscircle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M174 128a6 6 0 0 1-6 6H88a6 6 0 0 1 0-12h80a6 6 0 0 1 6 6m56 0A102 102 0 1 1 128 26a102.12 102.12 0 0 1 102 102m-12 0a90 90 0 1 0-90 90a90.1 90.1 0 0 0 90-90"/></svg> \ No newline at end of file diff --git a/public/icons/person.svg b/public/icons/person.svg new file mode 100644 index 00000000..8a66b874 --- /dev/null +++ b/public/icons/person.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M128 70a30 30 0 1 0-30-30a30 30 0 0 0 30 30m0-48a18 18 0 1 1-18 18a18 18 0 0 1 18-18m88.88 113.42l-45.21-51.26A30 30 0 0 0 149.17 74h-42.34a30 30 0 0 0-22.5 10.15l-45.21 51.27A18 18 0 0 0 64.46 161l21.11-16.93l-18.13 68.85a18 18 0 0 0 32.75 14.94L128 180l27.81 47.91a18 18 0 0 0 32.75-14.94l-18.13-68.87l21.11 16.9a18 18 0 0 0 25.34-25.56Zm-8.63 16.82a6 6 0 0 1-8.49 0a4 4 0 0 0-.49-.44l-35.51-28.48a6 6 0 0 0-9.56 6.2l22.87 86.93a8 8 0 0 0 .37 1a6 6 0 0 1-10.88 5.07a4 4 0 0 0-.25-.48L133.19 165a6 6 0 0 0-10.38 0l-33.12 57.05a4 4 0 0 0-.25.48a6 6 0 0 1-10.88-5.07a8 8 0 0 0 .37-1l22.87-86.93a6 6 0 0 0-2.53-6.53a6.07 6.07 0 0 0-3.27-1a6 6 0 0 0-3.76 1.32L56.73 151.8a4 4 0 0 0-.49.44a6 6 0 0 1-8.49-8.49l.26-.27l45.32-51.39a18 18 0 0 1 13.5-6.09h42.34a18 18 0 0 1 13.5 6.09L208 143.48l.26.27a6 6 0 0 1-.01 8.49"/></svg> \ No newline at end of file diff --git a/public/icons/pluscircle.svg b/public/icons/pluscircle.svg new file mode 100644 index 00000000..53122f79 --- /dev/null +++ b/public/icons/pluscircle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M128 26a102 102 0 1 0 102 102A102.12 102.12 0 0 0 128 26m0 192a90 90 0 1 1 90-90a90.1 90.1 0 0 1-90 90m46-90a6 6 0 0 1-6 6h-34v34a6 6 0 0 1-12 0v-34H88a6 6 0 0 1 0-12h34V88a6 6 0 0 1 12 0v34h34a6 6 0 0 1 6 6"/></svg> \ No newline at end of file diff --git a/public/icons/userfocus.svg b/public/icons/userfocus.svg new file mode 100644 index 00000000..4d35eaec --- /dev/null +++ b/public/icons/userfocus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M222 40v36a6 6 0 0 1-12 0V46h-30a6 6 0 0 1 0-12h36a6 6 0 0 1 6 6m-6 134a6 6 0 0 0-6 6v30h-30a6 6 0 0 0 0 12h36a6 6 0 0 0 6-6v-36a6 6 0 0 0-6-6M76 210H46v-30a6 6 0 0 0-12 0v36a6 6 0 0 0 6 6h36a6 6 0 0 0 0-12M40 82a6 6 0 0 0 6-6V46h30a6 6 0 0 0 0-12H40a6 6 0 0 0-6 6v36a6 6 0 0 0 6 6m136 92a6 6 0 0 1-4.8-2.4a54 54 0 0 0-86.4 0a6 6 0 1 1-9.6-7.2a65.65 65.65 0 0 1 29.69-22.26a38 38 0 1 1 46.22 0a65.65 65.65 0 0 1 29.69 22.26a6 6 0 0 1-4.8 9.6m-48-36a26 26 0 1 0-26-26a26 26 0 0 0 26 26"/></svg> \ No newline at end of file diff --git a/public/icons/userlist.svg b/public/icons/userlist.svg new file mode 100644 index 00000000..8eb0e2af --- /dev/null +++ b/public/icons/userlist.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 256 256"><path fill="currentColor" d="M154 80a6 6 0 0 1 6-6h88a6 6 0 0 1 0 12h-88a6 6 0 0 1-6-6m94 42h-88a6 6 0 0 0 0 12h88a6 6 0 0 0 0-12m0 48h-64a6 6 0 0 0 0 12h64a6 6 0 0 0 0-12m-98.19 20.5a6 6 0 1 1-11.62 3C131.7 168.29 107.23 150 80 150s-51.7 18.29-58.19 43.49a6 6 0 1 1-11.62-3c5.74-22.28 23-40.07 44.67-48a46 46 0 1 1 50.28 0c21.65 7.94 38.94 25.73 44.67 48.01M80 138a34 34 0 1 0-34-34a34 34 0 0 0 34 34"/></svg> \ No newline at end of file diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts index bc7656fd..606f5e82 100644 --- a/src/core/providers/canvas/canvas.model.ts +++ b/src/core/providers/canvas/canvas.model.ts @@ -122,6 +122,8 @@ export interface CanvasContextModel { setCanvasSize: (canvasDimensions: CanvasSize) => void; customColors: (string | null)[]; updateColorSlot: (color: string, index: number) => void; + dropRef: React.MutableRefObject<HTMLDivElement | null>; + setDropRef: (dropRef: React.MutableRefObject<HTMLDivElement | null>) => void; } export const APP_CONSTANTS = { diff --git a/src/core/providers/canvas/canvas.provider.tsx b/src/core/providers/canvas/canvas.provider.tsx index 105aee47..917f37db 100644 --- a/src/core/providers/canvas/canvas.provider.tsx +++ b/src/core/providers/canvas/canvas.provider.tsx @@ -183,11 +183,16 @@ export const CanvasProvider: React.FC<Props> = props => { }); }; + const [dropRef, setDropRef] = React.useState< + React.MutableRefObject<HTMLDivElement | null> + >(React.useRef<HTMLDivElement>(null)); + const { copyShapeToClipboard, pasteShapeFromClipboard, canCopy, canPaste } = useClipboard( pasteShapes, document.pages[document.activePageIndex].shapes, - selectionInfo + selectionInfo, + dropRef ); const createNewFullDocument = () => { @@ -362,6 +367,8 @@ export const CanvasProvider: React.FC<Props> = props => { setCanvasSize: setCanvasSize, customColors, updateColorSlot, + dropRef, + setDropRef, }} > {children} diff --git a/src/core/providers/canvas/use-clipboard.hook.tsx b/src/core/providers/canvas/use-clipboard.hook.tsx index 1b8c88a8..fa193712 100644 --- a/src/core/providers/canvas/use-clipboard.hook.tsx +++ b/src/core/providers/canvas/use-clipboard.hook.tsx @@ -10,7 +10,8 @@ import { export const useClipboard = ( pasteShapes: (shapes: ShapeModel[]) => void, shapes: ShapeModel[], - selectionInfo: { selectedShapesIds: string[] | null } + selectionInfo: { selectedShapesIds: string[] | null }, + dropRef: React.MutableRefObject<HTMLDivElement | null> ) => { const [clipboardShape, setClipboardShape] = useState<ShapeModel[] | null>( null @@ -30,11 +31,21 @@ export const useClipboard = ( } }; + const updateClipboardShapes = (shapes: ShapeModel[]) => { + copyCount.current = 0; + clipboardShapesRef.current = [...shapes]; + }; + const pasteShapeFromClipboard = () => { if (clipboardShapesRef.current) { const newShapes: ShapeModel[] = cloneShapes(clipboardShapesRef.current); validateShapes(newShapes); - adjustShapesPosition(newShapes, copyCount.current); + adjustShapesPosition( + newShapes, + copyCount.current, + dropRef, + updateClipboardShapes + ); pasteShapes(newShapes); copyCount.current++; } diff --git a/src/pods/canvas/canvas.pod.tsx b/src/pods/canvas/canvas.pod.tsx index e2f06233..837bdb89 100644 --- a/src/pods/canvas/canvas.pod.tsx +++ b/src/pods/canvas/canvas.pod.tsx @@ -30,6 +30,7 @@ export const CanvasPod = () => { updateShapePosition, stageRef, canvasSize, + setDropRef, } = useCanvasContext(); const { @@ -52,6 +53,9 @@ export const CanvasPod = () => { const { isDraggedOver, dropRef } = useDropShape(); useMonitorShape(dropRef, addNewShapeAndSetSelected); + useEffect(() => { + if (dropRef.current) setDropRef(dropRef); + }, [dropRef, setDropRef]); const getSelectedShapeKonvaId = (): string[] => { let result: string[] = []; diff --git a/src/pods/canvas/clipboard.utils.ts b/src/pods/canvas/clipboard.utils.ts index f8d4fcdc..c6879698 100644 --- a/src/pods/canvas/clipboard.utils.ts +++ b/src/pods/canvas/clipboard.utils.ts @@ -1,6 +1,16 @@ import cloneDeep from 'lodash.clonedeep'; import { ShapeModel } from '@/core/model'; import invariant from 'tiny-invariant'; +interface Displacement { + x: number; + y: number; +} +interface Viewport { + xMinVisible: number; + xMaxVisible: number; + yMinVisible: number; + yMaxVisible: number; +} export const findShapesById = ( shapeIds: string[], @@ -17,12 +27,89 @@ export const cloneShape = (shape: ShapeModel): ShapeModel => { return cloneDeep(shape); }; -export const adjustShapesPosition = (shapes: ShapeModel[], copyCount: number) => - shapes.map(shape => adjustShapePosition(shape, copyCount)); +function areAllShapesFullyVisible( + shapes: ShapeModel[], + viewport: Viewport, + copyCount: number +): boolean { + return shapes.every(shape => { + const offsetX = 20 * copyCount; + const offsetY = 20 * copyCount; + const newX = shape.x + offsetX; + const newY = shape.y + offsetY; + const left = newX; + const right = newX + shape.width; + const top = newY; + const bottom = newY + shape.height; -export const adjustShapePosition = (shape: ShapeModel, copyCount: number) => { - shape.x += 20 * copyCount; - shape.y += 20 * copyCount; + return ( + left >= viewport.xMinVisible && + right <= viewport.xMaxVisible && + top >= viewport.yMinVisible && + bottom <= viewport.yMaxVisible + ); + }); +} + +export const adjustShapesPosition = ( + shapes: ShapeModel[], + copyCount: number, + dropRef: React.MutableRefObject<HTMLDivElement | null>, + updateClipboardShapes: (shapes: ShapeModel[]) => void +) => { + const container = dropRef.current; + if (!container) return; + + const containerRect = container.getBoundingClientRect(); + const scrollLeft = container.scrollLeft; + const scrollTop = container.scrollTop; + + const viewPort: Viewport = { + xMinVisible: scrollLeft, + xMaxVisible: scrollLeft + containerRect.width, + yMinVisible: scrollTop, + yMaxVisible: scrollTop + containerRect.height, + }; + + const allVisible = areAllShapesFullyVisible(shapes, viewPort, copyCount); + + if (allVisible) { + shapes.forEach(shape => { + adjustShapePosition(shape, copyCount); + }); + } else { + const shape0 = shapes[0]; + const oldX0 = shape0.x + 20 * copyCount; + const oldY0 = shape0.y + 20 * copyCount; + + const centerX = scrollLeft + containerRect.width / 2; + const centerY = scrollTop + containerRect.height / 2; + + const targetX0 = centerX - shape0.width / 2; + const targetY0 = centerY - shape0.height / 2; + + const displacement: Displacement = { + x: targetX0 - oldX0, + y: targetY0 - oldY0, + }; + + shapes.forEach(shape => { + adjustShapePosition(shape, copyCount, displacement); + }); + } + + updateClipboardShapes(shapes); +}; + +export const adjustShapePosition = ( + shape: ShapeModel, + copyCount: number, + d: Displacement = { x: 0, y: 0 } +) => { + const originalX = shape.x + 20 * copyCount; + const originalY = shape.y + 20 * copyCount; + shape.x = originalX + d.x; + shape.y = originalY + d.y; }; export const validateShapes = (shapes: ShapeModel[]) => { diff --git a/src/pods/properties/components/icon-selector/modal/icons.ts b/src/pods/properties/components/icon-selector/modal/icons.ts index d5ef5652..4fee72bf 100644 --- a/src/pods/properties/components/icon-selector/modal/icons.ts +++ b/src/pods/properties/components/icon-selector/modal/icons.ts @@ -2302,4 +2302,70 @@ export const iconCollection: IconInfo[] = [ searchTerms: ['rocket', 'launch', 'space', 'fly'], categories: ['IT'], }, + { + name: 'Books', + filename: 'books.svg', + searchTerms: ['books', 'library', 'knowledge', 'read'], + categories: ['IT'], + }, + { + name: 'Chalkboard', + filename: 'chalkboard.svg', + searchTerms: ['chalkboard', 'blackboard', 'school', 'teach', 'learn'], + categories: ['IT'], + }, + { + name: 'Book', + filename: 'book.svg', + searchTerms: ['book', 'library', 'knowledge', 'read'], + categories: ['IT'], + }, + { + name: 'Book open', + filename: 'bookopen.svg', + searchTerms: ['book', 'open', 'library', 'knowledge', 'read'], + categories: ['IT'], + }, + { + name: 'User list', + filename: 'userlist.svg', + searchTerms: ['user', 'list', 'people', 'group', 'team'], + categories: ['IT'], + }, + { + name: 'Person', + filename: 'person.svg', + searchTerms: ['person', 'user', 'human', 'profile', 'individual'], + categories: ['IT'], + }, + { + name: 'User focus', + filename: 'userfocus.svg', + searchTerms: ['user', 'focus', 'human', 'profile', 'emphasis'], + categories: ['IT'], + }, + { + name: 'Clock user', + filename: 'clockuser.svg', + searchTerms: ['clock', 'user', 'human', 'timetable', 'schedule'], + categories: ['IT'], + }, + { + name: 'Plus circle', + filename: 'pluscircle.svg', + searchTerms: ['plus', 'circle', 'add', 'create', 'new', 'more'], + categories: ['IT'], + }, + { + name: 'Minus circle', + filename: 'minuscircle.svg', + searchTerms: ['minus', 'circle', 'remove', 'delete', 'less'], + categories: ['IT'], + }, + { + name: 'Calendar plus', + filename: 'calendarplus.svg', + searchTerms: ['calendar', 'plus', 'add', 'create', 'new'], + categories: ['IT'], + }, ];