'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Caveat } from 'next/font/google'; import { Check, ChevronsUpDown, Info } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { nanoid } from '@documenso/lib/universal/id'; import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from '@documenso/ui/primitives/command'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { TAddFieldsFormSchema } from './add-fields.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, DocumentFlowFormContainerFooter, DocumentFlowFormContainerStep, } from './document-flow-root'; import { FieldItem } from './field-item'; import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types'; const fontCaveat = Caveat({ weight: ['500'], subsets: ['latin'], display: 'swap', variable: '--font-caveat', }); const DEFAULT_HEIGHT_PERCENT = 5; const DEFAULT_WIDTH_PERCENT = 15; const MIN_HEIGHT_PX = 60; const MIN_WIDTH_PX = 200; export type AddFieldsFormProps = { documentFlow: DocumentFlowStep; hideRecipients?: boolean; recipients: Recipient[]; fields: Field[]; numberOfSteps: number; onSubmit: (_data: TAddFieldsFormSchema) => void; }; export const AddFieldsFormPartial = ({ documentFlow, hideRecipients = false, recipients, fields, numberOfSteps, onSubmit, }: AddFieldsFormProps) => { const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement(); const { control, handleSubmit, formState: { isSubmitting }, } = useForm({ defaultValues: { fields: fields.map((field) => ({ nativeId: field.id, formId: `${field.id}-${field.documentId}`, pageNumber: field.page, type: field.type, pageX: Number(field.positionX), pageY: Number(field.positionY), pageWidth: Number(field.width), pageHeight: Number(field.height), signerEmail: recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '', })), }, }); const onFormSubmit = handleSubmit(onSubmit); const { append, remove, update, fields: localFields, } = useFieldArray({ control, name: 'fields', }); const [selectedField, setSelectedField] = useState(null); const [selectedSigner, setSelectedSigner] = useState(null); const [showRecipientsSelector, setShowRecipientsSelector] = useState(false); const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT; const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false); const [coords, setCoords] = useState({ x: 0, y: 0, }); const fieldBounds = useRef({ height: 0, width: 0, }); const onMouseMove = useCallback( (event: MouseEvent) => { setIsFieldWithinBounds( isWithinPageBounds( event, PDF_VIEWER_PAGE_SELECTOR, fieldBounds.current.width, fieldBounds.current.height, ), ); setCoords({ x: event.clientX - fieldBounds.current.width / 2, y: event.clientY - fieldBounds.current.height / 2, }); }, [isWithinPageBounds], ); const onMouseClick = useCallback( (event: MouseEvent) => { if (!selectedField || !selectedSigner) { return; } const $page = getPage(event, PDF_VIEWER_PAGE_SELECTOR); if ( !$page || !isWithinPageBounds( event, PDF_VIEWER_PAGE_SELECTOR, fieldBounds.current.width, fieldBounds.current.height, ) ) { setSelectedField(null); return; } const { top, left, height, width } = getBoundingClientRect($page); const pageNumber = parseInt($page.getAttribute('data-page-number') ?? '1', 10); // Calculate x and y as a percentage of the page width and height let pageX = ((event.pageX - left) / width) * 100; let pageY = ((event.pageY - top) / height) * 100; // Get the bounds as a percentage of the page width and height const fieldPageWidth = (fieldBounds.current.width / width) * 100; const fieldPageHeight = (fieldBounds.current.height / height) * 100; // And center it based on the bounds pageX -= fieldPageWidth / 2; pageY -= fieldPageHeight / 2; append({ formId: nanoid(12), type: selectedField, pageNumber, pageX, pageY, pageWidth: fieldPageWidth, pageHeight: fieldPageHeight, signerEmail: selectedSigner.email, }); setIsFieldWithinBounds(false); setSelectedField(null); }, [append, isWithinPageBounds, selectedField, selectedSigner, getPage], ); const onFieldResize = useCallback( (node: HTMLElement, index: number) => { const field = localFields[index]; const $page = window.document.querySelector( `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`, ); if (!$page) { return; } const { x: pageX, y: pageY, width: pageWidth, height: pageHeight, } = getFieldPosition($page, node); update(index, { ...field, pageX, pageY, pageWidth, pageHeight, }); }, [getFieldPosition, localFields, update], ); const onFieldMove = useCallback( (node: HTMLElement, index: number) => { const field = localFields[index]; const $page = window.document.querySelector( `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`, ); if (!$page) { return; } const { x: pageX, y: pageY } = getFieldPosition($page, node); update(index, { ...field, pageX, pageY, }); }, [getFieldPosition, localFields, update], ); useEffect(() => { if (selectedField) { window.addEventListener('mousemove', onMouseMove); window.addEventListener('click', onMouseClick); } return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('click', onMouseClick); }; }, [onMouseClick, onMouseMove, selectedField]); useEffect(() => { const $page = window.document.querySelector(PDF_VIEWER_PAGE_SELECTOR); if (!$page) { return; } const { height, width } = $page.getBoundingClientRect(); fieldBounds.current = { height: Math.max(height * (DEFAULT_HEIGHT_PERCENT / 100), MIN_HEIGHT_PX), width: Math.max(width * (DEFAULT_WIDTH_PERCENT / 100), MIN_WIDTH_PX), }; }, []); useEffect(() => { setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]); }, [recipients]); return ( <>
{selectedField && ( {FRIENDLY_FIELD_TYPE[selectedField]} )} {localFields.map((field, index) => ( onFieldResize(options, index)} onMove={(options) => onFieldMove(options, index)} onRemove={() => remove(index)} /> ))} {!hideRecipients && ( {recipients.map((recipient, index) => ( { setSelectedSigner(recipient); setShowRecipientsSelector(false); }} > {recipient.sendStatus !== SendStatus.SENT ? ( ) : ( This document has already been sent to this recipient. You can no longer edit this recipient. )} {recipient.name && ( {recipient.name} ({recipient.email}) )} {!recipient.name && ( {recipient.email} )} ))} )}
void onFormSubmit()} /> ); };