diff --git a/packages/primitives/src/index.js b/packages/primitives/src/index.js index d1bc5fb5a..f363b81b3 100644 --- a/packages/primitives/src/index.js +++ b/packages/primitives/src/index.js @@ -8,6 +8,12 @@ export const Note = 'NOTE'; export const Path = 'PATH'; export const Rect = 'RECT'; export const Line = 'LINE'; +export const Form = 'FORM'; +export const FormField = 'FORM_FIELD'; +export const FormText = 'FORM_TEXT'; +export const FormPushButton = 'FORM_PUSH_BUTTON'; +export const FormCombo = 'FORM_COMBO'; +export const FormList = 'FORM_LIST'; export const Stop = 'STOP'; export const Defs = 'DEFS'; export const Image = 'IMAGE'; diff --git a/packages/primitives/tests/index.test.js b/packages/primitives/tests/index.test.js index 3d88b90a2..090878fe4 100644 --- a/packages/primitives/tests/index.test.js +++ b/packages/primitives/tests/index.test.js @@ -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(); }); diff --git a/packages/render/src/primitives/form/renderForm.js b/packages/render/src/primitives/form/renderForm.js new file mode 100644 index 000000000..2d3b28d62 --- /dev/null +++ b/packages/render/src/primitives/form/renderForm.js @@ -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; diff --git a/packages/render/src/primitives/form/renderFormCombo.js b/packages/render/src/primitives/form/renderFormCombo.js new file mode 100644 index 000000000..eb712ea33 --- /dev/null +++ b/packages/render/src/primitives/form/renderFormCombo.js @@ -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; diff --git a/packages/render/src/primitives/form/renderFormField.js b/packages/render/src/primitives/form/renderFormField.js new file mode 100644 index 000000000..9bd227d7b --- /dev/null +++ b/packages/render/src/primitives/form/renderFormField.js @@ -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; diff --git a/packages/render/src/primitives/form/renderFormList.js b/packages/render/src/primitives/form/renderFormList.js new file mode 100644 index 000000000..a2048ce76 --- /dev/null +++ b/packages/render/src/primitives/form/renderFormList.js @@ -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; diff --git a/packages/render/src/primitives/form/renderFormPushButton.js b/packages/render/src/primitives/form/renderFormPushButton.js new file mode 100644 index 000000000..618ddf576 --- /dev/null +++ b/packages/render/src/primitives/form/renderFormPushButton.js @@ -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; diff --git a/packages/render/src/primitives/form/renderFormText.js b/packages/render/src/primitives/form/renderFormText.js new file mode 100644 index 000000000..12573e114 --- /dev/null +++ b/packages/render/src/primitives/form/renderFormText.js @@ -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; diff --git a/packages/render/src/primitives/renderNode.js b/packages/render/src/primitives/renderNode.js index c9ab7e48a..8fadd6e3c 100644 --- a/packages/render/src/primitives/renderNode.js +++ b/packages/render/src/primitives/renderNode.js @@ -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; const renderChildren = (ctx, node, options) => { ctx.save(); @@ -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, diff --git a/packages/render/src/utils/parseFormOptions.js b/packages/render/src/utils/parseFormOptions.js new file mode 100644 index 000000000..0b9a7fb05 --- /dev/null +++ b/packages/render/src/utils/parseFormOptions.js @@ -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, +}; diff --git a/packages/render/tests/ctx.js b/packages/render/tests/ctx.js index 461373587..d2103b5fb 100644 --- a/packages/render/tests/ctx.js +++ b/packages/render/tests/ctx.js @@ -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; }; diff --git a/packages/render/tests/primitives/renderForm.test.js b/packages/render/tests/primitives/renderForm.test.js new file mode 100644 index 000000000..2c19bb3a0 --- /dev/null +++ b/packages/render/tests/primitives/renderForm.test.js @@ -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); + }); +}); diff --git a/packages/renderer/index.d.ts b/packages/renderer/index.d.ts index f2578139b..67cfdb1ff 100644 --- a/packages/renderer/index.d.ts +++ b/packages/renderer/index.d.ts @@ -208,6 +208,67 @@ declare namespace ReactPDF { */ class Link extends React.Component {} + /** + * The fundamental component for building a Form. + */ + class Form extends React.Component {} + + 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 {} + + // 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 {} + + interface FormComboAndListProps extends FormCommonProps { + sort?: boolean; + edit?: boolean; + multiSelect?: boolean; + noSpell?: boolean; + select?: string[]; + } + + class FormCombo extends React.Component {} + + class FormList extends React.Component {} + + // see http://pdfkit.org/docs/forms.html#button_field_options + interface FormButtonProps extends FormCommonProps { + label?: string; + } + + class FormPushButton extends React.Component {} + interface NoteProps extends NodeProps { children: string; } @@ -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;