Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added Form Annotation support #2845

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
eee292a
Adding Form Annotation
axel7083 Sep 8, 2022
e39acab
Adding types
axel7083 Sep 8, 2022
51b4493
Fixing FormField scope issue and adding Unit Test
axel7083 Sep 12, 2022
687aa06
HotFix: Preventing problem in renderFormCombo for accessing node.box
axel7083 Sep 12, 2022
b562369
fix: lint
diegomura Nov 7, 2022
824d05c
Merge remote-tracking branch 'axel7083/master'
runelk Jun 13, 2023
f20153a
Change FormText to TextInput for more consistent naming.
runelk Jun 13, 2023
33ef422
Change FormCombo to Picker for more consistent naming.
runelk Jun 13, 2023
c36d8c9
Fix spelling error.
runelk Jun 13, 2023
24183a0
Merge remote-tracking branch 'runelk/master'
natterstefan Aug 14, 2024
6264795
chore: format code
natterstefan Aug 18, 2024
e6fa36b
test: fixed tests
natterstefan Aug 18, 2024
9f68eaa
fix: fixed types
natterstefan Aug 18, 2024
c6f86f5
fix: fixed issue when using <Form/> multiple times
natterstefan Aug 18, 2024
49322f8
feat: added form example
natterstefan Aug 18, 2024
81b75d2
feat: updated example
natterstefan Aug 18, 2024
8aa7a0a
test: fixed tests
natterstefan Aug 18, 2024
d05e812
feat: updated example
natterstefan Aug 18, 2024
a08b945
feat: added checkbox
natterstefan Aug 19, 2024
c7fb5e2
fix: fixed types
natterstefan Aug 19, 2024
d9a1e84
feat: added multiline example
natterstefan Aug 21, 2024
96e4d7d
feat: added support for checked appearance (Checkbox)
natterstefan Aug 28, 2024
7162e6f
feat: removed Form component, check AcroForm in form components
natterstefan Aug 28, 2024
3dac42b
fixup! fixed obsolete !!this._root.data.AcroForm check
natterstefan Aug 28, 2024
ef4fee3
feat: removed FormPushButton
natterstefan Aug 28, 2024
3e6c660
fixup! removed P.Form
natterstefan Aug 28, 2024
de0c19d
fixup! once more remove leftover code
natterstefan Aug 28, 2024
fd6e5f9
feat: introduced cleanup feature, suggested by @PhilippBloss
natterstefan Aug 28, 2024
43bd666
fix: revert changes in acroform
natterstefan Aug 28, 2024
39cb88a
feat: improved macos appearance
natterstefan Aug 30, 2024
81dc6fa
feat: removed requirement to render TextInput and Checkbox within For…
natterstefan Sep 1, 2024
e1e3726
fix: fixed PDF appearance on macOS (e.g. checked Checkbox)
natterstefan Sep 1, 2024
0868599
feat: applied feedback (types)
natterstefan Dec 5, 2024
2f0c551
Merge remote-tracking branch 'upstream/master' into feat/form-annotation
natterstefan Dec 5, 2024
3409ca1
feat: added example form to next-14 and next-15
natterstefan Dec 6, 2024
1402f9e
test: updated tests and marked two as todo
natterstefan Dec 6, 2024
b6995a3
feat: exported class types
natterstefan Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions packages/examples/src/form/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import {
Document,
Page,
View,
Text,
Checkbox,
FormField,
TextInput,
Picker,
FormList,
} from '@react-pdf/renderer';

