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, resolveFieldOverflowMode, } from '../../types/field-meta'; import { calculateOverflowLayout } from './calculate-overflow-layout'; 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) => { const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options; const { fieldX, fieldY, 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', listening: false, }); // 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. // isLabel tracks whether we're rendering the field type name (like "Text", "Date", "Email") // or a user label — overflow should not apply to these, only to actual content. let isLabel = true; 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) { isLabel = false; 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) { isLabel = false; 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) { isLabel = false; 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; } } const overflowLayout = calculateOverflowLayout({ overflowMode: resolveFieldOverflowMode(fieldMeta), isLabel, textToRender, fontSize: textFontSize, fontFamily: konvaTextFontFamily, lineHeight: textLineHeight, letterSpacing: textLetterSpacing, textAlign, verticalAlign: textVerticalAlign, baseX: textX + DEFAULT_TEXT_X_PADDING, baseY: textY, baseWidth: fieldWidth - DEFAULT_TEXT_X_PADDING * 2, baseHeight: fieldHeight, groupX: fieldX, groupY: fieldY, pageWidth, pageHeight, }); // 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: overflowLayout.x, y: overflowLayout.y, verticalAlign: overflowLayout.verticalAlign, wrap: overflowLayout.wrap, text: textToRender, fontSize: textFontSize, align: overflowLayout.textAlign, lineHeight: textLineHeight, letterSpacing: textLetterSpacing, fontFamily: konvaTextFontFamily, fill: konvaTextFill, width: overflowLayout.width, height: overflowLayout.height, } satisfies Partial); return { fieldText, isLabel, textToRender, textFontSize, textAlign, textVerticalAlign, textLineHeight, textLetterSpacing, }; }; export const renderGenericTextFieldElement = (field: FieldToRender, options: RenderFieldElementOptions) => { const { mode = 'edit', pageLayer, color } = options; const { pageWidth, pageHeight } = options; const fieldMeta = field.fieldMeta as GenericTextFieldTypeMetas | undefined; 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, isLabel, textToRender, textFontSize, textAlign, textVerticalAlign, textLineHeight, textLetterSpacing, } = upsertFieldText(field, options); fieldGroup.add(fieldRect); fieldGroup.add(fieldText); 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; // During active transform, use crop dimensions (field bounds only). fieldText.x(DEFAULT_TEXT_X_PADDING); fieldText.y(0); fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2); fieldText.height(rectHeight); fieldText.wrap('word'); fieldGroup.getLayer()?.batchDraw(); }); fieldGroup.on('transformend', () => { fieldText.scaleX(1); fieldText.scaleY(1); const rectWidth = fieldRect.width(); const rectHeight = fieldRect.height(); // Recalculate overflow layout with new field dimensions. const newOverflowLayout = calculateOverflowLayout({ overflowMode: resolveFieldOverflowMode(fieldMeta), isLabel, textToRender, fontSize: textFontSize, fontFamily: konvaTextFontFamily, lineHeight: textLineHeight, letterSpacing: textLetterSpacing, textAlign, verticalAlign: textVerticalAlign, baseX: DEFAULT_TEXT_X_PADDING, baseY: 0, baseWidth: rectWidth - DEFAULT_TEXT_X_PADDING * 2, baseHeight: rectHeight, groupX: fieldGroup.x(), groupY: fieldGroup.y(), pageWidth, pageHeight, }); fieldText.x(newOverflowLayout.x); fieldText.y(newOverflowLayout.y); fieldText.width(newOverflowLayout.width); fieldText.height(newOverflowLayout.height); fieldText.wrap(newOverflowLayout.wrap); fieldText.verticalAlign(newOverflowLayout.verticalAlign); 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, }; };