mirror of
https://github.com/documenso/documenso
synced 2026-04-21 21:37:18 +00:00
## Description Replace the PDF renderer with an custom image renderer. This allows us to remove the "react-pdf" dependency and allows us to use a virtual list to improve performance.
138 lines
4.3 KiB
TypeScript
138 lines
4.3 KiB
TypeScript
import type { I18n } from '@lingui/core';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { type Envelope, type Field, FieldType } from '@prisma/client';
|
|
|
|
import { PDF_VIEWER_CONTENT_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|
|
|
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<Envelope, 'type' | 'secondaryId'>,
|
|
) => {
|
|
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<FieldType, string> => {
|
|
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`),
|
|
};
|
|
};
|