Skip to content

Commit 58a39a7

Browse files
committed
Refactor form fill-mode; add listbox & hooks
Split the monolithic FormWidgetFillMode into per-widget fill-mode renderers (Text, Toggle, Combobox, Listbox, PushButton) and wire them into formRenderers. Add a reusable useFormWidgetState hook to centralize form widget state, event subscriptions and field updates. Introduce a ListboxField component and types for listbox fields, and make combobox/listbox annotation renderers use standardFontCssProperties for font handling. Minor UI/behavior tweaks: ComboboxField is rendered invisible (opacity:0) layer-over-widget, ListboxField implements selection/multi-select logic and disabled handling. Remove the old form-widget-fill-mode file and update the property schema to expose form combobox/listbox properties.
1 parent 7d822f2 commit 58a39a7

14 files changed

Lines changed: 369 additions & 163 deletions

File tree

packages/plugin-form/src/shared/components/annotations/form-combobox.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { CSSProperties, MouseEvent, useState } from '@framework';
2-
import { PdfWidgetAnnoObject, PDF_FORM_FIELD_TYPE } from '@embedpdf/models';
2+
import {
3+
PdfWidgetAnnoObject,
4+
PDF_FORM_FIELD_TYPE,
5+
standardFontCssProperties,
6+
} from '@embedpdf/models';
37
import { TrackedAnnotation } from '@embedpdf/plugin-annotation';
48

