import { PDF_VIEWER_CONTENT_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import type { I18n } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { type Envelope, type Field, FieldType } from '@prisma/client'; import { extractLegacyIds } from '../universal/id'; /** * Sort the fields by the Y position on the document. */ export const sortFieldsByPosition = (fields: Field[]): Field[] => { const clonedFields: Field[] = JSON.parse(JSON.stringify(fields)); // Sort by page first, then position on page second. return clonedFields.sort((a, b) => a.page - b.page || Number(a.positionY) - Number(b.positionY)); }; /** * Validate whether all the provided fields are inserted. * * If there are any non-inserted fields it will be highlighted and scrolled into view. * * @returns `true` if all fields are inserted, `false` otherwise. */ export const validateFieldsInserted = (fields: Field[]): boolean => { const fieldCardElements = document.getElementsByClassName('field-card-container'); const pdfContent = document.querySelector(PDF_VIEWER_CONTENT_SELECTOR); const uninsertedFields = sortFieldsByPosition(fields.filter((field) => !field.inserted)); // All fields are inserted — clear the validation signal. if (uninsertedFields.length === 0) { pdfContent?.removeAttribute('data-validate-fields'); return true; } // Attach validate attribute on all fields currently in the DOM. Array.from(fieldCardElements).forEach((element) => { element.setAttribute('data-validate', 'true'); }); // Also set a signal on the PDF viewer container so that field elements that // mount later (e.g. after the virtual list scrolls to a new page) can pick // up the validation state. pdfContent?.setAttribute('data-validate-fields', 'true'); const firstUninsertedField = uninsertedFields[0]; if (firstUninsertedField) { // Try direct element scroll first (works if the field's page is currently rendered). const firstUninsertedFieldElement = document.getElementById(`field-${firstUninsertedField.id}`); if (firstUninsertedFieldElement) { firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else { // Field not in DOM (page virtualized away) — signal the PDF viewer to // scroll to the correct page via the data attribute. if (pdfContent) { pdfContent.setAttribute('data-scroll-to-page', String(firstUninsertedField.page)); } } } return false; }; export const validateFieldsUninserted = (): boolean => { const fieldCardElements = document.getElementsByClassName('react-draggable'); const errorElements: HTMLElement[] = []; Array.from(fieldCardElements).forEach((element) => { const innerDiv = element.querySelector('div'); const hasError = innerDiv?.getAttribute('data-error') === 'true'; if (hasError) { errorElements.push(element as HTMLElement); } else { element.removeAttribute('data-error'); } }); if (errorElements.length > 0) { errorElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); return false; } return errorElements.length === 0; }; export const mapFieldToLegacyField = (field: Field, envelope: Pick) => { const legacyId = extractLegacyIds(envelope); return { ...field, ...legacyId, }; }; export const parseCheckboxCustomText = (customText: string): number[] => { if (!customText) { return []; } return JSON.parse(customText); }; export const toCheckboxCustomText = (checkedValues: number[]): string => { return JSON.stringify(checkedValues); }; export const parseRadioCustomText = (customText: string): number => { return Number(customText); }; export const toRadioCustomText = (value: number): string => { return value.toString(); }; export const getClientSideFieldTranslations = ({ t }: I18n): Record => { return { [FieldType.TEXT]: t(msg`Text`), [FieldType.CHECKBOX]: t(msg`Checkbox`), [FieldType.RADIO]: t(msg`Radio`), [FieldType.DROPDOWN]: t(msg`Select Option`), [FieldType.SIGNATURE]: t(msg`Signature`), [FieldType.FREE_SIGNATURE]: t(msg`Free Signature`), [FieldType.INITIALS]: t(msg`Initials`), [FieldType.NAME]: t(msg`Name`), [FieldType.NUMBER]: t(msg`Number`), [FieldType.DATE]: t(msg`Date`), [FieldType.EMAIL]: t(msg`Email`), }; };