mirror of
https://github.com/documenso/documenso.git
synced 2026-07-13 22:37:24 +10:00
fix: virtualisation issues
This commit is contained in:
@@ -178,8 +178,6 @@ export const ConfigureFieldsView = ({
|
||||
name: 'fields',
|
||||
});
|
||||
|
||||
const highestPageNumber = Math.max(...localFields.map((field) => field.pageNumber));
|
||||
|
||||
const onFieldCopy = useCallback(
|
||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||
@@ -556,9 +554,7 @@ export const ConfigureFieldsView = ({
|
||||
scrollParentRef="window"
|
||||
/>
|
||||
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||
>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{localFields.map((field, index) => {
|
||||
const recipientIndex = recipients.findIndex((r) => r.id === field.recipientId);
|
||||
|
||||
|
||||
@@ -101,8 +101,6 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
localFields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
|
||||
|
||||
const hasSignatureField = localFields.some((field) => isSignatureFieldType(field.type));
|
||||
|
||||
const signatureValid = !hasSignatureField || (signature && signature.trim() !== '');
|
||||
@@ -479,9 +477,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
|
||||
>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
|
||||
@@ -50,10 +50,8 @@ export const EmbedDocumentFields = ({
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: EmbedDocumentFieldsProps) => {
|
||||
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||
|
||||
return (
|
||||
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
|
||||
@@ -106,8 +106,6 @@ export const EmbedSignDocumentV1ClientPage = ({
|
||||
fields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
@@ -492,9 +490,7 @@ export const EmbedSignDocumentV1ClientPage = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
|
||||
>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
|
||||
@@ -95,8 +95,6 @@ export const MultiSignDocumentSigningView = ({
|
||||
[],
|
||||
];
|
||||
|
||||
const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page));
|
||||
|
||||
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
|
||||
|
||||
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
|
||||
@@ -370,9 +368,7 @@ export const MultiSignDocumentSigningView = ({
|
||||
</div>
|
||||
|
||||
{hasDocumentLoaded && (
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPendingPageNumber}"]`}
|
||||
>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip
|
||||
key={pendingFields[0].id}
|
||||
|
||||
@@ -82,8 +82,6 @@ export const DirectTemplateSigningForm = ({
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const highestPageNumber = Math.max(...localFields.map((field) => field.page));
|
||||
|
||||
const fieldsRequiringValidation = useMemo(() => {
|
||||
return localFields.filter((field) => isFieldUnsignedAndRequired(field));
|
||||
}, [localFields]);
|
||||
@@ -250,9 +248,7 @@ export const DirectTemplateSigningForm = ({
|
||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||
>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{validateUninsertedFields && uninsertedFields[0] && (
|
||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
|
||||
+1
-5
@@ -162,8 +162,6 @@ export const DocumentSigningPageViewV1 = ({
|
||||
: undefined;
|
||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||
|
||||
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||
|
||||
const pendingFields = fieldsRequiringValidation.filter((field) => !field.inserted);
|
||||
const hasPendingFields = pendingFields.length > 0;
|
||||
|
||||
@@ -401,9 +399,7 @@ export const DocumentSigningPageViewV1 = ({
|
||||
<DocumentSigningAutoSign recipient={recipient} fields={fields} />
|
||||
)}
|
||||
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||
>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields
|
||||
.filter(
|
||||
(field) =>
|
||||
|
||||
+8
@@ -71,6 +71,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
|
||||
if (fieldTooltip) {
|
||||
fieldTooltip.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
} else {
|
||||
// Tooltip not in DOM (page virtualized away) — signal the PDF viewer
|
||||
// to scroll to the correct page via the data attribute.
|
||||
const pdfContent = document.querySelector('[data-pdf-content]');
|
||||
|
||||
if (pdfContent) {
|
||||
pdfContent.setAttribute('data-scroll-to-page', String(nextField.page));
|
||||
}
|
||||
}
|
||||
},
|
||||
isEnvelopeItemSwitch ? 150 : 50,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
|
||||
: elementOrSelector;
|
||||
|
||||
if (!$el) {
|
||||
throw new Error('Element not found');
|
||||
return { top: 0, left: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
if (withScroll) {
|
||||
|
||||
@@ -57,23 +57,48 @@ export const useFieldPageCoords = (
|
||||
};
|
||||
}, [calculateCoords]);
|
||||
|
||||
// Watch for the page element to appear in the DOM (e.g. after a virtual list
|
||||
// scroll) and recalculate coords. Also attach a ResizeObserver once the page
|
||||
// element exists.
|
||||
useEffect(() => {
|
||||
const $page = document.querySelector<HTMLElement>(
|
||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
|
||||
);
|
||||
const pageSelector = `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`;
|
||||
|
||||
if (!$page) {
|
||||
return;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const attachResizeObserver = ($page: HTMLElement) => {
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
calculateCoords();
|
||||
});
|
||||
resizeObserver.observe($page);
|
||||
};
|
||||
|
||||
// Try to attach immediately if the page already exists.
|
||||
const existingPage = document.querySelector<HTMLElement>(pageSelector);
|
||||
|
||||
if (existingPage) {
|
||||
attachResizeObserver(existingPage);
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
calculateCoords();
|
||||
// Watch for DOM mutations to detect when the page element appears (e.g.
|
||||
// after the virtual list scrolls to a new page and renders it).
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
const $page = document.querySelector<HTMLElement>(pageSelector);
|
||||
|
||||
if ($page) {
|
||||
calculateCoords();
|
||||
attachResizeObserver($page);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe($page);
|
||||
mutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, [calculateCoords, field.page]);
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
|
||||
/**
|
||||
* Returns whether the PDF page element for the given page number is currently
|
||||
* present in the DOM. With virtual list rendering only pages near the viewport
|
||||
* are mounted, so this hook lets consumers skip rendering when their page is
|
||||
* virtualised away.
|
||||
*/
|
||||
export const useIsPageInDom = (pageNumber: number) => {
|
||||
const [isPageInDom, setIsPageInDom] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const selector = `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${pageNumber}"]`;
|
||||
|
||||
setIsPageInDom(document.querySelector(selector) !== null);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsPageInDom(document.querySelector(selector) !== null);
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [pageNumber]);
|
||||
|
||||
return isPageInDom;
|
||||
};
|
||||
@@ -23,25 +23,44 @@ export const sortFieldsByPosition = (fields: Field[]): Field[] => {
|
||||
*/
|
||||
export const validateFieldsInserted = (fields: Field[]): boolean => {
|
||||
const fieldCardElements = document.getElementsByClassName('field-card-container');
|
||||
const pdfContent = document.querySelector('[data-pdf-content]');
|
||||
|
||||
// Attach validate attribute on all fields.
|
||||
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');
|
||||
});
|
||||
|
||||
const uninsertedFields = sortFieldsByPosition(fields.filter((field) => !field.inserted));
|
||||
// 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];
|
||||
|
||||
const firstUninsertedFieldElement =
|
||||
firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
|
||||
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' });
|
||||
return false;
|
||||
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 uninsertedFields.length === 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const validateFieldsUninserted = (): boolean => {
|
||||
|
||||
@@ -98,10 +98,8 @@ export const DocumentReadOnlyFields = ({
|
||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
||||
};
|
||||
|
||||
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||
|
||||
return (
|
||||
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map(
|
||||
(field) =>
|
||||
!hiddenFieldIds[field.secondaryId] && (
|
||||
@@ -163,7 +161,7 @@ export const DocumentReadOnlyFields = ({
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
<p className="mt-1 text-center text-xs text-muted-foreground">
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
|
||||
@@ -81,6 +82,8 @@ export function FieldRootContainer({
|
||||
readonly,
|
||||
}: FieldRootContainerProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const isPageInDom = useIsPageInDom(field.page);
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,6 +91,21 @@ export function FieldRootContainer({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the validation signal on the PDF viewer container. When a field
|
||||
// mounts after the virtual list scrolls to its page, the per-element
|
||||
// `data-validate` attribute will not have been set yet. The signal on the
|
||||
// `[data-pdf-content]` container bridges this gap so newly-rendered fields
|
||||
// pick up the validation state immediately.
|
||||
const pdfContent = document.querySelector('[data-pdf-content]');
|
||||
|
||||
if (
|
||||
pdfContent?.getAttribute('data-validate-fields') === 'true' &&
|
||||
isFieldUnsignedAndRequired(field)
|
||||
) {
|
||||
ref.current.setAttribute('data-validate', 'true');
|
||||
setIsValidating(true);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((_mutations) => {
|
||||
if (ref.current) {
|
||||
setIsValidating(ref.current.getAttribute('data-validate') === 'true');
|
||||
@@ -101,7 +119,11 @@ export function FieldRootContainer({
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [isPageInDom]);
|
||||
|
||||
if (!isPageInDom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldContainerPortal field={field}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/al
|
||||
import type { ScrollTarget } from '../virtual-list/use-virtual-list';
|
||||
import { useVirtualList } from '../virtual-list/use-virtual-list';
|
||||
import { PdfViewerErrorState, PdfViewerLoadingState } from './pdf-viewer-states';
|
||||
import { useScrollToPage } from './use-scroll-to-page';
|
||||
|
||||
export type EnvelopePdfViewerProps = {
|
||||
className?: string;
|
||||
@@ -156,7 +157,7 @@ const VirtualizedPageList = ({
|
||||
}: VirtualizedPageListProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { virtualItems, totalSize, constraintWidth } = useVirtualList({
|
||||
const { virtualItems, totalSize, constraintWidth, scrollToItem } = useVirtualList({
|
||||
scrollRef: scrollParentRef,
|
||||
constraintRef,
|
||||
contentRef,
|
||||
@@ -174,9 +175,12 @@ const VirtualizedPageList = ({
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
useScrollToPage(contentRef, scrollToItem);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-pdf-content=""
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
width: '100%',
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useToast } from '../../primitives/use-toast';
|
||||
import type { ScrollTarget } from '../virtual-list/use-virtual-list';
|
||||
import { useVirtualList } from '../virtual-list/use-virtual-list';
|
||||
import { PdfViewerErrorState, PdfViewerLoadingState } from './pdf-viewer-states';
|
||||
import { useScrollToPage } from './use-scroll-to-page';
|
||||
|
||||
export type OverrideImage = {
|
||||
image: string;
|
||||
@@ -217,7 +218,7 @@ const VirtualizedPageList = ({
|
||||
}: VirtualizedPageListProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { virtualItems, totalSize, constraintWidth } = useVirtualList({
|
||||
const { virtualItems, totalSize, constraintWidth, scrollToItem } = useVirtualList({
|
||||
scrollRef: scrollParentRef,
|
||||
constraintRef,
|
||||
contentRef,
|
||||
@@ -235,9 +236,12 @@ const VirtualizedPageList = ({
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
useScrollToPage(contentRef, scrollToItem);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-pdf-content=""
|
||||
style={{
|
||||
height: `${totalSize}px`,
|
||||
width: '100%',
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { type RefObject, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Watch for `data-scroll-to-page` attribute changes on a container element.
|
||||
*
|
||||
* When set (by `validateFieldsInserted`, `handleOnNextFieldClick`, or similar),
|
||||
* scroll the virtual list to the requested page and clear the attribute.
|
||||
*
|
||||
* This is the communication bridge between field validation logic (which knows
|
||||
* which page to scroll to) and the virtual list (which knows how to scroll).
|
||||
*/
|
||||
export const useScrollToPage = (
|
||||
contentRef: RefObject<HTMLElement | null>,
|
||||
scrollToItem: (index: number) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const el = contentRef.current;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-scroll-to-page') {
|
||||
const raw = el.getAttribute('data-scroll-to-page');
|
||||
|
||||
if (raw) {
|
||||
const pageNumber = parseInt(raw, 10);
|
||||
|
||||
if (!isNaN(pageNumber) && pageNumber >= 1) {
|
||||
// Pages are 1-indexed, virtual list items are 0-indexed.
|
||||
scrollToItem(pageNumber - 1);
|
||||
}
|
||||
|
||||
el.removeAttribute('data-scroll-to-page');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(el, { attributes: true, attributeFilter: ['data-scroll-to-page'] });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [contentRef, scrollToItem]);
|
||||
};
|
||||
@@ -34,6 +34,14 @@ export type VirtualListResult = {
|
||||
virtualItems: VirtualItem[];
|
||||
totalSize: number;
|
||||
constraintWidth: number;
|
||||
|
||||
/**
|
||||
* Scroll the scroll container so that the item at the given index is visible.
|
||||
*
|
||||
* The scroll position is calculated from the precomputed item offsets and
|
||||
* adjusted for any content offset (e.g. headers above the virtual list).
|
||||
*/
|
||||
scrollToItem: (index: number) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -292,9 +300,56 @@ export const useVirtualList = (options: VirtualListOptions): VirtualListResult =
|
||||
findStartIndex,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Imperatively scroll the scroll container so that the item at the given
|
||||
* index is at the top of the viewport.
|
||||
*/
|
||||
const scrollToItem = useCallback(
|
||||
(index: number) => {
|
||||
if (index < 0 || index >= itemCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemOffset = offsets[index] ?? 0;
|
||||
|
||||
if (scrollRef === 'window') {
|
||||
const contentEl = contentRef?.current;
|
||||
const contentTop = contentEl ? contentEl.getBoundingClientRect().top + window.scrollY : 0;
|
||||
|
||||
window.scrollTo({
|
||||
top: contentTop + itemOffset,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
} else {
|
||||
const scrollEl = scrollRef.current;
|
||||
|
||||
if (!scrollEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recalculate content offset to get the most up-to-date value.
|
||||
const contentEl = contentRef?.current;
|
||||
let contentOffset = 0;
|
||||
|
||||
if (contentEl) {
|
||||
const scrollRect = scrollEl.getBoundingClientRect();
|
||||
const contentRect = contentEl.getBoundingClientRect();
|
||||
contentOffset = contentRect.top - scrollRect.top + scrollEl.scrollTop;
|
||||
}
|
||||
|
||||
scrollEl.scrollTo({
|
||||
top: contentOffset + itemOffset,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
},
|
||||
[scrollRef, contentRef, offsets, itemCount],
|
||||
);
|
||||
|
||||
return {
|
||||
virtualItems,
|
||||
totalSize,
|
||||
constraintWidth,
|
||||
scrollToItem,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Rnd } from 'react-rnd';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
|
||||
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
@@ -50,7 +51,17 @@ export type FieldItemProps = {
|
||||
/**
|
||||
* The item when editing fields??
|
||||
*/
|
||||
export const FieldItem = ({
|
||||
export const FieldItem = (props: FieldItemProps) => {
|
||||
const isPageInDom = useIsPageInDom(props.field.pageNumber);
|
||||
|
||||
if (!isPageInDom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FieldItemInner {...props} />;
|
||||
};
|
||||
|
||||
const FieldItemInner = ({
|
||||
fieldClassName,
|
||||
field,
|
||||
passive,
|
||||
|
||||
Reference in New Issue
Block a user