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: add Form Annotation support #2013

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/primitives/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export const Note = 'NOTE';
export const Path = 'PATH';
export const Rect = 'RECT';
export const Line = 'LINE';
export const Form = 'FORM';
Copy link
Owner

Choose a reason for hiding this comment

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

Is this necessary? From pdfkit docs, initForm only needs to be called once. I tested this code and trying to render 2 forms crashes rendering. Maybe we can call it once globally if we detect any form elements

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.

Is this necessary? I get how you can create hierarchical structures but wonder it it's useful at all to expose this

export const FormText = 'FORM_TEXT';
Copy link
Owner

Choose a reason for hiding this comment

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

What do you think on renaming this to TextInput to match react native primitives?

export const FormPushButton = 'FORM_PUSH_BUTTON';
Copy link
Owner

Choose a reason for hiding this comment

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

What do you think about renaming this to Button to match react native (web) primitives? Not sure either what purpose this element has

export const FormCombo = 'FORM_COMBO';
Copy link
Owner

Choose a reason for hiding this comment

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

What do you think about renaming this to Picker to match react native (web) primitives?

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

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

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

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

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

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

test('should export form push button', () => {
expect(primitives.FormPushButton).toBeTruthy();
});

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

const renderForm = (ctx, node, options) => {
ctx.save();
ctx.initForm();

const children = node.children || [];
children.forEach((child) => renderNode(ctx, child, options))

ctx.restore();
};

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

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

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

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

export default renderFormCombo;
12 changes: 12 additions & 0 deletions packages/render/src/primitives/form/renderFormField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import renderNode from '../renderNode';

const renderFormField = (ctx, node, options) => {
const name = node.props?.name || '';

const formField = ctx.formField(name);

const children = node.children || [];
children.forEach((child) => renderNode(ctx, child, {...options, formField}))
};

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

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

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

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

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

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

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

ctx.formPushButton(name, left, top, width, height, parseButtonFieldOptions(node));
};

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

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

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

if(!options.formField)
throw new Error('The FormText element must be a children of a FormField element.')

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

export default renderFormText;
14 changes: 13 additions & 1 deletion packages/render/src/primitives/renderNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ import setLink from '../operations/setLink';
import clipNode from '../operations/clipNode';
import transform from '../operations/transform';
import setDestination from '../operations/setDestination';
import renderForm from './form/renderForm';
import renderFormField from './form/renderFormField';
import renderFormText from './form/renderFormText';
import renderFormPushButton from './form/renderFormPushButton';
import renderFormCombo from './form/renderFormCombo';
import renderFormList from './form/renderFormList';

const isRecursiveNode = node => node.type !== P.Text && node.type !== P.Svg;
const isRecursiveNode = node => node.type !== P.Text && node.type !== P.Svg && node.type !== P.Form && node.type !== P.FormField;
Copy link
Owner

Choose a reason for hiding this comment

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

Why Form and FormField aren't recursive? This creates dependency import cycles between renderForm and renderFormField and renderNode


