Skip to content

Commit 8cd872f

Browse files
authored
feat: added Form Annotation support (#2845)
1 parent e5968c6 commit 8cd872f

File tree

14 files changed

+693
-0
lines changed

14 files changed

+693
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use client';
2+
3+
import dynamic from 'next/dynamic';
4+
import {
5+
Document,
6+
Page,
7+
View,
8+
Text,
9+
Checkbox,
10+
FormField,
11+
TextInput,
12+
Select,
13+
List,
14+
} from '@react-pdf/renderer';
15+
16+
const PDFViewer = dynamic(
17+
() => import('@react-pdf/renderer').then((mod) => mod.PDFViewer),
18+
{
19+
ssr: false,
20+
loading: () => <p>Loading...</p>,
21+
},
22+
);
23+
24+
export default function Form() {
25+
const doc = (
26+
<Document>
27+
<Page>
28+
<View
29+
style={{
30+
backgroundColor: 'rgba(182,28,28,0.62)',
31+
width: '30%',
32+
height: '100%',
33+
}}
34+
>
35+
<FormField name="user-info" style={{ flexDirection: 'column' }}>
36+
<Text>TextInput</Text>
37+
<TextInput
38+
name="username"
39+
value="foo"
40+
align="center"
41+
style={{ height: '50px' }}
42+
/>
43+
44+
{/* Nested works as well */}
45+
<View>
46+
<Text>TextInput</Text>
47+
<TextInput
48+
name="password"
49+
value="bar"
50+
align="center"
51+
style={{ height: '50px' }}
52+
password
53+
/>
54+
</View>
55+
56+
<Text>Checkbox (not checked)</Text>
57+
<Checkbox name="checkbox-default" style={{ height: '20px' }} />
58+
59+
<Text>Checkbox (checked)</Text>
60+
<Checkbox
61+
name="checkbox-checked"
62+
checked
63+
style={{ height: '20px' }}
64+
/>
65+
66+
<Text>Select</Text>
67+
<Select
68+
name="combo"
69+
select={['', 'option 1', 'option 2']}
70+
value=""
71+
defaultValue=""
72+
style={{ height: '20px' }}
73+
/>
74+
75+
<Text>List</Text>
76+
<List
77+
name="list"
78+
select={['', 'option 1', 'option 2']}
79+
value=""
80+
defaultValue=""
81+
style={{ height: '50px' }}
82+
/>
83+
</FormField>
84+
</View>
85+
</Page>
86+
87+
<Page>
88+
<View
89+
style={{
90+
backgroundColor: 'rgba(182,28,28,0.62)',
91+
width: '30%',
92+
height: '100%',
93+
}}
94+
>
95+
<FormField name="user-details" style={{ flexDirection: 'column' }}>
96+
<Text>TextInput (multiline)</Text>
97+
<TextInput
98+
name="details"
99+
value="hello"
100+
align="center"
101+
multiline
102+
style={{ fontSize: 8, height: '100px' }}
103+
/>
104+
</FormField>
105+
</View>
106+
</Page>
107+
108+
<Page>
109+
<View
110+
style={{
111+
backgroundColor: 'rgba(182,28,28,0.62)',
112+
width: '30%',
113+
height: '100%',
114+
}}
115+
>
116+
<Text>TextInput (no FormField)</Text>
117+
<TextInput
118+
name="textinput-no-formfield"
119+
value="no formfield"
120+
align="center"
121+
style={{ height: '50px' }}
122+
/>
123+
124+
<Text>Checkbox (checked, no FormField)</Text>
125+
<Checkbox
126+
name="checkbox-no-formfield"
127+
checked
128+
style={{ height: '20px' }}
129+
/>
130+
</View>
131+
</Page>
132+
</Document>
133+
);
134+
135+
return <PDFViewer className="w-full h-svh">{doc}</PDFViewer>;
136+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use client';
2+
3+
import dynamic from 'next/dynamic';
4+
import {
5+
Document,
6+
Page,
7+
View,
8+
Text,
9+
Checkbox,
10+
FormField,
11+
TextInput,
12+
Select,
13+
List,
14+
} from '@react-pdf/renderer';
15+
16+
const PDFViewer = dynamic(
17+
() => import('@react-pdf/renderer').then((mod) => mod.PDFViewer),
18+
{
19+
ssr: false,
20+
loading: () => <p>Loading...</p>,
21+
},
22+
);
23+
24+
export default function Form() {
25+
const doc = (
26+
<Document>
27+
<Page>
28+
<View
29+
style={{
30+
backgroundColor: 'rgba(182,28,28,0.62)',
31+
width: '30%',
32+
height: '100%',
33+
}}
34+
>
35+
<FormField name="user-info" style={{ flexDirection: 'column' }}>
36+
<Text>TextInput</Text>
37+
<TextInput
38+
name="username"
39+
value="foo"
40+
align="center"
41+
style={{ height: '50px' }}
42+
/>
43+
44+
{/* Nested works as well */}
45+
<View>
46+
<Text>TextInput</Text>
47+
<TextInput
48+
name="password"
49+
value="bar"
50+
align="center"
51+
style={{ height: '50px' }}
52+
password
53+
/>
54+
</View>
55+
56+
<Text>Checkbox (not checked)</Text>
57+
<Checkbox name="checkbox-default" style={{ height: '20px' }} />
58+
59+
<Text>Checkbox (checked)</Text>
60+
<Checkbox
61+
name="checkbox-checked"
62+
checked
63+
style={{ height: '20px' }}
64+
/>
65+
66+
<Text>Select</Text>
67+
<Select
68+
name="combo"
69+
select={['', 'option 1', 'option 2']}
70+
value=""
71+
defaultValue=""
72+
style={{ height: '20px' }}
73+
/>
74+
75+
<Text>List</Text>
76+
<List
77+
name="list"
78+
select={['', 'option 1', 'option 2']}
79+
value=""
80+
defaultValue=""
81+
style={{ height: '50px' }}
82+
/>
83+
</FormField>
84+
</View>
85+
</Page>
86+
87+
<Page>
88+
<View
89+
style={{
90+
backgroundColor: 'rgba(182,28,28,0.62)',
91+
width: '30%',
92+
height: '100%',
93+
}}
94+
>
95+
<FormField name="user-details" style={{ flexDirection: 'column' }}>
96+
<Text>TextInput (multiline)</Text>
97+
<TextInput
98+
name="details"
99+
value="hello"
100+
align="center"
101+
multiline
102+
style={{ fontSize: 8, height: '100px' }}
103+
/>
104+
</FormField>
105+
</View>
106+
</Page>
107+
108+
<Page>
109+
<View
110+
style={{
111+
backgroundColor: 'rgba(182,28,28,0.62)',
112+
width: '30%',
113+
height: '100%',
114+
}}
115+
>
116+
<Text>TextInput (no FormField)</Text>
117+
<TextInput
118+
name="textinput-no-formfield"
119+
value="no formfield"
120+
align="center"
121+
style={{ height: '50px' }}
122+
/>
123+
124+
<Text>Checkbox (checked, no FormField)</Text>
125+
<Checkbox
126+
name="checkbox-no-formfield"
127+
checked
128+
style={{ height: '20px' }}
129+
/>
130+
</View>
131+
</Page>
132+
</Document>
133+
);
134+
135+
return <PDFViewer className="w-full h-svh">{doc}</PDFViewer>;
136+
}

packages/primitives/src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export const Note = 'NOTE';
88
export const Path = 'PATH';
99
export const Rect = 'RECT';
1010
export const Line = 'LINE';
11+
export const FormField = 'FORM_FIELD';
12+
export const TextInput = 'TEXT_INPUT';
13+
export const Select = 'SELECT';
14+
export const Checkbox = 'CHECKBOX';
15+
export const List = 'LIST';
1116
export const Stop = 'STOP';
1217
export const Defs = 'DEFS';
1318
export const Image = 'IMAGE';

packages/primitives/tests/index.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ describe('primitives', () => {
3939
expect(primitives.Line).toBeTruthy();
4040
});
4141

42+
test('should export form field', () => {
43+
expect(primitives.FormField).toBeTruthy();
44+
});
45+
46+
test('should export text input', () => {
47+
expect(primitives.TextInput).toBeTruthy();
48+
});
49+
50+
test('should export form list', () => {
51+
expect(primitives.List).toBeTruthy();
52+
});
53+
54+
test('should export select', () => {
55+
expect(primitives.Select).toBeTruthy();
56+
});
57+
4258
test('should export stop', () => {
4359
expect(primitives.Stop).toBeTruthy();
4460
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { parseCheckboxOptions } from '../../utils/parseFormOptions';
2+
3+
const renderCheckbox = (ctx, node, options = {}) => {
4+
const { top, left, width, height } = node.box || {};
5+
6+
// Element's name
7+
const name = node.props?.name || '';
8+
const formFieldOptions = options.formFields?.at(0);
9+
10+
if (!ctx._root.data.AcroForm) {
11+
ctx.initForm();
12+
}
13+
14+
ctx.formCheckbox(
15+
name,
16+
left,
17+
top,
18+
width,
19+
height,
20+
parseCheckboxOptions(ctx, node, formFieldOptions),
21+
);
22+
};
23+
24+
export default renderCheckbox;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const renderFormField = (ctx, node, options = {}) => {
2+
const name = node.props?.name || '';
3+
4+
if (!ctx._root.data.AcroForm) {
5+
ctx.initForm();
6+
}
7+
8+
const formField = ctx.formField(name);
9+
const option = options;
10+
if (!option.formFields) option.formFields = [formField];
11+
else option.formFields.push(formField);
12+
};
13+
14+
export const cleanUpFormField = (_ctx, _node, options) => {
15+
options.formFields.pop();
16+
};
17+
18+
export default renderFormField;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { parseSelectAndListFieldOptions } from '../../utils/parseFormOptions';
2+
3+
const renderList = (ctx, node) => {
4+
const { top, left, width, height } = node.box || {};
5+
6+
// Element's name
7+
const name = node.props?.name || '';
8+
9+
if (!ctx._root.data.AcroForm) {
10+
ctx.initForm();
11+
}
12+
13+
ctx.formList(
14+
name,
15+
left,
16+
top,
17+
width,
18+
height,
19+
parseSelectAndListFieldOptions(node),
20+
);
21+
};
22+
23+
export default renderList;

0 commit comments

Comments
 (0)