import { AccessTime, CalendarMonth, ExpandMore, Gesture, PictureAsPdf, Badge, Work, Close } from '@mui/icons-material' import { Box, Typography, Accordion, AccordionDetails, AccordionSummary, CircularProgress, 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 = 'node_modules/pdfjs-dist/build/pdf.worker.mjs'; 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, setShowDrawToolBox] = useState(false) 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) { setParsingPdf(true) parsePdfPages().finally(() => { setParsingPdf(false) }) } }, [selectedFiles]) useEffect(() => { if (pdfFiles) props.onDrawFieldsChange(pdfFiles) }, [pdfFiles]) /** * 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: any, 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.target.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 = (event: MouseEvent) => { 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: any, 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: any, drawnField: DrawnField) => { 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: any, drawnField: DrawnField) => { if (mouseState.dragging) { const { mouseX, mouseY, rect } = getMouseCoordinates(event, event.target.parentNode) 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: any, drawnField: DrawnField) => { // 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: any, drawnField: DrawnField) => { if (mouseState.resizing) { const { mouseX, mouseY } = getMouseCoordinates(event, event.target.parentNode.parentNode) 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: any, 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: any) => { 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: any, customTarget?: any) => { const target = customTarget ? customTarget : event.target 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 } } /** * 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); console.log('pdf files: ', pdfFiles); setPdfFiles(pdfFiles) } /** * * @returns if expanded pdf accordion is present */ const hasExpandedPdf = () => { return !!pdfFiles.filter(pdfFile => !!pdfFile.expanded).length } const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => { pdfFile.expanded = expanded refreshPdfFiles() setShowDrawToolBox(hasExpandedPdf()) } /** * 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 (
{ onDrawnFieldMouseDown(event, drawnField) }} onMouseMove={(event) => { 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' }} > {onResizeHandleMouseDown(event, drawnField)}} onMouseMove={(event) => {onResizeHandleMouseMove(event, drawnField)}} className={styles.resizeHandle} > {onRemoveHandleMouseDown(event, pdfFileIndex, pdfPageIndex, drawnFieldIndex)}} className={styles.removeHandle} >
{onUserSelectHandleMouseDown(event)}} className={styles.userSelect} > Counterpart
) })}
) })}
) } if (parsingPdf) { return ( ) } if (!pdfFiles.length) { return '' } return ( Draw fields on the PDFs: {pdfFiles.map((pdfFile, pdfFileIndex: number) => { return ( {handleAccordionExpandChange(expanded, pdfFile)}}> } aria-controls={`panel${pdfFileIndex}-content`} id={`panel${pdfFileIndex}header`} > {pdfFile.file.name} {getPdfPages(pdfFile, pdfFileIndex)} ) })} {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 } ) })} )} ) }