mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
@ -8,11 +8,15 @@ import {
|
||||
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';
|
||||
export const konvaTextFill = 'black';
|
||||
|
||||
export const upsertFieldGroup = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
): Konva.Group => {
|
||||
const { pageWidth, pageHeight, pageLayer, editable } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, editable, scale } = options;
|
||||
|
||||
const { fieldX, fieldY, fieldWidth, fieldHeight } = calculateFieldPosition(
|
||||
field,
|
||||
@ -27,6 +31,9 @@ export const upsertFieldGroup = (
|
||||
name: 'field-group',
|
||||
});
|
||||
|
||||
const maxXPosition = (pageWidth - fieldWidth) * scale;
|
||||
const maxYPosition = (pageHeight - fieldHeight) * scale;
|
||||
|
||||
fieldGroup.setAttrs({
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
@ -34,8 +41,9 @@ export const upsertFieldGroup = (
|
||||
y: fieldY,
|
||||
draggable: editable,
|
||||
dragBoundFunc: (pos) => {
|
||||
const newX = Math.max(0, Math.min(pageWidth - fieldWidth, pos.x));
|
||||
const newY = Math.max(0, Math.min(pageHeight - fieldHeight, pos.y));
|
||||
const newX = Math.max(0, Math.min(maxXPosition, pos.x));
|
||||
const newY = Math.max(0, Math.min(maxYPosition, pos.y));
|
||||
|
||||
return { x: newX, y: newY };
|
||||
},
|
||||
} satisfies Partial<Konva.GroupConfig>);
|
||||
@ -89,14 +97,18 @@ export const createSpinner = ({
|
||||
width: fieldWidth - 8,
|
||||
height: fieldHeight - 8,
|
||||
fill: 'white',
|
||||
opacity: 1,
|
||||
opacity: 0.8,
|
||||
});
|
||||
|
||||
const maxSpinnerSize = 10;
|
||||
const smallerDimension = Math.min(fieldWidth, fieldHeight);
|
||||
const spinnerSize = Math.min(smallerDimension, maxSpinnerSize);
|
||||
|
||||
const spinner = new Konva.Arc({
|
||||
x: fieldWidth / 2,
|
||||
y: fieldHeight / 2,
|
||||
innerRadius: fieldWidth / 10,
|
||||
outerRadius: fieldHeight / 10,
|
||||
innerRadius: spinnerSize,
|
||||
outerRadius: spinnerSize / 2,
|
||||
angle: 270,
|
||||
rotation: 0,
|
||||
fill: 'rgba(122, 195, 85, 1)',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Signature } from '@prisma/client';
|
||||
import type { FieldType, Signature } from '@prisma/client';
|
||||
import { type Field } from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
|
||||
@ -26,9 +26,11 @@ export type RenderFieldElementOptions = {
|
||||
pageLayer: Konva.Layer;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
mode?: 'edit' | 'sign' | 'export';
|
||||
mode: 'edit' | 'sign' | 'export';
|
||||
editable?: boolean;
|
||||
scale: number;
|
||||
color?: TRecipientColor;
|
||||
translations: Record<FieldType, string> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -107,6 +109,11 @@ type CalculateMultiItemPositionOptions = {
|
||||
*/
|
||||
fieldPadding: number;
|
||||
|
||||
/**
|
||||
* The direction of the items.
|
||||
*/
|
||||
direction: 'horizontal' | 'vertical';
|
||||
|
||||
type: 'checkbox' | 'radio';
|
||||
};
|
||||
|
||||
@ -122,6 +129,7 @@ export const calculateMultiItemPosition = (options: CalculateMultiItemPositionOp
|
||||
itemSize,
|
||||
spacingBetweenItemAndText,
|
||||
fieldPadding,
|
||||
direction,
|
||||
type,
|
||||
} = options;
|
||||
|
||||
@ -130,6 +138,39 @@ export const calculateMultiItemPosition = (options: CalculateMultiItemPositionOp
|
||||
const innerFieldX = fieldPadding;
|
||||
const innerFieldY = fieldPadding;
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
const itemHeight = innerFieldHeight;
|
||||
const itemWidth = innerFieldWidth / itemCount;
|
||||
|
||||
const y = innerFieldY;
|
||||
const x = itemIndex * itemWidth + innerFieldX;
|
||||
|
||||
let itemInputY = y + itemHeight / 2 - itemSize / 2;
|
||||
let itemInputX = x;
|
||||
|
||||
// We need a little different logic to center the radio circle icon.
|
||||
if (type === 'radio') {
|
||||
itemInputX = x + itemSize / 2;
|
||||
itemInputY = y + itemHeight / 2;
|
||||
}
|
||||
|
||||
const textX = x + itemSize + spacingBetweenItemAndText;
|
||||
const textY = y;
|
||||
|
||||
// Multiplied by 2 for extra padding on the right hand side of the text and the next item.
|
||||
const textWidth = itemWidth - itemSize - spacingBetweenItemAndText * 2;
|
||||
const textHeight = itemHeight;
|
||||
|
||||
return {
|
||||
itemInputX,
|
||||
itemInputY,
|
||||
textX,
|
||||
textY,
|
||||
textWidth,
|
||||
textHeight,
|
||||
};
|
||||
}
|
||||
|
||||
const itemHeight = innerFieldHeight / itemCount;
|
||||
|
||||
const y = itemIndex * itemHeight + innerFieldY;
|
||||
@ -137,6 +178,7 @@ export const calculateMultiItemPosition = (options: CalculateMultiItemPositionOp
|
||||
let itemInputY = y + itemHeight / 2 - itemSize / 2;
|
||||
let itemInputX = innerFieldX;
|
||||
|
||||
// We need a little different logic to center the radio circle icon.
|
||||
if (type === 'radio') {
|
||||
itemInputX = innerFieldX + itemSize / 2;
|
||||
itemInputY = y + itemHeight / 2;
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
import Konva from 'konva';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TCheckboxFieldMeta } from '../../types/field-meta';
|
||||
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items';
|
||||
import {
|
||||
konvaTextFill,
|
||||
konvaTextFontFamily,
|
||||
upsertFieldGroup,
|
||||
upsertFieldRect,
|
||||
} from './field-generic-items';
|
||||
import { calculateFieldPosition, calculateMultiItemPosition } from './field-renderer';
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
|
||||
// Do not change any of these values without consulting with the team.
|
||||
const checkboxFieldPadding = 8;
|
||||
const checkboxSize = 16;
|
||||
const spacingBetweenCheckboxAndText = 8;
|
||||
|
||||
const calculateCheckboxSize = (fontSize: number) => {
|
||||
return fontSize;
|
||||
};
|
||||
|
||||
export const renderCheckboxFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
@ -21,134 +30,156 @@ export const renderCheckboxFieldElement = (
|
||||
|
||||
const fieldGroup = upsertFieldGroup(field, options);
|
||||
|
||||
// Clear previous children to re-render fresh
|
||||
// Clear previous children and listeners to re-render fresh.
|
||||
fieldGroup.removeChildren();
|
||||
fieldGroup.off('transform');
|
||||
|
||||
fieldGroup.add(upsertFieldRect(field, options));
|
||||
|
||||
if (isFirstRender) {
|
||||
pageLayer.add(fieldGroup);
|
||||
|
||||
// Handle rescaling items during transforms.
|
||||
fieldGroup.on('transform', () => {
|
||||
const groupScaleX = fieldGroup.scaleX();
|
||||
const groupScaleY = fieldGroup.scaleY();
|
||||
|
||||
const fieldRect = fieldGroup.findOne('.field-rect');
|
||||
|
||||
if (!fieldRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
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()));
|
||||
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()));
|
||||
|
||||
const groupedItems = squares.map((square, i) => ({
|
||||
squareElement: square,
|
||||
checkmarkElement: checkmarks[i],
|
||||
textElement: text[i],
|
||||
}));
|
||||
|
||||
groupedItems.forEach((item, i) => {
|
||||
const { squareElement, checkmarkElement, textElement } = item;
|
||||
|
||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||
calculateMultiItemPosition({
|
||||
fieldWidth: rectWidth,
|
||||
fieldHeight: rectHeight,
|
||||
itemCount: checkboxValues.length,
|
||||
itemIndex: i,
|
||||
itemSize: checkboxSize,
|
||||
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
||||
fieldPadding: checkboxFieldPadding,
|
||||
type: 'checkbox',
|
||||
});
|
||||
|
||||
squareElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
checkmarkElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
textElement.setAttrs({
|
||||
x: textX,
|
||||
y: textY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
});
|
||||
});
|
||||
|
||||
fieldRect.setAttrs({
|
||||
width: rectWidth,
|
||||
height: rectHeight,
|
||||
});
|
||||
|
||||
fieldGroup.scale({
|
||||
x: 1,
|
||||
y: 1,
|
||||
});
|
||||
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
}
|
||||
|
||||
const checkboxMeta: TCheckboxFieldMeta | null = (field.fieldMeta as TCheckboxFieldMeta) || null;
|
||||
const checkboxValues = checkboxMeta?.values || [];
|
||||
|
||||
const fontSize = checkboxMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
if (isFirstRender) {
|
||||
pageLayer.add(fieldGroup);
|
||||
}
|
||||
|
||||
// Handle rescaling items during transforms.
|
||||
fieldGroup.on('transform', () => {
|
||||
const groupScaleX = fieldGroup.scaleX();
|
||||
const groupScaleY = fieldGroup.scaleY();
|
||||
|
||||
const fieldRect = fieldGroup.findOne('.field-rect');
|
||||
|
||||
if (!fieldRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
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()));
|
||||
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()));
|
||||
|
||||
const groupedItems = squares.map((square, i) => ({
|
||||
squareElement: square,
|
||||
checkmarkElement: checkmarks[i],
|
||||
textElement: text[i],
|
||||
}));
|
||||
|
||||
groupedItems.forEach((item, i) => {
|
||||
const { squareElement, checkmarkElement, textElement } = item;
|
||||
|
||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||
calculateMultiItemPosition({
|
||||
fieldWidth: rectWidth,
|
||||
fieldHeight: rectHeight,
|
||||
itemCount: checkboxValues.length,
|
||||
itemIndex: i,
|
||||
itemSize: calculateCheckboxSize(fontSize),
|
||||
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
||||
fieldPadding: checkboxFieldPadding,
|
||||
direction: checkboxMeta?.direction || 'vertical',
|
||||
type: 'checkbox',
|
||||
});
|
||||
|
||||
squareElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
checkmarkElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
});
|
||||
|
||||
textElement.setAttrs({
|
||||
x: textX,
|
||||
y: textY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
});
|
||||
});
|
||||
|
||||
fieldRect.setAttrs({
|
||||
width: rectWidth,
|
||||
height: rectHeight,
|
||||
});
|
||||
|
||||
fieldGroup.scale({
|
||||
x: 1,
|
||||
y: 1,
|
||||
});
|
||||
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
const checkedValues: number[] = field.customText ? JSON.parse(field.customText) : [];
|
||||
|
||||
checkboxValues.forEach(({ id, value, checked }, index) => {
|
||||
const isCheckboxChecked = match(mode)
|
||||
.with('edit', () => checked)
|
||||
.with('sign', () => checkedValues.includes(index))
|
||||
.with('export', () => {
|
||||
// If it's read-only, check the originally checked state.
|
||||
if (checkboxMeta.readOnly) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
return checkedValues.includes(index);
|
||||
})
|
||||
.exhaustive();
|
||||
|
||||
console.log('wtf?');
|
||||
|
||||
const itemSize = calculateCheckboxSize(fontSize);
|
||||
|
||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||
calculateMultiItemPosition({
|
||||
fieldWidth,
|
||||
fieldHeight,
|
||||
itemCount: checkboxValues.length,
|
||||
itemIndex: index,
|
||||
itemSize: checkboxSize,
|
||||
itemSize,
|
||||
spacingBetweenItemAndText: spacingBetweenCheckboxAndText,
|
||||
fieldPadding: checkboxFieldPadding,
|
||||
direction: checkboxMeta?.direction || 'vertical',
|
||||
type: 'checkbox',
|
||||
});
|
||||
|
||||
const square = new Konva.Rect({
|
||||
internalCheckboxId: id,
|
||||
internalCheckboxValue: value,
|
||||
internalCheckboxIndex: index,
|
||||
id: `checkbox-square-${index}`,
|
||||
name: 'checkbox-square',
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
width: checkboxSize,
|
||||
height: checkboxSize,
|
||||
width: itemSize,
|
||||
height: itemSize,
|
||||
stroke: '#374151',
|
||||
strokeWidth: 2,
|
||||
cornerRadius: 2,
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
const checkboxScale = itemSize / 16;
|
||||
|
||||
const checkmark = new Konva.Line({
|
||||
internalCheckboxId: id,
|
||||
internalCheckboxValue: value,
|
||||
internalCheckboxIndex: index,
|
||||
id: `checkbox-checkmark-${index}`,
|
||||
name: 'checkbox-checkmark',
|
||||
x: itemInputX,
|
||||
@ -156,12 +187,12 @@ export const renderCheckboxFieldElement = (
|
||||
strokeWidth: 2,
|
||||
stroke: '#111827',
|
||||
points: [3, 8, 7, 12, 13, 4],
|
||||
visible: checked,
|
||||
scale: { x: checkboxScale, y: checkboxScale },
|
||||
visible: isCheckboxChecked,
|
||||
});
|
||||
|
||||
const text = new Konva.Text({
|
||||
internalCheckboxId: id,
|
||||
internalCheckboxValue: value,
|
||||
internalCheckboxIndex: index,
|
||||
id: `checkbox-text-${index}`,
|
||||
name: 'checkbox-text',
|
||||
x: textX,
|
||||
@ -169,10 +200,10 @@ export const renderCheckboxFieldElement = (
|
||||
text: value,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
fontSize: DEFAULT_STANDARD_FONT_SIZE,
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fontSize,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
verticalAlign: 'middle',
|
||||
fill: '#111827', // Todo: Envelopes - Sort colours
|
||||
});
|
||||
|
||||
fieldGroup.add(square);
|
||||
|
||||
@ -2,7 +2,12 @@ import Konva from 'konva';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TDropdownFieldMeta } from '../../types/field-meta';
|
||||
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items';
|
||||
import {
|
||||
konvaTextFill,
|
||||
konvaTextFontFamily,
|
||||
upsertFieldGroup,
|
||||
upsertFieldRect,
|
||||
} from './field-generic-items';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
|
||||
@ -26,7 +31,7 @@ const calculateDropdownPosition = (options: CalculateDropdownPositionOptions) =>
|
||||
const textY = fieldPadding;
|
||||
|
||||
const arrowX = fieldWidth - arrowSize - fieldPadding;
|
||||
const arrowY = fieldHeight / 2;
|
||||
const arrowY = fieldHeight / 2 - arrowSize / 4;
|
||||
|
||||
return {
|
||||
arrowX,
|
||||
@ -111,6 +116,8 @@ export const renderDropdownFieldElement = (
|
||||
const dropdownMeta: TDropdownFieldMeta | null = (field.fieldMeta as TDropdownFieldMeta) || null;
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
// Todo: Envelopes - Translations
|
||||
let selectedValue = 'Select Option';
|
||||
|
||||
@ -132,9 +139,9 @@ export const renderDropdownFieldElement = (
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
text: selectedValue,
|
||||
fontSize: DEFAULT_STANDARD_FONT_SIZE,
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fill: '#111827',
|
||||
fontSize,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
verticalAlign: 'middle',
|
||||
});
|
||||
|
||||
|
||||
@ -36,6 +36,8 @@ type RenderFieldOptions = {
|
||||
|
||||
color?: TRecipientColor;
|
||||
|
||||
translations: Record<FieldType, string> | null;
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
@ -47,15 +49,18 @@ type RenderFieldOptions = {
|
||||
*/
|
||||
mode: 'edit' | 'sign' | 'export';
|
||||
|
||||
scale: number;
|
||||
editable?: boolean;
|
||||
};
|
||||
|
||||
export const renderField = ({
|
||||
field,
|
||||
translations,
|
||||
pageLayer,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
mode,
|
||||
scale,
|
||||
editable,
|
||||
color,
|
||||
}: RenderFieldOptions) => {
|
||||
@ -63,9 +68,11 @@ export const renderField = ({
|
||||
pageLayer,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
translations,
|
||||
mode,
|
||||
color,
|
||||
editable,
|
||||
scale,
|
||||
};
|
||||
|
||||
return match(field.type)
|
||||
@ -74,5 +81,5 @@ export const renderField = ({
|
||||
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
|
||||
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
|
||||
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
|
||||
.otherwise(() => renderTextFieldElement(field, options)); // Todo
|
||||
.otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
|
||||
};
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
import Konva from 'konva';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TRadioFieldMeta } from '../../types/field-meta';
|
||||
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items';
|
||||
import {
|
||||
konvaTextFill,
|
||||
konvaTextFontFamily,
|
||||
upsertFieldGroup,
|
||||
upsertFieldRect,
|
||||
} from './field-generic-items';
|
||||
import { calculateFieldPosition, calculateMultiItemPosition } from './field-renderer';
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
|
||||
// Do not change any of these values without consulting with the team.
|
||||
const radioFieldPadding = 8;
|
||||
const radioSize = 16;
|
||||
const spacingBetweenRadioAndText = 8;
|
||||
|
||||
const calculateRadioSize = (fontSize: number) => {
|
||||
return fontSize;
|
||||
};
|
||||
|
||||
export const renderRadioFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
@ -26,110 +35,129 @@ export const renderRadioFieldElement = (
|
||||
|
||||
fieldGroup.add(upsertFieldRect(field, options));
|
||||
|
||||
if (isFirstRender) {
|
||||
pageLayer.add(fieldGroup);
|
||||
|
||||
// Handle rescaling items during transforms.
|
||||
fieldGroup.on('transform', () => {
|
||||
const groupScaleX = fieldGroup.scaleX();
|
||||
const groupScaleY = fieldGroup.scaleY();
|
||||
|
||||
const fieldRect = fieldGroup.findOne('.field-rect');
|
||||
|
||||
if (!fieldRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
const circles = fieldGroup.find('.radio-circle').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
const checkmarks = fieldGroup.find('.radio-dot').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
const text = fieldGroup.find('.radio-text').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
|
||||
const groupedItems = circles.map((circle, i) => ({
|
||||
circleElement: circle,
|
||||
checkmarkElement: checkmarks[i],
|
||||
textElement: text[i],
|
||||
}));
|
||||
|
||||
groupedItems.forEach((item, i) => {
|
||||
const { circleElement, checkmarkElement, textElement } = item;
|
||||
|
||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||
calculateMultiItemPosition({
|
||||
fieldWidth: rectWidth,
|
||||
fieldHeight: rectHeight,
|
||||
itemCount: radioValues.length,
|
||||
itemIndex: i,
|
||||
itemSize: radioSize,
|
||||
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
||||
fieldPadding: radioFieldPadding,
|
||||
type: 'radio',
|
||||
});
|
||||
|
||||
circleElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
checkmarkElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
textElement.setAttrs({
|
||||
x: textX,
|
||||
y: textY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
});
|
||||
});
|
||||
|
||||
fieldRect.width(rectWidth);
|
||||
fieldRect.height(rectHeight);
|
||||
|
||||
fieldGroup.scale({
|
||||
x: 1,
|
||||
y: 1,
|
||||
});
|
||||
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
}
|
||||
|
||||
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
||||
const radioValues = radioMeta?.values || [];
|
||||
|
||||
const fontSize = radioMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
if (isFirstRender) {
|
||||
pageLayer.add(fieldGroup);
|
||||
}
|
||||
|
||||
fieldGroup.off('transform');
|
||||
|
||||
// Handle rescaling items during transforms.
|
||||
fieldGroup.on('transform', () => {
|
||||
const groupScaleX = fieldGroup.scaleX();
|
||||
const groupScaleY = fieldGroup.scaleY();
|
||||
|
||||
const fieldRect = fieldGroup.findOne('.field-rect');
|
||||
|
||||
if (!fieldRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
const circles = fieldGroup.find('.radio-circle').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
const checkmarks = fieldGroup.find('.radio-dot').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
const text = fieldGroup.find('.radio-text').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
|
||||
const groupedItems = circles.map((circle, i) => ({
|
||||
circleElement: circle,
|
||||
checkmarkElement: checkmarks[i],
|
||||
textElement: text[i],
|
||||
}));
|
||||
|
||||
groupedItems.forEach((item, i) => {
|
||||
const { circleElement, checkmarkElement, textElement } = item;
|
||||
|
||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||
calculateMultiItemPosition({
|
||||
fieldWidth: rectWidth,
|
||||
fieldHeight: rectHeight,
|
||||
itemCount: radioValues.length,
|
||||
itemIndex: i,
|
||||
itemSize: calculateRadioSize(fontSize),
|
||||
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
||||
fieldPadding: radioFieldPadding,
|
||||
type: 'radio',
|
||||
direction: radioMeta?.direction || 'vertical',
|
||||
});
|
||||
|
||||
circleElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
checkmarkElement.setAttrs({
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
|
||||
textElement.setAttrs({
|
||||
x: textX,
|
||||
y: textY,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
});
|
||||
});
|
||||
|
||||
fieldRect.width(rectWidth);
|
||||
fieldRect.height(rectHeight);
|
||||
|
||||
fieldGroup.scale({
|
||||
x: 1,
|
||||
y: 1,
|
||||
});
|
||||
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
radioValues.forEach(({ value, checked }, index) => {
|
||||
const isRadioValueChecked = match(mode)
|
||||
.with('edit', () => checked)
|
||||
.with('sign', () => index.toString() === field.customText)
|
||||
.with('export', () => {
|
||||
// If it's read-only, check the originally checked state.
|
||||
if (radioMeta.readOnly) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
return index.toString() === field.customText;
|
||||
})
|
||||
.exhaustive();
|
||||
|
||||
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
|
||||
calculateMultiItemPosition({
|
||||
fieldWidth,
|
||||
fieldHeight,
|
||||
itemCount: radioValues.length,
|
||||
itemIndex: index,
|
||||
itemSize: radioSize,
|
||||
itemSize: calculateRadioSize(fontSize),
|
||||
spacingBetweenItemAndText: spacingBetweenRadioAndText,
|
||||
fieldPadding: radioFieldPadding,
|
||||
type: 'radio',
|
||||
direction: radioMeta?.direction || 'vertical',
|
||||
});
|
||||
|
||||
// Circle which represents the radio button.
|
||||
const circle = new Konva.Circle({
|
||||
internalRadioValue: value,
|
||||
internalRadioIndex: index,
|
||||
id: `radio-circle-${index}`,
|
||||
name: 'radio-circle',
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
radius: radioSize / 2,
|
||||
radius: calculateRadioSize(fontSize) / 2,
|
||||
stroke: '#374151',
|
||||
strokeWidth: 2,
|
||||
fill: 'white',
|
||||
@ -137,20 +165,18 @@ export const renderRadioFieldElement = (
|
||||
|
||||
// Dot which represents the selected state.
|
||||
const dot = new Konva.Circle({
|
||||
internalRadioValue: value,
|
||||
internalRadioIndex: index,
|
||||
id: `radio-dot-${index}`,
|
||||
name: 'radio-dot',
|
||||
x: itemInputX,
|
||||
y: itemInputY,
|
||||
radius: radioSize / 4,
|
||||
radius: calculateRadioSize(fontSize) / 4,
|
||||
fill: '#111827',
|
||||
// Todo: Envelopes
|
||||
visible: value === field.customText,
|
||||
// visible: checked,
|
||||
visible: isRadioValueChecked,
|
||||
});
|
||||
|
||||
const text = new Konva.Text({
|
||||
internalRadioValue: value,
|
||||
internalRadioIndex: index,
|
||||
id: `radio-text-${index}`,
|
||||
name: 'radio-text',
|
||||
x: textX,
|
||||
@ -158,10 +184,10 @@ export const renderRadioFieldElement = (
|
||||
text: value,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
fontSize: DEFAULT_STANDARD_FONT_SIZE,
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fontSize,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
verticalAlign: 'middle',
|
||||
fill: '#111827', // Todo: Envelopes - Sort colours
|
||||
});
|
||||
|
||||
fieldGroup.add(circle);
|
||||
|
||||
@ -5,58 +5,74 @@ import {
|
||||
RECIPIENT_COLOR_STYLES,
|
||||
} from '@documenso/ui/lib/recipient-colors';
|
||||
|
||||
import {
|
||||
DEFAULT_HANDWRITING_FONT_SIZE,
|
||||
DEFAULT_STANDARD_FONT_SIZE,
|
||||
MIN_HANDWRITING_FONT_SIZE,
|
||||
} from '../../constants/pdf';
|
||||
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../../constants/pdf';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
|
||||
const minFontSize = MIN_HANDWRITING_FONT_SIZE;
|
||||
const maxFontSize = DEFAULT_HANDWRITING_FONT_SIZE;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SkiaImage: any = undefined;
|
||||
|
||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer } = options;
|
||||
void (async () => {
|
||||
if (typeof window === 'undefined') {
|
||||
const mod = await import('skia-canvas');
|
||||
SkiaImage = mod.Image;
|
||||
}
|
||||
})();
|
||||
|
||||
console.log({
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
});
|
||||
const getImageDimensions = (img: HTMLImageElement, fieldWidth: number, fieldHeight: number) => {
|
||||
let imageWidth = img.width;
|
||||
let imageHeight = img.height;
|
||||
|
||||
const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1);
|
||||
|
||||
imageWidth = imageWidth * scalingFactor;
|
||||
imageHeight = imageHeight * scalingFactor;
|
||||
|
||||
const imageX = (fieldWidth - imageWidth) / 2;
|
||||
const imageY = (fieldHeight - imageHeight) / 2;
|
||||
|
||||
return {
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
x: imageX,
|
||||
y: imageY,
|
||||
};
|
||||
};
|
||||
|
||||
const createFieldSignature = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
): Konva.Text | Konva.Image => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', translations } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
const fontSize = field.fieldMeta?.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE;
|
||||
|
||||
const fieldText: Konva.Text =
|
||||
pageLayer.findOne(`#${field.renderId}-text`) ||
|
||||
new Konva.Text({
|
||||
id: `${field.renderId}-text`,
|
||||
name: 'field-text',
|
||||
});
|
||||
const fieldText = new Konva.Text({
|
||||
id: `${field.renderId}-text`,
|
||||
name: 'field-text',
|
||||
});
|
||||
|
||||
const fieldTypeName = translations?.[field.type] || field.type;
|
||||
|
||||
// Calculate text positioning based on alignment
|
||||
const textX = 0;
|
||||
const textY = 0;
|
||||
const textAlign = 'center';
|
||||
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
|
||||
let textFontSize = DEFAULT_STANDARD_FONT_SIZE;
|
||||
const textPadding = 10;
|
||||
|
||||
let textToRender: string = field.type;
|
||||
let textToRender: string = fieldTypeName;
|
||||
|
||||
const signature = field.signature;
|
||||
|
||||
// Handle edit mode.
|
||||
if (mode === 'edit') {
|
||||
textToRender = field.type; // Todo: Envelope - Need translations
|
||||
textToRender = fieldTypeName;
|
||||
}
|
||||
|
||||
// Handle sign mode.
|
||||
if (mode === 'sign' || mode === 'export') {
|
||||
textToRender = field.type; // Todo: Envelope - Need translations
|
||||
textFontSize = DEFAULT_STANDARD_FONT_SIZE;
|
||||
textVerticalAlign = 'middle';
|
||||
textToRender = fieldTypeName;
|
||||
|
||||
if (field.inserted && !signature) {
|
||||
throw new AppError('MISSING_SIGNATURE');
|
||||
@ -65,20 +81,57 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
if (signature?.typedSignature) {
|
||||
textToRender = signature.typedSignature;
|
||||
}
|
||||
|
||||
if (signature?.signatureImageAsBase64) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Create a new HTML Image element
|
||||
const img = new Image();
|
||||
|
||||
const image = new Konva.Image({
|
||||
image: img,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
});
|
||||
|
||||
img.onload = () => {
|
||||
image.setAttrs({
|
||||
...getImageDimensions(img, fieldWidth, fieldHeight),
|
||||
});
|
||||
};
|
||||
|
||||
img.src = signature.signatureImageAsBase64;
|
||||
|
||||
return image;
|
||||
} else {
|
||||
// Node.js with skia-canvas
|
||||
if (!SkiaImage) {
|
||||
throw new Error('Skia image not found');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const img = new SkiaImage(signature?.signatureImageAsBase64) as unknown as HTMLImageElement;
|
||||
|
||||
const image = new Konva.Image({
|
||||
image: img,
|
||||
...getImageDimensions(img, fieldWidth, fieldHeight),
|
||||
});
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldText.setAttrs({
|
||||
x: textX,
|
||||
y: textY,
|
||||
verticalAlign: textVerticalAlign,
|
||||
wrap: 'word',
|
||||
padding: textPadding,
|
||||
|
||||
verticalAlign: 'middle',
|
||||
wrap: 'char',
|
||||
text: textToRender,
|
||||
|
||||
fontSize: textFontSize,
|
||||
fontFamily: 'Caveat, Inter', // Todo: Envelopes - Fix all fonts for sans
|
||||
align: textAlign,
|
||||
fontSize,
|
||||
fontFamily: 'Caveat, sans-serif',
|
||||
align: 'center',
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
} satisfies Partial<Konva.TextConfig>);
|
||||
@ -96,78 +149,63 @@ export const renderSignatureFieldElement = (
|
||||
|
||||
const fieldGroup = upsertFieldGroup(field, options);
|
||||
|
||||
// ABOVE IS GENERIC, EXTRACT IT.
|
||||
|
||||
// Render the field background and text.
|
||||
const fieldRect = upsertFieldRect(field, options);
|
||||
const fieldText = upsertFieldText(field, options);
|
||||
// Clear previous children and listeners to re-render fresh.
|
||||
fieldGroup.removeChildren();
|
||||
fieldGroup.off('transform');
|
||||
|
||||
// Assign elements to group and any listeners that should only be run on initialization.
|
||||
if (isFirstRender) {
|
||||
fieldGroup.add(fieldRect);
|
||||
fieldGroup.add(fieldText);
|
||||
pageLayer.add(fieldGroup);
|
||||
|
||||
// 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 group position and clipping
|
||||
// fieldGroup.clipFunc(function (ctx) {
|
||||
// ctx.rect(0, 0, rectWidth, rectHeight);
|
||||
// });
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
console.log({
|
||||
rectWidth,
|
||||
});
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
// textInsideField.getTextHeight(); // This forces recalculation
|
||||
fieldText.height(); // This forces recalculation
|
||||
|
||||
// fieldGroup.draw();
|
||||
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 group position and clipping
|
||||
// fieldGroup.clipFunc(function (ctx) {
|
||||
// ctx.rect(0, 0, rectWidth, rectHeight);
|
||||
// });
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
// textInsideField.getTextHeight(); // This forces recalculation
|
||||
fieldText.height(); // This forces recalculation
|
||||
|
||||
// fieldGroup.draw();
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
}
|
||||
|
||||
// Render the field background and text.
|
||||
const fieldRect = upsertFieldRect(field, options);
|
||||
const fieldSignature = createFieldSignature(field, options);
|
||||
|
||||
fieldGroup.add(fieldRect);
|
||||
fieldGroup.add(fieldSignature);
|
||||
|
||||
// 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.
|
||||
fieldSignature.scaleX(1 / groupScaleX);
|
||||
fieldSignature.scaleY(1 / groupScaleY);
|
||||
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Update text dimensions
|
||||
fieldSignature.width(rectWidth);
|
||||
fieldSignature.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
fieldSignature.height();
|
||||
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Reset the text after transform has ended.
|
||||
fieldGroup.on('transformend', () => {
|
||||
fieldSignature.scaleX(1);
|
||||
fieldSignature.scaleY(1);
|
||||
|
||||
const rectWidth = fieldRect.width();
|
||||
const rectHeight = fieldRect.height();
|
||||
|
||||
// Update text dimensions
|
||||
fieldSignature.width(rectWidth); // Account for padding
|
||||
fieldSignature.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
fieldSignature.height();
|
||||
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Handle export mode.
|
||||
if (mode === 'export') {
|
||||
// Hide the rectangle.
|
||||
|
||||
@ -7,12 +7,19 @@ import {
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TTextFieldMeta } from '../../types/field-meta';
|
||||
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items';
|
||||
import {
|
||||
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 } = options;
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||
|
||||
const fieldTypeName = translations?.[field.type] || field.type;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -30,24 +37,21 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
const textY = 0;
|
||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
|
||||
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
|
||||
let textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
const textPadding = 10;
|
||||
|
||||
let textToRender: string = field.type;
|
||||
let textToRender: string = fieldTypeName;
|
||||
|
||||
// Handle edit mode.
|
||||
if (mode === 'edit') {
|
||||
textToRender = field.type; // Todo: Envelope - Need translations
|
||||
textToRender = fieldTypeName;
|
||||
textAlign = 'center';
|
||||
textFontSize = DEFAULT_STANDARD_FONT_SIZE;
|
||||
textVerticalAlign = 'middle';
|
||||
|
||||
if (textMeta?.label) {
|
||||
textToRender = textMeta.label;
|
||||
textFontSize = textMeta.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
} else if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
textFontSize = textMeta.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
@ -59,19 +63,16 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
|
||||
// Handle sign mode.
|
||||
if (mode === 'sign' || mode === 'export') {
|
||||
textToRender = field.type; // Todo: Envelope - Need translations
|
||||
textToRender = fieldTypeName;
|
||||
textAlign = 'center';
|
||||
textFontSize = DEFAULT_STANDARD_FONT_SIZE;
|
||||
textVerticalAlign = 'middle';
|
||||
|
||||
if (textMeta?.label) {
|
||||
textToRender = textMeta.label;
|
||||
textFontSize = textMeta.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
}
|
||||
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
textFontSize = textMeta.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
@ -82,7 +83,6 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
@ -98,11 +98,10 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
verticalAlign: textVerticalAlign,
|
||||
wrap: 'word',
|
||||
padding: textPadding,
|
||||
|
||||
text: textToRender,
|
||||
|
||||
fontSize: textFontSize,
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
align: textAlign,
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
@ -121,77 +120,62 @@ export const renderTextFieldElement = (
|
||||
|
||||
const fieldGroup = upsertFieldGroup(field, options);
|
||||
|
||||
// ABOVE IS GENERIC, EXTRACT IT.
|
||||
// Clear previous children and listeners to re-render fresh.
|
||||
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);
|
||||
|
||||
// Assign elements to group and any listeners that should only be run on initialization.
|
||||
if (isFirstRender) {
|
||||
fieldGroup.add(fieldRect);
|
||||
fieldGroup.add(fieldText);
|
||||
pageLayer.add(fieldGroup);
|
||||
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();
|
||||
// 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);
|
||||
// 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;
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// // Update text group position and clipping
|
||||
// fieldGroup.clipFunc(function (ctx) {
|
||||
// ctx.rect(0, 0, rectWidth, rectHeight);
|
||||
// });
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
// Force Konva to recalculate text layout
|
||||
fieldText.height();
|
||||
|
||||
console.log({
|
||||
rectWidth,
|
||||
});
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
// textInsideField.getTextHeight(); // This forces recalculation
|
||||
fieldText.height(); // This forces recalculation
|
||||
// Reset the text after transform has ended.
|
||||
fieldGroup.on('transformend', () => {
|
||||
fieldText.scaleX(1);
|
||||
fieldText.scaleY(1);
|
||||
|
||||
// fieldGroup.draw();
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
const rectWidth = fieldRect.width();
|
||||
const rectHeight = fieldRect.height();
|
||||
|
||||
// Reset the text after transform has ended.
|
||||
fieldGroup.on('transformend', () => {
|
||||
fieldText.scaleX(1);
|
||||
fieldText.scaleY(1);
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
const rectWidth = fieldRect.width();
|
||||
const rectHeight = fieldRect.height();
|
||||
// Force Konva to recalculate text layout
|
||||
fieldText.height();
|
||||
|
||||
// // Update text group position and clipping
|
||||
// fieldGroup.clipFunc(function (ctx) {
|
||||
// ctx.rect(0, 0, rectWidth, rectHeight);
|
||||
// });
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
// textInsideField.getTextHeight(); // This forces recalculation
|
||||
fieldText.height(); // This forces recalculation
|
||||
|
||||
// fieldGroup.draw();
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
}
|
||||
fieldGroup.getLayer()?.batchDraw();
|
||||
});
|
||||
|
||||
// Handle export mode.
|
||||
if (mode === 'export') {
|
||||
|
||||
Reference in New Issue
Block a user