const FormPdf = () => (
<Document>
<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<FormField name="user-info" style={{ flexDirection: 'column' }}>
<Text>TextInput</Text>
<TextInput
name="username"
value="foo"
align="center"
style={{ height: '50px' }}
/>

{/* Nested works as well */}
<View>
<Text>TextInput</Text>
<TextInput
name="password"
value="bar"
align="center"
style={{ height: '50px' }}
password
/>
</View>

<Text>Checkbox (not checked)</Text>
<Checkbox name="checkbox-default" style={{ height: '20px' }} />

<Text>Checkbox (checked)</Text>
<Checkbox
name="checkbox-checked"
checked
style={{ height: '20px' }}
/>

<Text>Picker</Text>
<Picker
name="combo"
select={['', 'option 1', 'option 2']}
value=""
defaultValue=""
style={{ height: '20px' }}
/>

<Text>FormList</Text>
<FormList
name="list"
select={['', 'option 1', 'option 2']}
value=""
defaultValue=""
style={{ height: '50px' }}
/>
</FormField>
</View>
</Page>

<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<FormField name="user-details" style={{ flexDirection: 'column' }}>
<Text>TextInput (multiline)</Text>
<TextInput
name="details"
value="hello"
align="center"
multiline
style={{ fontSize: 8, height: '100px' }}
/>
</FormField>
</View>
</Page>

<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<Text>TextInput (no FormField)</Text>
<TextInput
name="textinput-no-formfield"
value="no formfield"
align="center"
style={{ height: '50px' }}
/>

<Text>Checkbox (checked, no FormField)</Text>
<Checkbox
name="checkbox-no-formfield"
checked
style={{ height: '20px' }}
/>
</View>
</Page>
</Document>
);

export default FormPdf;
5 changes: 5 additions & 0 deletions packages/primitives/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const Note = 'NOTE';
export const Path = 'PATH';
export const Rect = 'RECT';
export const Line = 'LINE';
export const FormField = 'FORM_FIELD';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a strong opinion, but would Form be more semantic? For Field I imagine like an input, but this is actually a form wrapper

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @diegomura,

thanks for your response. Let's see what I suggested here in the past: #2845 (comment). 🤔

My initial thought was to align the names with the ones used in pdfkit (here). So TextInput becomes FormText, Picker becomes FormPicker, and so on. But you made a valid point back in 2022 suggesting we should stick to the native web primitives. I got used to the current names while preparing the PR.

Regarding FormField: I chose the same name as pdfkit mainly to align our (react-pdf) and their (pdfkit) docs and keep them similar. I don't know if that's practical.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like FormGroup? Form rather sounds like the single and complete set of inputs, but they rather are a subsection. E.g., you could separate billing address and shipping address into two groups.

Copy link
Author

@natterstefan natterstefan Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @PhilippBloss,

your suggestion makes sense to me, considering what pdfkit states in their docs as well:

Using the formField method you might create a shipping field that is added to the root of the document, an address field that refers to the shipping field as it's parent, and a street Form Annotation that would refer to the address field as it's parent.

What do you think of fieldset(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset):

The <fieldset> HTML element is used to group several controls as well as labels (<label>) within a web form.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good as well. Would give the decision to @diegomura

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would take over the docs, except @natterstefan has already started with those

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would take over the docs, except @natterstefan has already started with those

Hi @PhilippBloss, I haven't taken care of the docs yet. You can take over the docs if you want to or we can share the task. It's fine for me.

Also thanks for the review. I will try to apply your suggestions tomorrow or at least in the next few days.

Thanks for the ping @diegomura.

export const TextInput = 'TEXT_INPUT';
export const Picker = 'PICKER';
export const Checkbox = 'CHECKBOX';
export const FormList = 'FORM_LIST';
export const Stop = 'STOP';
export const Defs = 'DEFS';
export const Image = 'IMAGE';
Expand Down
16 changes: 16 additions & 0 deletions packages/primitives/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ describe('primitives', () => {
expect(primitives.Line).toBeTruthy();
});

test('should export form field', () => {
expect(primitives.FormField).toBeTruthy();
});

test('should export text input', () => {
expect(primitives.TextInput).toBeTruthy();
});

test('should export form list', () => {
expect(primitives.FormList).toBeTruthy();
});

test('should export picker', () => {
expect(primitives.Picker).toBeTruthy();
});

test('should export stop', () => {
expect(primitives.Stop).toBeTruthy();
});
Expand Down
24 changes: 24 additions & 0 deletions packages/render/src/primitives/form/renderCheckbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseCheckboxOptions } from '../../utils/parseFormOptions';

