575 lines
16 KiB
TypeScript
Raw Normal View History

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'
2024-07-16 12:36:18 +03:00
import { toPdfFiles } from '../../utils/pdf.ts'
2024-08-08 12:39:29 +03:00
PDFJS.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
2024-08-08 12:39:29 +03:00
import.meta.url
).toString()
interface Props {
2024-07-16 12:36:18 +03:00
selectedFiles: File[]
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] = useState<boolean>(true)
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) {
/**
* 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])
}
2024-07-12 15:07:36 +02:00
/**
* 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
*
2024-07-12 15:07:36 +02:00
* @param event Mouse event
* @param page PdfPage where press happened
*/
const onMouseDown = (
event: React.MouseEvent<HTMLDivElement, 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
}
})
}
2024-07-12 15:07:36 +02:00
/**
* 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
}
})
}
2024-07-12 15:07:36 +02:00
/**
* 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<HTMLDivElement, 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()
}
}
2024-07-12 15:07:36 +02:00
/**
* 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
*
2024-07-12 15:07:36 +02:00
* @param event Mouse event
* @param drawnField Which we are moving
*/
const onDrawnFieldMouseDown = (
event: React.MouseEvent<HTMLDivElement, 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
}
})
}
2024-07-12 15:07:36 +02:00
/**
* 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<HTMLDivElement, 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()
}
}
}
2024-07-12 15:07:36 +02:00
/**
* Fired when clicked on the resize handle, sets the state for a resize action
2024-07-12 15:07:36 +02:00
* @param event Mouse event
* @param drawnField which we are resizing
*/
const onResizeHandleMouseDown = (
event: React.MouseEvent<HTMLSpanElement, MouseEvent>
) => {
// Proceed only if left click
if (event.button !== 0) return
event.stopPropagation()
setMouseState({
resizing: true
})
}
2024-07-12 15:07:36 +02:00
/**
* Resizes the drawn element by the mouse position
* @param event Mouse event
* @param drawnField which we are resizing
*/
const onResizeHandleMouseMove = (
event: React.MouseEvent<HTMLSpanElement, 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()
}
}
2024-07-12 15:07:36 +02:00
/**
* 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<HTMLSpanElement, MouseEvent>,
pdfFileIndex: number,
pdfPageIndex: number,
drawnFileIndex: number
) => {
event.stopPropagation()
pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(
drawnFileIndex,
1
)
}
2024-07-12 15:07:36 +02:00
/**
* Used to stop mouse click propagating to the parent elements
* so select can work properly
* @param event Mouse event
*/
const onUserSelectHandleMouseDown = (
event: React.MouseEvent<HTMLDivElement, 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<HTMLElement, 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
}
}
2024-07-12 15:07:36 +02:00
/**
* 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)
}
2024-07-12 15:07:36 +02:00
/**
* Renders the pdf pages and drawing elements
*/
const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => {
return (
<>
{pdfFile.pages.map((page, pdfPageIndex: number) => {
return (
<div
key={pdfPageIndex}
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={onDrawnFieldMouseDown}
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={onResizeHandleMouseDown}
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={onUserSelectHandleMouseDown}
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>
)
})}
</>
)
}
if (parsingPdf) {
return (
<Box sx={{ width: '100%', textAlign: 'center' }}>
<CircularProgress />
</Box>
)
}
if (!pdfFiles.length) {
return ''
}
return (
<div className={styles.view}>
{pdfFiles.map((pdfFile, pdfFileIndex: number) => {
return (
<>
<div className={styles.fileWrapper}>
{getPdfPages(pdfFile, pdfFileIndex)}
</div>
{pdfFileIndex < pdfFiles.length - 1 && (
<Divider
sx={{
fontSize: '12px',
color: 'rgba(0,0,0,0.15)'
}}
>
File Separator
</Divider>
)}
</>
)
})}
{showDrawToolBox && (
<Box className={styles.drawToolBoxContainer}>
<Box className={styles.drawToolBox}>
{toolbox
.filter((drawTool) => drawTool.active)
.map((drawTool: DrawTool, index: number) => {
return (
<div
key={index}
onClick={() => {
handleToolSelect(drawTool)
}}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}
>
{drawTool.icon}
{drawTool.label}
</div>
)
})}
</Box>
</Box>
)}
</div>
)
}