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:
@@ -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()}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user