feat(12.1-01): add optional text-edit props and click-to-select interaction

- PdfViewerWrapper: add selectedFieldId?, textFillData?, onFieldSelect?, onFieldValueChange? and forward to PdfViewer
- PdfViewer: add same 4 optional props, forward to FieldPlacer; call onFieldSelect?.(null) on page navigation
- FieldPlacer: extend FieldPlacerProps with 4 new optional props
- DroppableZone: add optional onClick prop for background deselect
- renderFields: text fields show inline input when selected (isSelected), value/label otherwise
- Per-field onClick: text fields call onFieldSelect(id), non-text fields call onFieldSelect(null)
- Cursor updated to 'text' for text field type; boxShadow ring on selected state
This commit is contained in:
Chandler Copeland
2026-03-21 16:22:02 -06:00
parent df02a1e3f7
commit eaf377d97d
3 changed files with 118 additions and 11 deletions

View File

@@ -123,12 +123,14 @@ function DroppableZone({
children,
onZonePointerMove,
onZonePointerUp,
onClick,
}: {
id: string;
containerRef: React.RefObject<HTMLDivElement | null>;
children: React.ReactNode;
onZonePointerMove: (e: React.PointerEvent<HTMLDivElement>) => void;
onZonePointerUp: (e: React.PointerEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}) {
const { setNodeRef } = useDroppable({ id });
@@ -143,6 +145,7 @@ function DroppableZone({
style={{ position: 'relative' }}
onPointerMove={onZonePointerMove}
onPointerUp={onZonePointerUp}
onClick={onClick}
>
{children}
</div>
@@ -156,9 +159,13 @@ interface FieldPlacerProps {
children: React.ReactNode;
readOnly?: boolean;
onFieldsChanged?: () => void;
selectedFieldId?: string | null;
textFillData?: Record<string, string>;
onFieldSelect?: (fieldId: string | null) => void;
onFieldValueChange?: (fieldId: string, value: string) => void;
}
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged }: FieldPlacerProps) {
export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly = false, onFieldsChanged, selectedFieldId, textFillData, onFieldSelect, onFieldValueChange }: FieldPlacerProps) {
const [fields, setFields] = useState<SignatureFieldData[]>([]);
const [isDraggingToken, setIsDraggingToken] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -566,6 +573,8 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
const heightPx = (field.height / pageInfo.originalHeight) * renderedH;
const isMoving = activeDragFieldId === field.id && activeDragType === 'move';
const isSelected = selectedFieldId === field.id;
const currentValue = textFillData?.[field.id] ?? '';
// Per-type color and label
const fieldType = getFieldType(field);
@@ -632,8 +641,10 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
zIndex: 10,
pointerEvents: readOnly ? 'none' : 'all',
boxSizing: 'border-box',
cursor: readOnly ? 'default' : (isMoving ? 'grabbing' : 'grab'),
boxShadow: isMoving ? `0 4px 12px ${fieldColor}59` : undefined,
cursor: readOnly ? 'default' : (isMoving ? 'grabbing' : (fieldType === 'text' ? 'text' : 'grab')),
boxShadow: isSelected && fieldType === 'text'
? `0 0 0 2px ${fieldColor}66${isMoving ? `, 0 4px 12px ${fieldColor}59` : ''}`
: (isMoving ? `0 4px 12px ${fieldColor}59` : undefined),
userSelect: 'none',
touchAction: 'none',
opacity: readOnly ? 0.6 : 1,
@@ -644,9 +655,47 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
if ((e.target as HTMLElement).closest('[data-no-move]')) return;
handleMoveStart(e, field.id);
}}
onClick={(e) => {
if (readOnly) return;
if (getFieldType(field) === 'text') {
e.stopPropagation(); // prevent DroppableZone's deselect handler from firing
onFieldSelect?.(field.id);
} else {
// Non-text field click: deselect any selected text field
onFieldSelect?.(null);
}
}}
>
{fieldType !== 'checkbox' && (
<span style={{ pointerEvents: 'none' }}>{fieldLabel}</span>
{fieldType === 'text' ? (
isSelected ? (
<input
data-no-move
autoFocus
value={currentValue}
onChange={(e) => onFieldValueChange?.(field.id, e.target.value)}
onPointerDown={(e) => e.stopPropagation()}
style={{
flex: 1,
background: 'transparent',
border: 'none',
outline: 'none',
fontSize: '10px',
color: fieldColor,
width: '100%',
cursor: 'text',
padding: 0,
}}
placeholder="Type value..."
/>
) : (
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', pointerEvents: 'none' }}>
{currentValue || fieldLabel}
</span>
)
) : (
fieldType !== 'checkbox' && (
<span style={{ pointerEvents: 'none' }}>{fieldLabel}</span>
)
)}
{!readOnly && (
<button
@@ -728,6 +777,12 @@ export function FieldPlacer({ docId, pageInfo, currentPage, children, readOnly =
containerRef={containerRef}
onZonePointerMove={handleZonePointerMove}
onZonePointerUp={handleZonePointerUp}
onClick={(e) => {
// Deselect if clicking the zone background (not a field box)
if (!(e.target as HTMLElement).closest('[data-field-id]')) {
onFieldSelect?.(null);
}
}}
>
{children}
{renderFields()}

View File

@@ -19,7 +19,23 @@ interface PageInfo {
scale: number;
}
export function PdfViewer({ docId, docStatus, onFieldsChanged }: { docId: string; docStatus?: string; onFieldsChanged?: () => void }) {
export function PdfViewer({
docId,
docStatus,
onFieldsChanged,
selectedFieldId,
textFillData,
onFieldSelect,
onFieldValueChange,
}: {
docId: string;
docStatus?: string;
onFieldsChanged?: () => void;
selectedFieldId?: string | null;
textFillData?: Record<string, string>;
onFieldSelect?: (fieldId: string | null) => void;
onFieldValueChange?: (fieldId: string, value: string) => void;
}) {
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [scale, setScale] = useState(1.0);
@@ -31,7 +47,7 @@ export function PdfViewer({ docId, docStatus, onFieldsChanged }: { docId: string
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-3 text-sm">
<button
onClick={() => setPageNumber(p => Math.max(1, p - 1))}
onClick={() => { setPageNumber(p => Math.max(1, p - 1)); onFieldSelect?.(null); }}
disabled={pageNumber <= 1}
className="px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-100"
>
@@ -39,7 +55,7 @@ export function PdfViewer({ docId, docStatus, onFieldsChanged }: { docId: string
</button>
<span>{pageNumber} / {numPages || '?'}</span>
<button
onClick={() => setPageNumber(p => Math.min(numPages, p + 1))}
onClick={() => { setPageNumber(p => Math.min(numPages, p + 1)); onFieldSelect?.(null); }}
disabled={pageNumber >= numPages}
className="px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-100"
>
@@ -69,7 +85,17 @@ export function PdfViewer({ docId, docStatus, onFieldsChanged }: { docId: string
</div>
{/* PDF canvas wrapped in FieldPlacer for drag-and-drop field placement */}
<FieldPlacer docId={docId} pageInfo={pageInfo} currentPage={pageNumber} readOnly={readOnly} onFieldsChanged={onFieldsChanged}>
<FieldPlacer
docId={docId}
pageInfo={pageInfo}
currentPage={pageNumber}
readOnly={readOnly}
onFieldsChanged={onFieldsChanged}
selectedFieldId={selectedFieldId}
textFillData={textFillData}
onFieldSelect={onFieldSelect}
onFieldValueChange={onFieldValueChange}
>
<Document
file={`/api/documents/${docId}/file`}
onLoadSuccess={({ numPages }) => setNumPages(numPages)}

View File

@@ -3,6 +3,32 @@ import dynamic from 'next/dynamic';
const PdfViewer = dynamic(() => import('./PdfViewer').then(m => m.PdfViewer), { ssr: false });
export function PdfViewerWrapper({ docId, docStatus, onFieldsChanged }: { docId: string; docStatus?: string; onFieldsChanged?: () => void }) {
return <PdfViewer docId={docId} docStatus={docStatus} onFieldsChanged={onFieldsChanged} />;
export function PdfViewerWrapper({
docId,
docStatus,
onFieldsChanged,
selectedFieldId,
textFillData,
onFieldSelect,
onFieldValueChange,
}: {
docId: string;
docStatus?: string;
onFieldsChanged?: () => void;
selectedFieldId?: string | null;
textFillData?: Record<string, string>;
onFieldSelect?: (fieldId: string | null) => void;
onFieldValueChange?: (fieldId: string, value: string) => void;
}) {
return (
<PdfViewer
docId={docId}
docStatus={docStatus}
onFieldsChanged={onFieldsChanged}
selectedFieldId={selectedFieldId}
textFillData={textFillData}
onFieldSelect={onFieldSelect}
onFieldValueChange={onFieldValueChange}
/>
);
}