Files
documenso/packages/lib/universal/field-renderer/render-grid-lines.ts
David Nguyen 7f09ba72f4 feat: add envelopes (#2025)
This PR is handles the changes required to support envelopes. The new
envelope editor/signing page will be hidden during release.

The core changes here is to migrate the documents and templates model to
a centralized envelopes model.

Even though Documents and Templates are removed, from the user
perspective they will still exist as we remap envelopes to documents and
templates.
2025-10-14 21:56:36 +11:00

327 lines
8.7 KiB
TypeScript

import Konva from 'konva';
const SNAP_THRESHOLD = 10;
type SnapPoint = {
position: number;
type: 'edge' | 'center';
direction: 'horizontal' | 'vertical';
};
type SnapResult = {
x: number;
y: number;
horizontalGuide?: number;
verticalGuide?: number;
};
type ResizeSnapResult = {
x: number;
y: number;
width: number;
height: number;
horizontalGuides: number[];
verticalGuides: number[];
};
export function initializeSnapGuides(stage: Konva.Stage): Konva.Layer {
// Remove any existing snap guide layers from this stage
const existingSnapLayers = stage.find('.snap-guide-layer');
existingSnapLayers.forEach((layer) => layer.destroy());
const snapGuideLayer = new Konva.Layer({
name: 'snap-guide-layer',
});
stage.add(snapGuideLayer);
return snapGuideLayer;
}
export function calculateSnapPositions(
stage: Konva.Stage,
excludeId?: string,
): { horizontal: SnapPoint[]; vertical: SnapPoint[] } {
const fieldGroups = stage
.find('.field-group')
.filter((node): node is Konva.Group => node instanceof Konva.Group);
const horizontal: SnapPoint[] = [];
const vertical: SnapPoint[] = [];
fieldGroups.forEach((group) => {
if (excludeId && group.id() === excludeId) {
return;
}
const rect = group.getClientRect();
// Vertical snap points (for horizontal alignment)
horizontal.push(
{ position: rect.y, type: 'edge', direction: 'horizontal' },
{ position: rect.y + rect.height / 2, type: 'center', direction: 'horizontal' },
{ position: rect.y + rect.height, type: 'edge', direction: 'horizontal' },
);
// Horizontal snap points (for vertical alignment)
vertical.push(
{ position: rect.x, type: 'edge', direction: 'vertical' },
{ position: rect.x + rect.width / 2, type: 'center', direction: 'vertical' },
{ position: rect.x + rect.width, type: 'edge', direction: 'vertical' },
);
});
return { horizontal, vertical };
}
export function calculateSnapSizes(
stage: Konva.Stage,
excludeId?: string,
): { widths: number[]; heights: number[] } {
const fieldGroups = stage
.find('.field-group')
.filter((node): node is Konva.Group => node instanceof Konva.Group);
const widths: number[] = [];
const heights: number[] = [];
fieldGroups.forEach((group) => {
if (excludeId && group.id() === excludeId) {
return;
}
const rect = group.getClientRect();
widths.push(rect.width);
heights.push(rect.height);
});
return { widths, heights };
}
export function getSnappedPosition(
stage: Konva.Stage,
movingGroup: Konva.Group,
newX: number,
newY: number,
): SnapResult {
const { horizontal, vertical } = calculateSnapPositions(stage, movingGroup.id());
const rect = movingGroup.getClientRect();
let snappedX = newX;
let snappedY = newY;
let horizontalGuide: number | undefined;
let verticalGuide: number | undefined;
// Calculate the moving field's snap points
const movingTop = newY;
const movingBottom = newY + rect.height;
const movingCenterY = newY + rect.height / 2;
const movingLeft = newX;
const movingRight = newX + rect.width;
const movingCenterX = newX + rect.width / 2;
// Check horizontal snapping (Y position)
for (const snapPoint of horizontal) {
const distanceTop = Math.abs(movingTop - snapPoint.position);
const distanceBottom = Math.abs(movingBottom - snapPoint.position);
const distanceCenter = Math.abs(movingCenterY - snapPoint.position);
if (distanceTop <= SNAP_THRESHOLD) {
snappedY = snapPoint.position;
horizontalGuide = snapPoint.position;
break;
} else if (distanceBottom <= SNAP_THRESHOLD) {
snappedY = snapPoint.position - rect.height;
horizontalGuide = snapPoint.position;
break;
} else if (distanceCenter <= SNAP_THRESHOLD) {
snappedY = snapPoint.position - rect.height / 2;
horizontalGuide = snapPoint.position;
break;
}
}
// Check vertical snapping (X position)
for (const snapPoint of vertical) {
const distanceLeft = Math.abs(movingLeft - snapPoint.position);
const distanceRight = Math.abs(movingRight - snapPoint.position);
const distanceCenter = Math.abs(movingCenterX - snapPoint.position);
if (distanceLeft <= SNAP_THRESHOLD) {
snappedX = snapPoint.position;
verticalGuide = snapPoint.position;
break;
} else if (distanceRight <= SNAP_THRESHOLD) {
snappedX = snapPoint.position - rect.width;
verticalGuide = snapPoint.position;
break;
} else if (distanceCenter <= SNAP_THRESHOLD) {
snappedX = snapPoint.position - rect.width / 2;
verticalGuide = snapPoint.position;
break;
}
}
return {
x: snappedX,
y: snappedY,
horizontalGuide,
verticalGuide,
};
}
export function getSnappedResize(
stage: Konva.Stage,
resizingGroup: Konva.Group,
newX: number,
newY: number,
newWidth: number,
newHeight: number,
): ResizeSnapResult {
const { horizontal, vertical } = calculateSnapPositions(stage, resizingGroup.id());
const { widths, heights } = calculateSnapSizes(stage, resizingGroup.id());
const snappedX = newX;
const snappedY = newY;
let snappedWidth = newWidth;
let snappedHeight = newHeight;
const horizontalGuides: number[] = [];
const verticalGuides: number[] = [];
// Snap width to other field widths
for (const width of widths) {
if (Math.abs(newWidth - width) <= SNAP_THRESHOLD) {
snappedWidth = width;
break;
}
}
// Snap height to other field heights
for (const height of heights) {
if (Math.abs(newHeight - height) <= SNAP_THRESHOLD) {
snappedHeight = height;
break;
}
}
// Calculate field edges with new snapped dimensions
const movingTop = snappedY;
const movingBottom = snappedY + snappedHeight;
const movingLeft = snappedX;
const movingRight = snappedX + snappedWidth;
// Snap edges to alignment guides
for (const snapPoint of horizontal) {
if (Math.abs(movingTop - snapPoint.position) <= SNAP_THRESHOLD) {
horizontalGuides.push(snapPoint.position);
} else if (Math.abs(movingBottom - snapPoint.position) <= SNAP_THRESHOLD) {
horizontalGuides.push(snapPoint.position);
}
}
for (const snapPoint of vertical) {
if (Math.abs(movingLeft - snapPoint.position) <= SNAP_THRESHOLD) {
verticalGuides.push(snapPoint.position);
} else if (Math.abs(movingRight - snapPoint.position) <= SNAP_THRESHOLD) {
verticalGuides.push(snapPoint.position);
}
}
return {
x: snappedX,
y: snappedY,
width: snappedWidth,
height: snappedHeight,
horizontalGuides,
verticalGuides,
};
}
export function showSnapGuides(
snapGuideLayer: Konva.Layer,
horizontalGuide?: number,
verticalGuide?: number,
stageWidth?: number,
stageHeight?: number,
): void {
if (!snapGuideLayer) {
return;
}
hideSnapGuides(snapGuideLayer);
if (horizontalGuide !== undefined && stageWidth) {
const horizontalLine = new Konva.Line({
name: 'snap-guide-horizontal',
points: [0, horizontalGuide, stageWidth, horizontalGuide],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
dash: [5, 5],
listening: false,
});
snapGuideLayer.add(horizontalLine);
}
if (verticalGuide !== undefined && stageHeight) {
const verticalLine = new Konva.Line({
name: 'snap-guide-vertical',
points: [verticalGuide, 0, verticalGuide, stageHeight],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
dash: [5, 5],
listening: false,
});
snapGuideLayer.add(verticalLine);
}
snapGuideLayer.batchDraw();
}
export function showMultipleSnapGuides(
snapGuideLayer: Konva.Layer,
horizontalGuides: number[],
verticalGuides: number[],
stageWidth: number,
stageHeight: number,
): void {
if (!snapGuideLayer) {
return;
}
hideSnapGuides(snapGuideLayer);
// Show horizontal guides
horizontalGuides.forEach((guide) => {
const horizontalLine = new Konva.Line({
name: 'snap-guide-horizontal',
points: [0, guide, stageWidth, guide],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
dash: [5, 5],
listening: false,
});
snapGuideLayer.add(horizontalLine);
});
// Show vertical guides
verticalGuides.forEach((guide) => {
const verticalLine = new Konva.Line({
name: 'snap-guide-vertical',
points: [guide, 0, guide, stageHeight],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
dash: [5, 5],
listening: false,
});
snapGuideLayer.add(verticalLine);
});
snapGuideLayer.batchDraw();
}
export function hideSnapGuides(snapGuideLayer: Konva.Layer): void {
if (!snapGuideLayer) {
return;
}
const guides = snapGuideLayer.find('.snap-guide-horizontal, .snap-guide-vertical');
guides.forEach((guide: Konva.Node) => guide.destroy());
snapGuideLayer.batchDraw();
}