59
export interface FormComboboxProps {
@@ -47,7 +51,7 @@ export function FormCombobox({ annotation, isSelected, scale, onClick, style }:
4751
flex: 1,
4852
padding: `0 ${4 * scale}px`,
4953
fontSize,
50-
fontFamily: 'Helvetica, Arial, sans-serif',
54+
...standardFontCssProperties(object.fontFamily),
5155
color: object.fontColor ?? '#000000',
5256
whiteSpace: 'nowrap',
5357
overflow: 'hidden',

packages/plugin-form/src/shared/components/annotations/form-listbox.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { CSSProperties, MouseEvent, useState } from '@framework';
2-
import { PdfWidgetAnnoObject, PDF_FORM_FIELD_TYPE } from '@embedpdf/models';
2+
import {
3+
PdfWidgetAnnoObject,
4+
PDF_FORM_FIELD_TYPE,
5+
standardFontCssProperties,
6+
} from '@embedpdf/models';
37
import { TrackedAnnotation } from '@embedpdf/plugin-annotation';
48

59
export interface FormListboxProps {
@@ -49,7 +53,7 @@ export function FormListbox({ annotation, isSelected, scale, onClick, style }: F
4953
padding: `0 ${4 * scale}px`,
5054
fontSize,
5155
lineHeight: `${lineHeight}px`,
52-
fontFamily: 'Helvetica, Arial, sans-serif',
56+
...standardFontCssProperties(object.fontFamily),
5357
color: opt.isSelected ? '#FFFFFF' : (object.fontColor ?? '#000000'),
5458
background: opt.isSelected ? 'rgba(0, 51, 113, 1)' : 'transparent',
5559
whiteSpace: 'nowrap',

packages/plugin-form/src/shared/components/fields/combobox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function ComboboxField(props: ComboboxFieldProps) {
4040
{...selectProps(isMultipleChoice, selectedTexts)}
4141
onChange={handleChange}
4242
onBlur={onBlur}
43-
style={selectStyle}
43+
style={{ ...selectStyle, opacity: 0 }}
4444
>
4545
{options.map((option: PdfWidgetAnnoOption, index) => {
4646
return (
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { PDF_FORM_FIELD_FLAG, standardFontCssProperties } from '@embedpdf/models';
2+
import { CSSProperties, useCallback, useMemo } from '@framework';
3+
4+
import { ListboxFieldProps } from '../types';
5+
6+
export function ListboxField(props: ListboxFieldProps) {
7+
const { annotation, isEditable, onChangeField, onBlur, inputRef, scale } = props;
8+
const field = annotation.field;
9+
10+
const { flag, options } = field;
11+
const isDisabled = !isEditable || !!(flag & PDF_FORM_FIELD_FLAG.READONLY);
12+
const isMultipleChoice = !!(flag & PDF_FORM_FIELD_FLAG.CHOICE_MULTL_SELECT);
13+
14+
const bw = (annotation.strokeWidth ?? 0) * scale;
15+
const fontSize = (annotation.fontSize ?? 12) * scale;
16+
const lineHeight = fontSize * 1.2;
17+
const fontCss = standardFontCssProperties(annotation.fontFamily);
18+
19+
const containerStyle: CSSProperties = useMemo(
20+
() => ({
21+
position: 'absolute',
22+
top: 0,
23+
left: 0,
24+
width: '100%',
25+
height: '100%',
26+
background: annotation.color ?? '#FFFFFF',
27+
borderStyle: 'solid',
28+
borderColor: annotation.strokeColor ?? '#000000',
29+
borderWidth: bw,
30+
boxSizing: 'border-box',
31+
overflow: 'auto',
32+
display: 'flex',
33+
flexDirection: 'column',
34+
outline: 'none',
35+
}),
36+
[annotation.color, annotation.strokeColor, bw],
37+
);
38+
39+
const handleOptionClick = useCallback(
40+
(clickedIndex: number) => {
41+
if (isDisabled) return;
42+
const updatedOptions = options.map((opt, i) => ({
43+
...opt,
44+
isSelected: isMultipleChoice
45+
? i === clickedIndex
46+
? !opt.isSelected
47+
: opt.isSelected
48+
: i === clickedIndex,
49+
}));
50+
onChangeField?.({ ...field, options: updatedOptions });
51+
},
52+
[isDisabled, isMultipleChoice, options, field, onChangeField],
53+
);
54+
55+
return (
56+
<div
57+
ref={inputRef as (el: HTMLDivElement | null) => void}
58+
tabIndex={0}
59+
onBlur={onBlur}
60+
style={containerStyle}
61+
>
62+
{options.map((opt, i) => (
63+
<div
64+
key={i}
65+
onClick={() => handleOptionClick(i)}
66+
style={{
67+
padding: `0 ${4 * scale}px`,
68+
fontSize,
69+
lineHeight: `${lineHeight}px`,
70+
...fontCss,
71+
color: opt.isSelected ? '#FFFFFF' : (annotation.fontColor ?? '#000000'),
72+
background: opt.isSelected ? 'rgba(0, 51, 113, 1)' : 'transparent',
73+
whiteSpace: 'nowrap',
74+
overflow: 'hidden',
75+
textOverflow: 'ellipsis',
76+
cursor: isDisabled ? 'default' : 'pointer',
77+
}}
78+
>
79+
{opt.label}
80+
</div>
81+
))}
82+
</div>
83+
);
84+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { PdfWidgetAnnoObject } from '@embedpdf/models';
2+
import { AnnotationRendererProps } from '@embedpdf/plugin-annotation/@framework';
3+
import { useFormWidgetState } from '../../hooks/use-form-widget-state';
4+
import { RenderWidget } from '../render-widget';
5+
import { ComboboxField } from '../fields/combobox';
6+
import { ComboboxFieldProps } from '../types';
7+
8+
export function ComboboxFillMode(props: AnnotationRendererProps<PdfWidgetAnnoObject>) {
9+
const { annotation, scale, pageIndex, handleChangeField, renderKey, isReadOnly } =
10+
useFormWidgetState(props);
11+
12+
return (
13+
<div
14+
style={{
15+
width: '100%',
16+
height: '100%',
17+
overflow: 'hidden',
18+
cursor: isReadOnly ? 'default' : 'pointer',
19+
pointerEvents: 'auto',
20+
}}
21+
>
22+
<RenderWidget
23+
pageIndex={pageIndex}
24+
annotation={annotation}
25+
scaleFactor={scale}
26+
renderKey={renderKey}
27+
style={{ pointerEvents: 'none' }}
28+
/>
29+
<ComboboxField
30+
annotation={annotation as ComboboxFieldProps['annotation']}
31+
scale={scale}
32+
pageIndex={pageIndex}
33+
isEditable={true}
34+
onChangeField={handleChangeField}
35+
/>
36+
</div>
37+
);
38+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useCallback, useState } from '@framework';
2+
import { PdfWidgetAnnoObject } from '@embedpdf/models';
3+
import { AnnotationRendererProps } from '@embedpdf/plugin-annotation/@framework';
4+
import { useFormWidgetState } from '../../hooks/use-form-widget-state';
5+
import { RenderWidget } from '../render-widget';
6+
import { ListboxField } from '../fields/listbox';
7+
import { ListboxFieldProps } from '../types';
8+
9+
export function ListboxFillMode(props: AnnotationRendererProps<PdfWidgetAnnoObject>) {
10+
const { annotation, scale, pageIndex, handleChangeField, renderKey, isReadOnly } =
11+
useFormWidgetState(props);
12+
const [editing, setEditing] = useState(false);
13+
14+
const handleClick = useCallback(() => {
15+
if (isReadOnly) return;
16+
setEditing(true);
17+
}, [isReadOnly]);
18+
19+
const handleBlur = useCallback(() => {
20+
setEditing(false);
21+
}, []);
22+
23+
return (
24+
<div
25+
onClick={handleClick}
26+
style={{
27+
width: '100%',
28+
height: '100%',
29+
overflow: 'hidden',
30+
cursor: isReadOnly ? 'default' : 'pointer',
31+
pointerEvents: 'auto',
32+
}}
33+
>
34+
<ListboxField
35+
annotation={annotation as ListboxFieldProps['annotation']}
36+
scale={scale}
37+
pageIndex={pageIndex}
38+
isEditable={true}
39+
onChangeField={handleChangeField}
40+
onBlur={handleBlur}
41+
/>
42+
{!editing && (
43+
<RenderWidget
44+
pageIndex={pageIndex}
45+
annotation={annotation}
46+
scaleFactor={scale}
47+
renderKey={renderKey}
48+
style={{ pointerEvents: 'none' }}
49+
/>
50+
)}
51+
</div>
52+
);
53+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { PdfWidgetAnnoObject } from '@embedpdf/models';
2+
import { AnnotationRendererProps } from '@embedpdf/plugin-annotation/@framework';
3+
import { useFormWidgetState } from '../../hooks/use-form-widget-state';
4+
import { RenderWidget } from '../render-widget';
5+
6+
export function PushButtonFillMode(props: AnnotationRendererProps<PdfWidgetAnnoObject>) {
7+
const { annotation, scale, pageIndex, renderKey } = useFormWidgetState(props);
8+
9+
return (
10+
<div
11+
style={{
12+
width: '100%',
13+
height: '100%',
14+
overflow: 'hidden',
15+
pointerEvents: 'auto',
16+
}}
17+
>
18+
<RenderWidget
19+
pageIndex={pageIndex}
20+
annotation={annotation}
21+
scaleFactor={scale}
22+
renderKey={renderKey}
23+
style={{ pointerEvents: 'none' }}
24+
/>
25+
</div>
26+
);
27+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useCallback, useState } from '@framework';
2+
import { PdfWidgetAnnoObject } from '@embedpdf/models';
3+
import { AnnotationRendererProps } from '@embedpdf/plugin-annotation/@framework';
4+
import { useFormWidgetState } from '../../hooks/use-form-widget-state';
5+
import { RenderWidget } from '../render-widget';
6+
import { TextField } from '../fields/text';
7+
import { TextFieldProps } from '../types';
8+
9+
export function TextFillMode(props: AnnotationRendererProps<PdfWidgetAnnoObject>) {
10+
const { annotation, scale, pageIndex, handleChangeField, renderKey, isReadOnly } =
11+
useFormWidgetState(props);
12+
const [editing, setEditing] = useState(false);
13+
14+
const handleClick = useCallback(() => {
15+
if (isReadOnly) return;
16+
setEditing(true);
17+
}, [isReadOnly]);
18+
19+
const handleBlur = useCallback(() => {
20+
setEditing(false);
21+
}, []);
22+
23+
const focusRef = useCallback((el: HTMLElement | null) => {
24+
if (el) el.focus();
25+
}, []);
26+
27+
return (
28+
<div
29+
onClick={handleClick}
30+
style={{
31+
width: '100%',
32+
height: '100%',
33+
overflow: 'hidden',
34+
cursor: isReadOnly ? 'default' : 'pointer',
35+
pointerEvents: 'auto',
36+
}}
37+
>
38+
{!editing && (
39+
<RenderWidget
40+
pageIndex={pageIndex}
41+
annotation={annotation}
42+
scaleFactor={scale}
43+
renderKey={renderKey}
44+
style={{ pointerEvents: 'none' }}
45+
/>
46+
)}
47+
{editing && (
48+
<TextField
49+
annotation={annotation as TextFieldProps['annotation']}
50+
scale={scale}
51+
pageIndex={pageIndex}
52+
isEditable={true}
53+
onChangeField={handleChangeField}
54+
onBlur={handleBlur}
55+
inputRef={focusRef}
56+
/>
57+
)}
58+
</div>
59+
);
60+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useCallback } from '@framework';
2+
import { PdfWidgetAnnoField, PdfWidgetAnnoObject } from '@embedpdf/models';
3+
import { AnnotationRendererProps } from '@embedpdf/plugin-annotation/@framework';
4+
import { useFormWidgetState } from '../../hooks/use-form-widget-state';
5+
import { RenderWidget } from '../render-widget';
6+
7+
export function ToggleFillMode(props: AnnotationRendererProps<PdfWidgetAnnoObject>) {
8+
const { annotation, field, scale, pageIndex, handleChangeField, renderKey, isReadOnly } =
9+
useFormWidgetState(props);
10+
11+
const handleClick = useCallback(() => {
12+
if (isReadOnly) return;
13+
if ('isChecked' in field) {
14+
handleChangeField({ ...field, isChecked: !field.isChecked } as PdfWidgetAnnoField);
15+
}
16+
}, [isReadOnly, field, handleChangeField]);
17+
18+
return (
19+
<div
20+
onClick={handleClick}
21+
style={{
22+
width: '100%',
23+
height: '100%',
24+
overflow: 'hidden',
25+
cursor: isReadOnly ? 'default' : 'pointer',
26+
pointerEvents: 'auto',
27+
}}
28+
>
29+
<RenderWidget
30+
pageIndex={pageIndex}
31+
annotation={annotation}
32+
scaleFactor={scale}
33+
renderKey={renderKey}
34+
style={{ pointerEvents: 'none' }}
35+
/>
36+
</div>
37+
);
38+
}

0 commit comments

Comments
 (0)