const renderCheckbox = (ctx, node, options) => {
const { top, left, width, height } = node.box || {};

// Element's name
const name = node.props?.name || '';
const formFieldOptions = options.formFields?.at(0);

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

ctx.formCheckbox(
name,
left,
top,
width,
height,
parseCheckboxOptions(ctx, node, formFieldOptions),
);
};

export default renderCheckbox;
18 changes: 18 additions & 0 deletions packages/render/src/primitives/form/renderFormField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const renderFormField = (ctx, node, options) => {
const name = node.props?.name || '';

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

const formField = ctx.formField(name);
const option = options;
if (!option.formFields) option.formFields = [formField];
else option.formFields.push(formField);
};

export const cleanUpFormField = (_ctx, _node, options) => {
options.formFields.pop();
};

export default renderFormField;
23 changes: 23 additions & 0 deletions packages/render/src/primitives/form/renderFormList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { parsePickerAndListFieldOptions } from '../../utils/parseFormOptions';

const renderFormList = (ctx, node) => {
const { top, left, width, height } = node.box || {};

// Element's name
const name = node.props?.name || '';

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

ctx.formList(
name,
left,
top,
width,
height,
parsePickerAndListFieldOptions(node),
);
};

export default renderFormList;
23 changes: 23 additions & 0 deletions packages/render/src/primitives/form/renderPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { parsePickerAndListFieldOptions } from '../../utils/parseFormOptions';

const renderPicker = (ctx, node) => {
const { top, left, width, height } = node.box || {};

// Element's name
const name = node.props?.name || '';

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

ctx.formCombo(
name,
left,
top,
width,
height,
parsePickerAndListFieldOptions(node),
);
};

export default renderPicker;
24 changes: 24 additions & 0 deletions packages/render/src/primitives/form/renderTextInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseTextFieldOptions } from '../../utils/parseFormOptions';

const renderTextInput = (ctx, node, options) => {
const { top, left, width, height } = node.box || {};

// Element's name
const name = node.props?.name || '';
const formFieldOptions = options.formFields?.at(0);

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

ctx.formText(
name,
left,
top,
width,
height,
parseTextFieldOptions(node, formFieldOptions),
);
};

export default renderTextInput;
19 changes: 19 additions & 0 deletions packages/render/src/primitives/renderNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import setLink from '../operations/setLink';
import clipNode from '../operations/clipNode';
import transform from '../operations/transform';
import setDestination from '../operations/setDestination';
import renderTextInput from './form/renderTextInput';
import renderPicker from './form/renderPicker';
import renderFormField, { cleanUpFormField } from './form/renderFormField';
import renderFormList from './form/renderFormList';
import renderCheckbox from './form/renderCheckbox';

const isRecursiveNode = (node) => node.type !== P.Text && node.type !== P.Svg;

Expand All @@ -23,6 +28,7 @@ const renderChildren = (ctx, node, options) => {
}

const children = node.children || [];
// eslint-disable-next-line no-use-before-define
const renderChild = (child) => renderNode(ctx, child, options);

children.forEach(renderChild);
Expand All @@ -34,11 +40,20 @@ const renderFns = {
[P.Text]: renderText,
[P.Note]: renderNote,
[P.Image]: renderImage,
[P.FormField]: renderFormField,
[P.TextInput]: renderTextInput,
[P.Picker]: renderPicker,
[P.Checkbox]: renderCheckbox,
[P.FormList]: renderFormList,
[P.Canvas]: renderCanvas,
[P.Svg]: renderSvg,
[P.Link]: setLink,
};

const cleanUpFns = {
[P.FormField]: cleanUpFormField,
};

const renderNode = (ctx, node, options) => {
const overflowHidden = node.style?.overflow === 'hidden';
const shouldRenderChildren = isRecursiveNode(node);
Expand All @@ -59,6 +74,10 @@ const renderNode = (ctx, node, options) => {

if (shouldRenderChildren) renderChildren(ctx, node, options);

const cleanUpFn = cleanUpFns[node.type];

if (cleanUpFn) cleanUpFn(ctx, node, options);

setDestination(ctx, node);
renderDebug(ctx, node);

Expand Down
Loading