mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 02:32:00 +10:00
fix: merge conflicts
This commit is contained in:
@ -30,3 +30,5 @@ export const symmetricDecrypt = ({ key, data }: SymmetricDecryptOptions) => {
|
||||
|
||||
return chacha.decrypt(dataAsBytes);
|
||||
};
|
||||
|
||||
export { sha256 };
|
||||
|
||||
@ -9,7 +9,7 @@ import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
export const konvaTextFontFamily =
|
||||
'Noto Sans, Noto Sans Japanese, Noto Sans Chinese, Noto Sans Korean, sans-serif';
|
||||
'"Noto Sans", "Noto Sans Japanese", "Noto Sans Chinese", "Noto Sans Korean", sans-serif';
|
||||
export const konvaTextFill = 'black';
|
||||
|
||||
export const upsertFieldGroup = (
|
||||
@ -153,6 +153,11 @@ export const createFieldHoverInteraction = ({
|
||||
const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover;
|
||||
|
||||
fieldGroup.on('mouseover', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
@ -161,6 +166,11 @@ export const createFieldHoverInteraction = ({
|
||||
});
|
||||
|
||||
fieldGroup.on('mouseout', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
@ -169,6 +179,11 @@ export const createFieldHoverInteraction = ({
|
||||
});
|
||||
|
||||
fieldGroup.on('transformstart', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
@ -177,6 +192,11 @@ export const createFieldHoverInteraction = ({
|
||||
});
|
||||
|
||||
fieldGroup.on('transformend', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
|
||||
@ -19,7 +19,7 @@ export type FieldToRender = Pick<
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
||||
};
|
||||
|
||||
export type RenderFieldElementOptions = {
|
||||
|
||||
@ -3,6 +3,7 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TCheckboxFieldMeta } from '../../types/field-meta';
|
||||
import { parseCheckboxCustomText } from '../../utils/fields';
|
||||
import {
|
||||
createFieldHoverInteraction,
|
||||
konvaTextFill,
|
||||
@ -25,7 +26,7 @@ export const renderCheckboxFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -62,16 +63,15 @@ export const renderCheckboxFieldElement = (
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Todo: Envelopes - check sorting more than 10
|
||||
// arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
|
||||
const squares = fieldGroup
|
||||
.find('.checkbox-square')
|
||||
.sort((a, b) => a.id().localeCompare(b.id()));
|
||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
||||
const checkmarks = fieldGroup
|
||||
.find('.checkbox-checkmark')
|
||||
.sort((a, b) => a.id().localeCompare(b.id()));
|
||||
const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
||||
const text = fieldGroup
|
||||
.find('.checkbox-text')
|
||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
||||
|
||||
const groupedItems = squares.map((square, i) => ({
|
||||
squareElement: square,
|
||||
@ -130,7 +130,7 @@ export const renderCheckboxFieldElement = (
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
const checkedValues: number[] = field.customText ? JSON.parse(field.customText) : [];
|
||||
const checkedValues: number[] = field.customText ? parseCheckboxCustomText(field.customText) : [];
|
||||
|
||||
checkboxValues.forEach(({ value, checked }, index) => {
|
||||
const isCheckboxChecked = match(mode)
|
||||
@ -170,7 +170,7 @@ export const renderCheckboxFieldElement = (
|
||||
width: itemSize,
|
||||
height: itemSize,
|
||||
stroke: '#374151',
|
||||
strokeWidth: 2,
|
||||
strokeWidth: 1.5,
|
||||
cornerRadius: 2,
|
||||
fill: 'white',
|
||||
});
|
||||
@ -210,7 +210,9 @@ export const renderCheckboxFieldElement = (
|
||||
fieldGroup.add(text);
|
||||
});
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -50,7 +50,7 @@ export const renderDropdownFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode, translations } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, translations, color } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -74,6 +74,21 @@ export const renderDropdownFieldElement = (
|
||||
|
||||
const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
// Don't show any labels when exporting.
|
||||
if (mode === 'export') {
|
||||
selectedValue = '';
|
||||
}
|
||||
|
||||
// Render the default value if readonly.
|
||||
if (
|
||||
dropdownMeta?.readOnly &&
|
||||
dropdownMeta.defaultValue &&
|
||||
dropdownMeta.values &&
|
||||
dropdownMeta.values.some((value) => value.value === dropdownMeta.defaultValue)
|
||||
) {
|
||||
selectedValue = dropdownMeta.defaultValue;
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
selectedValue = field.customText;
|
||||
}
|
||||
@ -166,7 +181,9 @@ export const renderDropdownFieldElement = (
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -8,13 +8,24 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import type { TFieldMetaSchema } from '../../types/field-meta';
|
||||
import { renderCheckboxFieldElement } from './render-checkbox-field';
|
||||
import { renderDropdownFieldElement } from './render-dropdown-field';
|
||||
import { renderGenericTextFieldElement } from './render-generic-text-field';
|
||||
import { renderRadioFieldElement } from './render-radio-field';
|
||||
import { renderSignatureFieldElement } from './render-signature-field';
|
||||
import { renderTextFieldElement } from './render-text-field';
|
||||
|
||||
export const MIN_FIELD_HEIGHT_PX = 12;
|
||||
export const MIN_FIELD_WIDTH_PX = 36;
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
* @default 'edit'
|
||||
*
|
||||
* - `edit` - The field is rendered in editor page.
|
||||
* - `sign` - The field is rendered for the signing page.
|
||||
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
|
||||
*/
|
||||
export type FieldRenderMode = 'edit' | 'sign' | 'export';
|
||||
|
||||
export type FieldToRender = Pick<
|
||||
Field,
|
||||
'envelopeItemId' | 'recipientId' | 'type' | 'page' | 'customText' | 'inserted' | 'recipientId'
|
||||
@ -25,7 +36,7 @@ export type FieldToRender = Pick<
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
||||
};
|
||||
|
||||
type RenderFieldOptions = {
|
||||
@ -38,16 +49,7 @@ type RenderFieldOptions = {
|
||||
|
||||
translations: Record<FieldType, string> | null;
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
* @default 'edit'
|
||||
*
|
||||
* - `edit` - The field is rendered in edit mode.
|
||||
* - `sign` - The field is rendered in sign mode. No interactive elements.
|
||||
* - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc.
|
||||
*/
|
||||
mode: 'edit' | 'sign' | 'export';
|
||||
mode: FieldRenderMode;
|
||||
|
||||
scale: number;
|
||||
editable?: boolean;
|
||||
@ -75,11 +77,23 @@ export const renderField = ({
|
||||
scale,
|
||||
};
|
||||
|
||||
// If the generic text field element array changes, update the `GenericTextFieldTypeMetas` type
|
||||
return match(field.type)
|
||||
.with(FieldType.TEXT, () => renderTextFieldElement(field, options))
|
||||
.with(
|
||||
FieldType.INITIALS,
|
||||
FieldType.NAME,
|
||||
FieldType.EMAIL,
|
||||
FieldType.DATE,
|
||||
FieldType.TEXT,
|
||||
FieldType.NUMBER,
|
||||
() => renderGenericTextFieldElement(field, options),
|
||||
)
|
||||
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
|
||||
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
|
||||
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
|
||||
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
|
||||
.otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
|
||||
.with(FieldType.FREE_SIGNATURE, () => {
|
||||
throw new Error('Free signature fields are not supported');
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@ -0,0 +1,199 @@
|
||||
import Konva from 'konva';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { GenericTextFieldTypeMetas } from '../../types/field-meta';
|
||||
import {
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||
FIELD_DEFAULT_LETTER_SPACING,
|
||||
FIELD_DEFAULT_LINE_HEIGHT,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
createFieldHoverInteraction,
|
||||
konvaTextFill,
|
||||
konvaTextFontFamily,
|
||||
upsertFieldGroup,
|
||||
upsertFieldRect,
|
||||
} from './field-generic-items';
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
const DEFAULT_TEXT_X_PADDING = 6;
|
||||
|
||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
const fieldMeta = field.fieldMeta as GenericTextFieldTypeMetas | undefined;
|
||||
|
||||
const fieldTypeName = translations?.[field.type] || field.type;
|
||||
|
||||
const fieldText: Konva.Text =
|
||||
pageLayer.findOne(`#${field.renderId}-text`) ||
|
||||
new Konva.Text({
|
||||
id: `${field.renderId}-text`,
|
||||
name: 'field-text',
|
||||
});
|
||||
|
||||
// Calculate text positioning based on alignment
|
||||
const textX = 0;
|
||||
const textY = 0;
|
||||
const textFontSize = fieldMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
// By default, render the field name or label centered
|
||||
let textToRender: string = fieldMeta?.label || fieldTypeName;
|
||||
let textAlign: 'left' | 'center' | 'right' = 'center';
|
||||
let textVerticalAlign: 'top' | 'middle' | 'bottom' = FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
let textLineHeight = FIELD_DEFAULT_LINE_HEIGHT;
|
||||
let textLetterSpacing = FIELD_DEFAULT_LETTER_SPACING;
|
||||
|
||||
// Render default values for text/number if provided for editing mode.
|
||||
if (mode === 'edit' && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||
|
||||
if (value) {
|
||||
textToRender = value;
|
||||
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to blank for export mode since we want to ensure we don't show
|
||||
// any placeholder text or labels unless actually it's inserted.
|
||||
if (mode === 'export') {
|
||||
textToRender = '';
|
||||
}
|
||||
|
||||
// Fallback render readonly fields if prefilled value exists.
|
||||
if (field?.fieldMeta?.readOnly && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||
|
||||
if (value) {
|
||||
textToRender = value;
|
||||
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// Override everything with value if it's inserted.
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
|
||||
textAlign = fieldMeta?.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
|
||||
if (fieldMeta?.type === 'text' || fieldMeta?.type === 'number') {
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Do not use native text padding since it's uniform.
|
||||
// We only want to have padding on the left and right hand sides.
|
||||
fieldText.setAttrs({
|
||||
x: textX + DEFAULT_TEXT_X_PADDING,
|
||||
y: textY,
|
||||
verticalAlign: textVerticalAlign,
|
||||
wrap: 'word',
|
||||
text: textToRender,
|
||||
fontSize: textFontSize,
|
||||
align: textAlign,
|
||||
lineHeight: textLineHeight,
|
||||
letterSpacing: textLetterSpacing,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
width: fieldWidth - DEFAULT_TEXT_X_PADDING * 2,
|
||||
height: fieldHeight,
|
||||
} satisfies Partial<Konva.TextConfig>);
|
||||
|
||||
return fieldText;
|
||||
};
|
||||
|
||||
export const renderGenericTextFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer, color } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
// Clear previous children and listeners to re-render fresh.
|
||||
const fieldGroup = upsertFieldGroup(field, options);
|
||||
fieldGroup.removeChildren();
|
||||
fieldGroup.off('transform');
|
||||
|
||||
// Assign elements to group and any listeners that should only be run on initialization.
|
||||
if (isFirstRender) {
|
||||
pageLayer.add(fieldGroup);
|
||||
}
|
||||
|
||||
// Render the field background and text.
|
||||
const fieldRect = upsertFieldRect(field, options);
|
||||
const fieldText = upsertFieldText(field, options);
|
||||
|
||||
fieldGroup.add(fieldRect);
|
||||
fieldGroup.add(fieldText);
|
||||
|
||||
// This is to keep the text inside the field at the same size
|
||||
// when the field is resized. Without this the text would be stretched.
|
||||
fieldGroup.on('transform', () => {
|
||||
const groupScaleX = fieldGroup.scaleX();
|
||||
const groupScaleY = fieldGroup.scaleY();
|
||||
|
||||
// Adjust text scale so it doesn't change while group is resized.
|
||||
fieldText.scaleX(1 / groupScaleX);
|
||||
fieldText.scaleY(1 / groupScaleY);
|
||||
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
fieldText.height();
|
||||
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Reset the text after transform has ended.
|
||||
fieldGroup.on('transformend', () => {
|
||||
fieldText.scaleX(1);
|
||||
fieldText.scaleY(1);
|
||||
|
||||
const rectWidth = fieldRect.width();
|
||||
const rectHeight = fieldRect.height();
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
fieldText.height();
|
||||
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Handle export mode.
|
||||
if (mode === 'export') {
|
||||
// Hide the rectangle.
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
isFirstRender,
|
||||
};
|
||||
};
|
||||
@ -25,7 +25,7 @@ export const renderRadioFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||
|
||||
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
||||
const radioValues = radioMeta?.values || [];
|
||||
@ -159,7 +159,7 @@ export const renderRadioFieldElement = (
|
||||
y: itemInputY,
|
||||
radius: calculateRadioSize(fontSize) / 2,
|
||||
stroke: '#374151',
|
||||
strokeWidth: 2,
|
||||
strokeWidth: 1.5,
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
@ -195,7 +195,9 @@ export const renderRadioFieldElement = (
|
||||
fieldGroup.add(text);
|
||||
});
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -142,7 +142,7 @@ export const renderSignatureFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer } = options;
|
||||
const { mode = 'edit', pageLayer, color } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
@ -211,7 +211,9 @@ export const renderSignatureFieldElement = (
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -1,187 +0,0 @@
|
||||
import Konva from 'konva';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TTextFieldMeta } from '../../types/field-meta';
|
||||
import {
|
||||
createFieldHoverInteraction,
|
||||
konvaTextFill,
|
||||
konvaTextFontFamily,
|
||||
upsertFieldGroup,
|
||||
upsertFieldRect,
|
||||
} from './field-generic-items';
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
const textMeta = field.fieldMeta as TTextFieldMeta | undefined;
|
||||
|
||||
const fieldTypeName = translations?.[field.type] || field.type;
|
||||
|
||||
const fieldText: Konva.Text =
|
||||
pageLayer.findOne(`#${field.renderId}-text`) ||
|
||||
new Konva.Text({
|
||||
id: `${field.renderId}-text`,
|
||||
name: 'field-text',
|
||||
});
|
||||
|
||||
// Calculate text positioning based on alignment
|
||||
const textX = 0;
|
||||
const textY = 0;
|
||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
|
||||
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
|
||||
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
const textPadding = 10;
|
||||
|
||||
let textToRender: string = fieldTypeName;
|
||||
|
||||
// Handle edit mode.
|
||||
if (mode === 'edit') {
|
||||
textToRender = fieldTypeName;
|
||||
textAlign = 'center';
|
||||
textVerticalAlign = 'middle';
|
||||
|
||||
if (textMeta?.label) {
|
||||
textToRender = textMeta.label;
|
||||
} else if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
if (textMeta.characterLimit) {
|
||||
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sign mode.
|
||||
if (mode === 'sign' || mode === 'export') {
|
||||
textToRender = fieldTypeName;
|
||||
textAlign = 'center';
|
||||
textVerticalAlign = 'middle';
|
||||
|
||||
if (textMeta?.label) {
|
||||
textToRender = textMeta.label;
|
||||
}
|
||||
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
if (textMeta.characterLimit) {
|
||||
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
if (textMeta?.characterLimit) {
|
||||
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldText.setAttrs({
|
||||
x: textX,
|
||||
y: textY,
|
||||
verticalAlign: textVerticalAlign,
|
||||
wrap: 'word',
|
||||
padding: textPadding,
|
||||
text: textToRender,
|
||||
fontSize: textFontSize,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
align: textAlign,
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
} satisfies Partial<Konva.TextConfig>);
|
||||
|
||||
return fieldText;
|
||||
};
|
||||
|
||||
export const renderTextFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
// Clear previous children and listeners to re-render fresh.
|
||||
const fieldGroup = upsertFieldGroup(field, options);
|
||||
fieldGroup.removeChildren();
|
||||
fieldGroup.off('transform');
|
||||
|
||||
// Assign elements to group and any listeners that should only be run on initialization.
|
||||
if (isFirstRender) {
|
||||
pageLayer.add(fieldGroup);
|
||||
}
|
||||
|
||||
// Render the field background and text.
|
||||
const fieldRect = upsertFieldRect(field, options);
|
||||
const fieldText = upsertFieldText(field, options);
|
||||
|
||||
fieldGroup.add(fieldRect);
|
||||
fieldGroup.add(fieldText);
|
||||
|
||||
// This is to keep the text inside the field at the same size
|
||||
// when the field is resized. Without this the text would be stretched.
|
||||
fieldGroup.on('transform', () => {
|
||||
const groupScaleX = fieldGroup.scaleX();
|
||||
const groupScaleY = fieldGroup.scaleY();
|
||||
|
||||
// Adjust text scale so it doesn't change while group is resized.
|
||||
fieldText.scaleX(1 / groupScaleX);
|
||||
fieldText.scaleY(1 / groupScaleY);
|
||||
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
fieldText.height();
|
||||
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Reset the text after transform has ended.
|
||||
fieldGroup.on('transformend', () => {
|
||||
fieldText.scaleX(1);
|
||||
fieldText.scaleY(1);
|
||||
|
||||
const rectWidth = fieldRect.width();
|
||||
const rectHeight = fieldRect.height();
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
fieldText.height();
|
||||
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Handle export mode.
|
||||
if (mode === 'export') {
|
||||
// Hide the rectangle.
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
isFirstRender,
|
||||
};
|
||||
};
|
||||
@ -7,7 +7,13 @@ export type GetFileOptions = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
export const getFile = async ({ type, data }: GetFileOptions) => {
|
||||
/**
|
||||
* KEPT FOR POSTERITY, SHOULD BE REMOVED IN THE FUTURE
|
||||
* DO NOT USE OR I WILL FIRE YOU
|
||||
*
|
||||
* - Lucas, 2025-11-04
|
||||
*/
|
||||
const getFile = async ({ type, data }: GetFileOptions) => {
|
||||
return await match(type)
|
||||
.with(DocumentDataType.BYTES, () => getFileFromBytes(data))
|
||||
.with(DocumentDataType.BYTES_64, () => getFileFromBytes64(data))
|
||||
|
||||
@ -7,6 +7,7 @@ import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { createDocumentData } from '../../server-only/document-data/create-document-data';
|
||||
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
|
||||
import { uploadS3File } from './server-actions';
|
||||
|
||||
type File = {
|
||||
@ -43,6 +44,28 @@ export const putPdfFileServerSide = async (file: File) => {
|
||||
return await createDocumentData({ type, data });
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a pdf file and normalizes it.
|
||||
*/
|
||||
export const putNormalizedPdfFileServerSide = async (file: File) => {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const normalized = await normalizePdf(buffer);
|
||||
|
||||
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
|
||||
|
||||
const documentData = await putFileServerSide({
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalized),
|
||||
});
|
||||
|
||||
return await createDocumentData({
|
||||
type: documentData.type,
|
||||
data: documentData.data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a file to the appropriate storage location.
|
||||
*/
|
||||
|
||||
@ -6,7 +6,7 @@ import { env } from '@documenso/lib/utils/env';
|
||||
import type {
|
||||
TGetPresignedPostUrlResponse,
|
||||
TUploadPdfResponse,
|
||||
} from '@documenso/remix/server/api/files.types';
|
||||
} from '@documenso/remix/server/api/files/files.types';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
|
||||
Reference in New Issue
Block a user