Files
documenso/packages/lib/universal/field-renderer/field-generic-items.ts
2025-10-27 16:11:10 +11:00

187 lines
4.3 KiB
TypeScript

import Konva from 'konva';
import {
DEFAULT_RECT_BACKGROUND,
RECIPIENT_COLOR_STYLES,
} from '@documenso/ui/lib/recipient-colors';
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, scale } = options;
const { fieldX, fieldY, fieldWidth, fieldHeight } = calculateFieldPosition(
field,
pageWidth,
pageHeight,
);
const fieldGroup: Konva.Group =
pageLayer.findOne(`#${field.renderId}`) ||
new Konva.Group({
id: field.renderId,
name: 'field-group',
});
const maxXPosition = (pageWidth - fieldWidth) * scale;
const maxYPosition = (pageHeight - fieldHeight) * scale;
fieldGroup.setAttrs({
scaleX: 1,
scaleY: 1,
x: fieldX,
y: fieldY,
draggable: editable,
dragBoundFunc: (pos) => {
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>);
return fieldGroup;
};
export const upsertFieldRect = (
field: FieldToRender,
options: RenderFieldElementOptions,
): Konva.Rect => {
const { pageWidth, pageHeight, mode, pageLayer, color } = options;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const fieldRect: Konva.Rect =
pageLayer.findOne(`#${field.renderId}-rect`) ||
new Konva.Rect({
id: `${field.renderId}-rect`,
name: 'field-rect',
});
fieldRect.setAttrs({
width: fieldWidth,
height: fieldHeight,
fill: DEFAULT_RECT_BACKGROUND,
stroke: color ? RECIPIENT_COLOR_STYLES[color].baseRing : '#e5e7eb',
strokeWidth: 2,
cornerRadius: 2,
strokeScaleEnabled: false,
visible: mode !== 'export',
} satisfies Partial<Konva.RectConfig>);
return fieldRect;
};
export const createSpinner = ({
fieldWidth,
fieldHeight,
}: {
fieldWidth: number;
fieldHeight: number;
}) => {
const loadingGroup = new Konva.Group({
name: 'loading-spinner-group',
});
const rect = new Konva.Rect({
x: 4,
y: 4,
width: fieldWidth - 8,
height: fieldHeight - 8,
fill: 'white',
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: spinnerSize,
outerRadius: spinnerSize / 2,
angle: 270,
rotation: 0,
fill: 'rgba(122, 195, 85, 1)',
lineCap: 'round',
});
rect.moveToTop();
spinner.moveToTop();
loadingGroup.add(rect);
loadingGroup.add(spinner);
const anim = new Konva.Animation((frame) => {
spinner.rotate(180 * (frame.timeDiff / 500));
});
anim.start();
return loadingGroup;
};
type CreateFieldHoverInteractionOptions = {
options: RenderFieldElementOptions;
fieldGroup: Konva.Group;
fieldRect: Konva.Rect;
};
/**
* Adds smooth transition-like behavior for hover effects to the field group and rectangle.
*/
export const createFieldHoverInteraction = ({
options,
fieldGroup,
fieldRect,
}: CreateFieldHoverInteractionOptions) => {
const { mode } = options;
if (mode === 'export' || !options.color) {
return;
}
const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover;
fieldGroup.on('mouseover', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: hoverColor,
}).play();
});
fieldGroup.on('mouseout', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: DEFAULT_RECT_BACKGROUND,
}).play();
});
fieldGroup.on('transformstart', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: hoverColor,
}).play();
});
fieldGroup.on('transformend', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: DEFAULT_RECT_BACKGROUND,
}).play();
});
};