484 lines
14 KiB
TypeScript
484 lines
14 KiB
TypeScript
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';
|
|
PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs';
|
|
|
|
interface Props {
|
|
selectedFiles: any[]
|
|
users: User[]
|
|
metadata: { [key: string]: ProfileMetadata }
|
|
onDrawFieldsChange: (pdfFiles: PdfFile[]) => void
|
|
}
|
|
|
|
export const DrawPDFFields = (props: Props) => {
|
|
const { selectedFiles } = props
|
|
|
|
const [pdfFiles, setPdfFiles] = useState<PdfFile[]>([])
|
|
const [parsingPdf, setParsingPdf] = useState<boolean>(false)
|
|
const [showDrawToolBox, setShowDrawToolBox] = useState<boolean>(false)
|
|
|
|
const [selectedTool, setSelectedTool] = useState<DrawTool | null>()
|
|
const [toolbox] = useState<DrawTool[]>([
|
|
{
|
|
identifier: MarkType.SIGNATURE,
|
|
icon: <Gesture/>,
|
|
label: 'Signature',
|
|
active: false
|
|
|
|
},
|
|
{
|
|
identifier: MarkType.FULLNAME,
|
|
icon: <Badge/>,
|
|
label: 'Full Name',
|
|
active: true
|
|
},
|
|
{
|
|
identifier: MarkType.JOBTITLE,
|
|
icon: <Work/>,
|
|
label: 'Job Title',
|
|
active: false
|
|
},
|
|
{
|
|
identifier: MarkType.DATE,
|
|
icon: <CalendarMonth/>,
|
|
label: 'Date',
|
|
active: false
|
|
},
|
|
{
|
|
identifier: MarkType.DATETIME,
|
|
icon: <AccessTime/>,
|
|
label: 'Datetime',
|
|
active: false
|
|
},
|
|
])
|
|
|
|
const [mouseState, setMouseState] = useState<MouseState>({
|
|
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])
|
|
}
|
|
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
const onMouseUp = (event: MouseEvent) => {
|
|
setMouseState((prev) => {
|
|
return {
|
|
...prev,
|
|
clicked: false,
|
|
dragging: false,
|
|
resizing: false
|
|
}
|
|
})
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
const onResizeHandleMouseDown = (event: any, drawnField: DrawnField) => {
|
|
// Proceed only if left click
|
|
if (event.button !== 0) return
|
|
|
|
event.stopPropagation()
|
|
|
|
setMouseState({
|
|
resizing: true
|
|
})
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
const onRemoveHandleMouseDown = (event: any, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number) => {
|
|
event.stopPropagation()
|
|
|
|
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(drawnFileIndex, 1)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
const parsePdfPages = async () => {
|
|
const pdfFiles: PdfFile[] = []
|
|
|
|
for (const file of selectedFiles) {
|
|
if (file.type.toLowerCase().includes('pdf')) {
|
|
const data = await readPdf(file)
|
|
const pages = await pdfToImages(data)
|
|
|
|
pdfFiles.push({
|
|
file: file,
|
|
pages: pages,
|
|
expanded: false
|
|
})
|
|
}
|
|
}
|
|
|
|
setPdfFiles(pdfFiles)
|
|
}
|
|
|
|
/**
|
|
* Converts pdf to the images
|
|
* @param data pdf file bytes
|
|
*/
|
|
const pdfToImages = async (data: any): Promise<PdfPage[]> => {
|
|
const images: string[] = [];
|
|
const pdf = await PDFJS.getDocument(data).promise;
|
|
const canvas = document.createElement("canvas");
|
|
|
|
for (let i = 0; i < pdf.numPages; i++) {
|
|
const page = await pdf.getPage(i + 1);
|
|
const viewport = page.getViewport({ scale: 3 });
|
|
const context = canvas.getContext("2d");
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
await page.render({ canvasContext: context!, viewport: viewport }).promise;
|
|
images.push(canvas.toDataURL());
|
|
}
|
|
|
|
return Promise.resolve(images.map((image) => {
|
|
return {
|
|
image,
|
|
drawnFields: []
|
|
}
|
|
}))
|
|
}
|
|
|
|
const readPdf = (file: File) => {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (e: any) => {
|
|
const data = e.target.result
|
|
|
|
resolve(data)
|
|
};
|
|
|
|
reader.onerror = (err) => {
|
|
console.error('err', err)
|
|
reject(err)
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
})
|
|
}
|
|
|
|
const hasExpandedPdf = () => {
|
|
return !!pdfFiles.filter(pdfFile => !!pdfFile.expanded).length
|
|
}
|
|
|
|
const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => {
|
|
pdfFile.expanded = expanded
|
|
|
|
refreshPdfFiles()
|
|
setShowDrawToolBox(hasExpandedPdf())
|
|
}
|
|
|
|
const handleToolSelect = (drawTool: DrawTool) => {
|
|
// If clicked on the same tool, unselect
|
|
if (drawTool.identifier === selectedTool?.identifier) {
|
|
setSelectedTool(null)
|
|
return
|
|
}
|
|
|
|
setSelectedTool(drawTool)
|
|
}
|
|
|
|
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
|
|
return (
|
|
<Box sx={{
|
|
width: '100%'
|
|
}}>
|
|
{pdfFile.pages.map((page, pdfPageIndex: number) => {
|
|
return (
|
|
<div
|
|
key={pdfPageIndex}
|
|
style={{
|
|
border: '1px solid #c4c4c4',
|
|
marginBottom: '10px'
|
|
}}
|
|
className={`${styles.pdfImageWrapper} ${selectedTool ? styles.drawing : ''}`}
|
|
onMouseMove={(event) => {onMouseMove(event, page)}}
|
|
onMouseDown={(event) => {onMouseDown(event, page)}}
|
|
>
|
|
<img draggable="false" style={{ width: '100%' }} src={page.image}/>
|
|
|
|
{page.drawnFields.map((drawnField, drawnFieldIndex: number) => {
|
|
return (
|
|
<div
|
|
key={drawnFieldIndex}
|
|
onMouseDown={(event) => { 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'
|
|
}}
|
|
>
|
|
<span
|
|
onMouseDown={(event) => {onResizeHandleMouseDown(event, drawnField)}}
|
|
onMouseMove={(event) => {onResizeHandleMouseMove(event, drawnField)}}
|
|
className={styles.resizeHandle}
|
|
></span>
|
|
<span
|
|
onMouseDown={(event) => {onRemoveHandleMouseDown(event, pdfFileIndex, pdfPageIndex, drawnFieldIndex)}}
|
|
className={styles.removeHandle}
|
|
>
|
|
<Close fontSize='small'/>
|
|
</span>
|
|
<div
|
|
onMouseDown={(event) => {onUserSelectHandleMouseDown(event)}}
|
|
className={styles.userSelect}
|
|
>
|
|
<FormControl fullWidth size='small'>
|
|
<InputLabel id="counterparts">Counterpart</InputLabel>
|
|
<Select
|
|
value={drawnField.counterpart}
|
|
onChange={(event) => { drawnField.counterpart = event.target.value; refreshPdfFiles() }}
|
|
labelId="counterparts"
|
|
label="Counterparts"
|
|
>
|
|
{props.users.map((user, index) => {
|
|
let displayValue = truncate(hexToNpub(user.pubkey), {
|
|
length: 16
|
|
})
|
|
|
|
const metadata = props.metadata[user.pubkey]
|
|
|
|
if (metadata) {
|
|
displayValue = truncate(metadata.name || metadata.display_name || metadata.username, {
|
|
length: 16
|
|
})
|
|
}
|
|
|
|
return <MenuItem key={index} value={hexToNpub(user.pubkey)}>{displayValue}</MenuItem>
|
|
})}
|
|
</Select>
|
|
</FormControl>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
})}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
if (parsingPdf) {
|
|
return (
|
|
<Box sx={{ width: '100%', textAlign: 'center' }}>
|
|
<CircularProgress/>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
if (!pdfFiles.length) {
|
|
return ''
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ mt: 1 }}>
|
|
<Typography sx={{ mb: 1 }}>Draw fields on the PDFs:</Typography>
|
|
|
|
{pdfFiles.map((pdfFile, pdfFileIndex: number) => {
|
|
return (
|
|
<Accordion key={pdfFileIndex} expanded={pdfFile.expanded} onChange={(_event, expanded) => {handleAccordionExpandChange(expanded, pdfFile)}}>
|
|
<AccordionSummary
|
|
expandIcon={<ExpandMore />}
|
|
aria-controls={`panel${pdfFileIndex}-content`}
|
|
id={`panel${pdfFileIndex}header`}
|
|
>
|
|
<PictureAsPdf sx={{ mr: 1 }}/>
|
|
{pdfFile.file.name}
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
{getPdfPages(pdfFile, pdfFileIndex)}
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
)
|
|
})}
|
|
</Box>
|
|
|
|
{showDrawToolBox && (
|
|
<Box className={styles.drawToolBoxContainer}>
|
|
<Box className={styles.drawToolBox}>
|
|
{toolbox.filter(drawTool => drawTool.active).map((drawTool: DrawTool, index: number) => {
|
|
return (
|
|
<Box
|
|
key={index}
|
|
onClick={() => {handleToolSelect(drawTool)}} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}>
|
|
{ drawTool.icon }
|
|
{ drawTool.label }
|
|
</Box>
|
|
)
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|