const renderChildren = (ctx, node, options) => {
ctx.save();
Expand All @@ -34,6 +40,12 @@ const renderFns = {
[P.Text]: renderText,
[P.Note]: renderNote,
[P.Image]: renderImage,
[P.Form]: renderForm,
[P.FormField]: renderFormField,
[P.FormText]: renderFormText,
[P.FormPushButton]: renderFormPushButton,
[P.FormCombo]: renderFormCombo,
[P.FormList]: renderFormList,
[P.Canvas]: renderCanvas,
[P.Svg]: renderSvg,
[P.Link]: setLink,
Expand Down
59 changes: 59 additions & 0 deletions packages/render/src/utils/parseFormOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const clean = options => {
const opt = { ...options };

// We need to ensure the elements are no present if not true
Object.entries(opt).forEach(pair => {
if (!pair[1]) {
delete opt[pair[0]];
}
});

return opt;
};

const parseCommonFormOptions = node => {
// Common Options
return {
required: node.props?.required || false,
noExport: node.props?.noExport || false,
readOnly: node.props?.readOnly || false,
value: node.props?.value || undefined,
defaultValue: node.props?.defaultValue || undefined,
};
};

const parseTextFieldOptions = (node, formField) => {
return clean({
...parseCommonFormOptions(node),
parent: formField || undefined,
align: node.props?.align || 'left',
multiline: node.props?.multiline || undefined,
password: node.props?.password || false,
noSpell: node.props?.noSpell || false,
format: node.props?.format || undefined,
});
};

const parseComboAndListFieldOptions = node => {
return clean({
...parseCommonFormOptions(node),
sort: node.props?.sort || false,
edit: node.props?.edit || false,
multiSelect: node.props?.multiSelect || false,
noSpell: node.props?.noSpell || false,
select: node.props?.select || [''],
});
};

const parseButtonFieldOptions = node => {
return clean({
...parseCommonFormOptions(node),
label: node.props?.label || '???',
});
};

export {
parseTextFieldOptions,
parseComboAndListFieldOptions,
parseButtonFieldOptions,
};
Comment on lines +55 to +59
Copy link
Owner

Choose a reason for hiding this comment

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

No need to be in an utils file. I think it's better for each helper fn to be closer to their element (button, combo, text, etc)

2 changes: 2 additions & 0 deletions packages/render/tests/ctx.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const createCTX = () => {
instance.lineCap = jest.fn().mockReturnValue(instance);
instance.text = jest.fn().mockReturnValue(instance);
instance.font = jest.fn().mockReturnValue(instance);
instance.formField = jest.fn().mockReturnValue(instance);
instance.formText = jest.fn().mockReturnValue(instance);

return instance;
};
Expand Down
49 changes: 49 additions & 0 deletions packages/render/tests/primitives/renderForm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as P from '@react-pdf/primitives';

import createCTX from '../ctx';
import renderFormField from '../../src/primitives/form/renderFormField';

describe('primitive renderFormField', () => {
test('should render FormField correctly', () => {
const ctx = createCTX();
const args = 'example';
const props = { name: args };
const node = { type: P.FormField, props };

renderFormField(ctx, node);

expect(ctx.formField.mock.calls).toHaveLength(1);
expect(ctx.formField.mock.calls[0]).toHaveLength(1);
expect(ctx.formField.mock.calls[0][0]).toBe(args);
});

test('FormField with one formText direct child', () => {
const ctx = createCTX();
const node = { type: P.FormField, children: [{type: P.FormText}]};

renderFormField(ctx, node);

expect(ctx.formText.mock.calls).toHaveLength(1);
});

test('FormField with one formText indirect child', () => {
const ctx = createCTX();
const node = {
type: P.FormField,
children: [
{
type: P.View,
children: [
{
type: P.FormText
}
]
}
]
};

renderFormField(ctx, node);

expect(ctx.formText.mock.calls).toHaveLength(1);
});
});
67 changes: 67 additions & 0 deletions packages/renderer/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,67 @@ declare namespace ReactPDF {
*/
class Link extends React.Component<LinkProps> {}

/**
* The fundamental component for building a Form.
*/
class Form extends React.Component<NodeProps> {}

interface FormCommonProps extends NodeProps {
name?: string;
required?: boolean;
noExport?: boolean;
readOnly?: boolean;
value ?: number | string;
defaultValue?: number | string;
}

interface FormFieldProps extends NodeProps {
name: string;
}

class FormField extends React.Component<FormFieldProps> {}

// see http://pdfkit.org/docs/forms.html#text_field_formatting
interface TextFieldFormatting {
type: 'date' | 'time' | 'percent' | 'number' | 'zip' | 'zipPlus4' | 'phone' | 'ssn';
param?: string;
nDec?: number;
sepComma?: boolean;
negStyle?: 'MinusBlack' | 'Red' | 'ParensBlack' | 'ParensRed';
currency?: string;
currencyPrepend?: boolean;
}

// see http://pdfkit.org/docs/forms.html#text_field_formatting
interface FormTextProps extends FormCommonProps {
align?: string;
multiline?: boolean;
password?: boolean;
noSpell?: boolean;
format?: TextFieldFormatting
}

class FormText extends React.Component<FormTextProps> {}

interface FormComboAndListProps extends FormCommonProps {
sort?: boolean;
edit?: boolean;
multiSelect?: boolean;
noSpell?: boolean;
select?: string[];
}

class FormCombo extends React.Component<FormComboAndListProps> {}

class FormList extends React.Component<FormComboAndListProps> {}

// see http://pdfkit.org/docs/forms.html#button_field_options
interface FormButtonProps extends FormCommonProps {
label?: string;
}

class FormPushButton extends React.Component<FormButtonProps> {}

interface NoteProps extends NodeProps {
children: string;
}
Expand Down Expand Up @@ -519,6 +580,12 @@ declare const Image: typeof ReactPDF.Image;
declare const Text: typeof ReactPDF.Text;
declare const Canvas: typeof ReactPDF.Canvas;
declare const Link: typeof ReactPDF.Link;
declare const Form: typeof ReactPDF.Form;
declare const FormField: typeof ReactPDF.FormField;
declare const FormText: typeof ReactPDF.FormText;
declare const FormCombo: typeof ReactPDF.FormCombo;
declare const FormList: typeof ReactPDF.FormList;
declare const FormPushButton: typeof ReactPDF.FormPushButton;
declare const Note: typeof ReactPDF.Note;
declare const Svg: typeof ReactPDF.Svg;
declare const Line: typeof ReactPDF.Line;
Expand Down