import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { FieldType } from '@prisma/client'; import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react'; import { createPortal } from 'react-dom'; import { Rnd } from 'react-rnd'; import { useSearchParams } from 'react-router'; import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta'; import { useRecipientColors } from '../../lib/recipient-colors'; import { cn } from '../../lib/utils'; import { FieldContent } from './field-content'; import type { TDocumentFlowFormSchema } from './types'; type Field = TDocumentFlowFormSchema['fields'][0]; export type FieldItemProps = { field: Field; fieldClassName?: string; passive?: boolean; disabled?: boolean; minHeight?: number; minWidth?: number; defaultHeight?: number; defaultWidth?: number; onResize?: (_node: HTMLElement) => void; onMove?: (_node: HTMLElement) => void; onRemove?: () => void; onDuplicate?: () => void; onDuplicateAllPages?: () => void; onAdvancedSettings?: () => void; onFocus?: () => void; onBlur?: () => void; onMouseEnter?: () => void; onMouseLeave?: () => void; recipientIndex?: number; hasErrors?: boolean; active?: boolean; onFieldActivate?: () => void; onFieldDeactivate?: () => void; }; /** * The item when editing fields?? */ export const FieldItem = ({ fieldClassName, field, passive, disabled, minHeight, minWidth, defaultHeight, defaultWidth, onResize, onMove, onRemove, onDuplicate, onDuplicateAllPages, onAdvancedSettings, onFocus, onBlur, recipientIndex = 0, hasErrors, active, onFieldActivate, onFieldDeactivate, }: FieldItemProps) => { const { _ } = useLingui(); const [searchParams] = useSearchParams(); const [coords, setCoords] = useState({ pageX: 0, pageY: 0, pageHeight: defaultHeight || 0, pageWidth: defaultWidth || 0, }); const [settingsActive, setSettingsActive] = useState(false); const $el = useRef(null); const $pageBounds = useElementBounds( `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`, ); const signerStyles = useRecipientColors(recipientIndex); const isDevMode = searchParams.get('devmode') === 'true'; const advancedField = [ 'NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT', 'INITIALS', 'EMAIL', 'DATE', 'NAME', ].includes(field.type); const calculateCoords = useCallback(() => { const $page = document.querySelector( `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`, ); if (!$page) { return; } const { height, width } = $page.getBoundingClientRect(); const top = $page.getBoundingClientRect().top + window.scrollY; const left = $page.getBoundingClientRect().left + window.scrollX; // X and Y are percentages of the page's height and width const pageX = (field.pageX / 100) * width + left; const pageY = (field.pageY / 100) * height + top; const pageHeight = (field.pageHeight / 100) * height; const pageWidth = (field.pageWidth / 100) * width; setCoords({ pageX: pageX, pageY: pageY, pageHeight: pageHeight, pageWidth: pageWidth, }); }, [field.pageHeight, field.pageNumber, field.pageWidth, field.pageX, field.pageY]); useEffect(() => { calculateCoords(); }, [calculateCoords]); useEffect(() => { const onResize = () => { calculateCoords(); }; window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); }; }, [calculateCoords]); useEffect(() => { const onClickOutsideOfField = (event: MouseEvent) => { const isOutsideOfField = $el.current && !event.composedPath().includes($el.current); setSettingsActive((active) => { if (active && isOutsideOfField) { return false; } return active; }); if (isOutsideOfField) { setSettingsActive(false); onFieldDeactivate?.(); onBlur?.(); } }; document.body.addEventListener('click', onClickOutsideOfField); return () => { document.body.removeEventListener('click', onClickOutsideOfField); }; }, [onBlur]); const hasFieldMetaValues = ( fieldType: string, fieldMeta: TFieldMetaSchema, parser: typeof ZCheckboxFieldMeta | typeof ZRadioFieldMeta, ) => { if (field.type !== fieldType || !fieldMeta) { return false; } const parsedMeta = parser?.parse(fieldMeta); return parsedMeta && parsedMeta.values && parsedMeta.values.length > 0; }; const checkBoxHasValues = useMemo( () => hasFieldMetaValues('CHECKBOX', field.fieldMeta, ZCheckboxFieldMeta), [field.fieldMeta], ); const radioHasValues = useMemo( () => hasFieldMetaValues('RADIO', field.fieldMeta, ZRadioFieldMeta), [field.fieldMeta], ); const hasCheckedValues = (fieldMeta: TFieldMetaSchema, type: FieldType) => { if (!fieldMeta || (type !== FieldType.RADIO && type !== FieldType.CHECKBOX)) { return false; } if (type === FieldType.RADIO) { const parsed = ZRadioFieldMeta.parse(fieldMeta); return parsed.values?.some((value) => value.checked) ?? false; } if (type === FieldType.CHECKBOX) { const parsed = ZCheckboxFieldMeta.parse(fieldMeta); return parsed.values?.some((value) => value.checked) ?? false; } return false; }; const fieldHasCheckedValues = useMemo( () => hasCheckedValues(field.fieldMeta, field.type), [field.fieldMeta, field.type], ); const fixedSize = checkBoxHasValues || radioHasValues; return createPortal( onFieldActivate?.()} onResizeStart={() => onFieldActivate?.()} onMouseEnter={() => onFocus?.()} onMouseLeave={() => onBlur?.()} enableResizing={!fixedSize} resizeHandleStyles={{ bottom: { bottom: -8, cursor: 'ns-resize' }, top: { top: -8, cursor: 'ns-resize' }, left: { cursor: 'ew-resize' }, right: { cursor: 'ew-resize' }, }} onResizeStop={(_e, _d, ref) => { onFieldDeactivate?.(); onResize?.(ref); }} onDragStop={(_e, d) => { onFieldDeactivate?.(); onMove?.(d.node); }} > {(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) && field.fieldMeta?.label && (
{field.fieldMeta.label}
)}
{ e.stopPropagation(); setSettingsActive((prev) => !prev); onFieldActivate?.(); onFocus?.(); }} ref={$el} data-field-id={field.nativeId} data-field-type={field.type} data-recipient-id={field.recipientId} > {/* On hover, display recipient initials on side of field. */}
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') + (field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
{isDevMode && (
{`x: ${field.pageX.toFixed(2)}, y: ${field.pageY.toFixed(2)}`}
)}
{!disabled && settingsActive && (
{advancedField && ( )}
)}
, document.body, ); };