import { AccessTime, CalendarMonth, Gesture, Badge, Work, Close } from '@mui/icons-material' import { Box, CircularProgress, Divider, FormControl, InputLabel, MenuItem, Select } from '@mui/material' import styles from './style.module.scss' import { useEffect, useState } from 'react' import * as PDFJS from 'pdfjs-dist' import { ProfileMetadata, User } from '../../types' import { PdfFile, DrawTool, MouseState, PdfPage, DrawnField, MarkType } from '../../types/drawing' import { truncate } from 'lodash' import { hexToNpub } from '../../utils' import { toPdfFiles } from '../../utils/pdf.ts' PDFJS.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url ).toString() interface Props { selectedFiles: File[] users: User[] metadata: { [key: string]: ProfileMetadata } onDrawFieldsChange: (pdfFiles: PdfFile[]) => void } export const DrawPDFFields = (props: Props) => { const { selectedFiles } = props const [pdfFiles, setPdfFiles] = useState([]) const [parsingPdf, setParsingPdf] = useState(false) const [showDrawToolBox] = useState(true) const [selectedTool, setSelectedTool] = useState() const [toolbox] = useState([ { identifier: MarkType.SIGNATURE, icon: , label: 'Signature', active: false }, { identifier: MarkType.FULLNAME, icon: , label: 'Full Name', active: true }, { identifier: MarkType.JOBTITLE, icon: , label: 'Job Title', active: false }, { identifier: MarkType.DATE, icon: , label: 'Date', active: false }, { identifier: MarkType.DATETIME, icon: , label: 'Datetime', active: false } ]) const [mouseState, setMouseState] = useState({ clicked: false }) useEffect(() => { if (selectedFiles) { /** * Reads the pdf binary files and converts it's pages to images * creates the pdfFiles object and sets to a state */ const parsePdfPages = async () => { const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles) setPdfFiles(pdfFiles) } setParsingPdf(true) parsePdfPages().finally(() => { setParsingPdf(false) }) } }, [selectedFiles]) useEffect(() => { if (pdfFiles) props.onDrawFieldsChange(pdfFiles) }, [pdfFiles, props]) /** * Drawing events */ useEffect(() => { // window.addEventListener('mousedown', onMouseDown); window.addEventListener('mouseup', onMouseUp) return () => { // window.removeEventListener('mousedown', onMouseDown); window.removeEventListener('mouseup', onMouseUp) } }, []) const refreshPdfFiles = () => { setPdfFiles([...pdfFiles]) } /** * Fired only when left click and mouse over pdf page * Creates new drawnElement and pushes in the array * It is re rendered and visible right away * * @param event Mouse event * @param page PdfPage where press happened */ const onMouseDown = ( event: React.MouseEvent, page: PdfPage ) => { // Proceed only if left click if (event.button !== 0) return // Only allow drawing if mouse is not over other drawn element const isOverPdfImageWrapper = event.currentTarget.tagName === 'IMG' if (!selectedTool || !isOverPdfImageWrapper) { return } const { mouseX, mouseY } = getMouseCoordinates(event) const newField: DrawnField = { left: mouseX, top: mouseY, width: 0, height: 0, counterpart: '', type: selectedTool.identifier } page.drawnFields.push(newField) setMouseState((prev) => { return { ...prev, clicked: true } }) } /** * Drawing is finished, resets all the variables used to draw * @param event Mouse event */ const onMouseUp = () => { setMouseState((prev) => { return { ...prev, clicked: false, dragging: false, resizing: false } }) } /** * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved * which alters the newly created drawing element, resizing it while mouse move * @param event Mouse event * @param page PdfPage where moving is happening */ const onMouseMove = ( event: React.MouseEvent, page: PdfPage ) => { if (mouseState.clicked && selectedTool) { const lastElementIndex = page.drawnFields.length - 1 const lastDrawnField = page.drawnFields[lastElementIndex] const { mouseX, mouseY } = getMouseCoordinates(event) const width = mouseX - lastDrawnField.left const height = mouseY - lastDrawnField.top lastDrawnField.width = width lastDrawnField.height = height const currentDrawnFields = page.drawnFields currentDrawnFields[lastElementIndex] = lastDrawnField refreshPdfFiles() } } /** * Fired when event happens on the drawn element which will be moved * mouse coordinates relative to drawn element will be stored * so when we start moving, offset can be calculated * mouseX - offsetX * mouseY - offsetY * * @param event Mouse event * @param drawnField Which we are moving */ const onDrawnFieldMouseDown = ( event: React.MouseEvent ) => { event.stopPropagation() // Proceed only if left click if (event.button !== 0) return const drawingRectangleCoords = getMouseCoordinates(event) setMouseState({ dragging: true, clicked: false, coordsInWrapper: { mouseX: drawingRectangleCoords.mouseX, mouseY: drawingRectangleCoords.mouseY } }) } /** * Moves the drawnElement by the mouse position (mouse can grab anywhere on the drawn element) * @param event Mouse event * @param drawnField which we are moving */ const onDranwFieldMouseMove = ( event: React.MouseEvent, drawnField: DrawnField ) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates( event, event.currentTarget.parentNode as HTMLElement ) const coordsOffset = mouseState.coordsInWrapper if (coordsOffset) { let left = mouseX - coordsOffset.mouseX let top = mouseY - coordsOffset.mouseY const rightLimit = rect.width - drawnField.width - 3 const bottomLimit = rect.height - drawnField.height - 3 if (left < 0) left = 0 if (top < 0) top = 0 if (left > rightLimit) left = rightLimit if (top > bottomLimit) top = bottomLimit drawnField.left = left drawnField.top = top refreshPdfFiles() } } } /** * Fired when clicked on the resize handle, sets the state for a resize action * @param event Mouse event * @param drawnField which we are resizing */ const onResizeHandleMouseDown = ( event: React.MouseEvent ) => { // Proceed only if left click if (event.button !== 0) return event.stopPropagation() setMouseState({ resizing: true }) } /** * Resizes the drawn element by the mouse position * @param event Mouse event * @param drawnField which we are resizing */ const onResizeHandleMouseMove = ( event: React.MouseEvent, drawnField: DrawnField ) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates( event, event.currentTarget.parentNode as HTMLElement ) const width = mouseX - drawnField.left const height = mouseY - drawnField.top drawnField.width = width drawnField.height = height refreshPdfFiles() } } /** * Removes the drawn element using the indexes in the params * @param event Mouse event * @param pdfFileIndex pdf file index * @param pdfPageIndex pdf page index * @param drawnFileIndex drawn file index */ const onRemoveHandleMouseDown = ( event: React.MouseEvent, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number ) => { event.stopPropagation() pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice( drawnFileIndex, 1 ) } /** * Used to stop mouse click propagating to the parent elements * so select can work properly * @param event Mouse event */ const onUserSelectHandleMouseDown = ( event: React.MouseEvent ) => { event.stopPropagation() } /** * Gets the mouse coordinates relative to a element in the `event` param * @param event MouseEvent * @param customTarget mouse coordinates relative to this element, if not provided * event.target will be used */ const getMouseCoordinates = ( event: React.MouseEvent, customTarget?: HTMLElement ) => { const target = customTarget ? customTarget : event.currentTarget const rect = target.getBoundingClientRect() const mouseX = event.clientX - rect.left //x position within the element. const mouseY = event.clientY - rect.top //y position within the element. return { mouseX, mouseY, rect } } /** * Changes the drawing tool * @param drawTool to draw with */ const handleToolSelect = (drawTool: DrawTool) => { // If clicked on the same tool, unselect if (drawTool.identifier === selectedTool?.identifier) { setSelectedTool(null) return } setSelectedTool(drawTool) } /** * Renders the pdf pages and drawing elements */ const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { return ( <> {pdfFile.pages.map((page, pdfPageIndex: number) => { return (
{ onMouseMove(event, page) }} onMouseDown={(event) => { onMouseDown(event, page) }} > {page.drawnFields.map((drawnField, drawnFieldIndex: number) => { return (
{ onDranwFieldMouseMove(event, drawnField) }} className={styles.drawingRectangle} style={{ left: `${drawnField.left}px`, top: `${drawnField.top}px`, width: `${drawnField.width}px`, height: `${drawnField.height}px`, pointerEvents: mouseState.clicked ? 'none' : 'all' }} > { onResizeHandleMouseMove(event, drawnField) }} className={styles.resizeHandle} > { onRemoveHandleMouseDown( event, pdfFileIndex, pdfPageIndex, drawnFieldIndex ) }} className={styles.removeHandle} >
Counterpart
) })}
) })} ) } if (parsingPdf) { return ( ) } if (!pdfFiles.length) { return '' } return (
{pdfFiles.map((pdfFile, pdfFileIndex: number) => { return ( <>
{getPdfPages(pdfFile, pdfFileIndex)}
{pdfFileIndex < pdfFiles.length - 1 && ( File Separator )} ) })} {showDrawToolBox && ( {toolbox .filter((drawTool) => drawTool.active) .map((drawTool: DrawTool, index: number) => { return (
{ handleToolSelect(drawTool) }} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`} > {drawTool.icon} {drawTool.label}
) })}
)}
) }