Задать вопрос
sanManjiro
@sanManjiro

Как сделать кликабельные аннотации в react-pdf?

У меня есть компонент, который позволяет просматривать PDF файлы. В Page я добавил renderAnnotationLayer={true} и ипортировал стили import 'react-pdf/dist/Page/AnnotationLayer.css'; имхо без них не работает. На localhost в браузере в мобильном и обычном режиме открыть ссылку получается, но при загрузке изменений на сайт, в мобильном режиме ссылки не открываются и с самого телефона соответственно тоже.
Компонент:
Component

'use client';
import { Document, Page, pdfjs } from 'react-pdf';

// COMPONENTS
import { useModalControl } from '@components/modals/document-modal/hooks/useModalControl';
import { usePdfViewer } from '@components/modals/document-modal/hooks/usePdfViewer';
import { PdfControls } from './components/PdfControls';

// STYLES
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
import style from './DocumentModal.module.scss';

// Установите worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();

interface DocumentModalProps {
	isOpen: boolean;
	onClose: () => void;
	documentUrl: string;
}

export const DocumentModal = ({ isOpen, onClose, documentUrl }: DocumentModalProps) => {
	const dialogRef = useModalControl(isOpen);
	const { scale, isLoading, error, numPages, handleZoomIn, handleZoomOut, handleLoadSuccess, handleLoadError } =
		usePdfViewer();

	const handleDownload = () => {
		const link = document.createElement('a');
		link.href = documentUrl;
		link.download = documentUrl.split('/').pop() || 'document.pdf';
		document.body.appendChild(link);
		link.click();
		document.body.removeChild(link);
	};

	if (!isOpen) return null;

	const handleLinkClick = (href: string) => {
		try {
			const isAbsolute = /^https?:\/\//i.test(href);
			const absoluteUrl = isAbsolute ? href : new URL(href, window.location.origin).toString();

			if (/Android|iPhone|iPad/i.test(navigator.userAgent)) {
				const newWindow = window.open(absoluteUrl, '_blank', 'noopener,noreferrer');
				if (!newWindow || newWindow.closed) {
					window.location.href = absoluteUrl;
				}
			} else {
				const a = document.createElement('a');
				a.href = absoluteUrl;
				a.target = '_blank';
				a.rel = 'noopener noreferrer';
				document.body.appendChild(a);
				a.click();
				document.body.removeChild(a);
			}
		} catch (error) {
			console.error('Error opening link:', error);
		}
	};

	return (
		<dialog ref={dialogRef} className={style['document-modal']} onClose={onClose}>
			<div
				className={style['document-modal__overlay']}
				onClick={e => {
					if (e.target === e.currentTarget) {
						onClose();
					}
				}}
			>
				<div className={style['document-modal__wrapper']}>
					<PdfControls
						scale={scale}
						onZoomIn={handleZoomIn}
						onZoomOut={handleZoomOut}
						onDownload={handleDownload}
						onClose={onClose}
					/>

					<div className={style['document-modal__content']}>
						{isLoading && <div className={style['document-modal__loader']}>Загрузка документа...</div>}
						{error && <div className={style['document-modal__error']}>{error}</div>}

						<Document
							file={documentUrl}
							onLoadSuccess={handleLoadSuccess}
							onLoadError={handleLoadError}
							loading={null}
							noData={null}
							externalLinkTarget='_blank'
						>
							{Array.from(new Array(numPages), (el, index) => (
								<Page
									key={`page_${index + 1}`}
									pageNumber={index + 1}
									renderTextLayer={true}
									renderAnnotationLayer={true}
									scale={scale}
									className={style['document-modal__page']}
									loading={null}
									onClick={e => {
										if (e.target instanceof HTMLAnchorElement) {
											e.preventDefault();
											handleLinkClick(e.target.href);
										}
									}}
									onTouchEnd={e => {
										const touch = e.changedTouches[0];
										const element = document.elementFromPoint(touch.clientX, touch.clientY);
										if (element && element.tagName === 'A') {
											e.preventDefault();
											handleLinkClick((element as HTMLAnchorElement).href);
										}
									}}
								/>
							))}
						</Document>
					</div>
				</div>
			</div>
		</dialog>
	);
};


SCSS:
module.scss
.document-modal {
	padding: 0;
	border: none;
	width: 100vw;
	height: 100vh;
	max-width: 100vw !important;
	max-height: 100vh !important;
	margin: 0;
	background: transparent;
	position: fixed;
	inset: 0;

	&::backdrop {
		background: rgba(0, 0, 0, 0.5);
		backdrop-filter: blur(3px);
	}

	&__overlay {
		display: flex;
		justify-content: center;
		align-items: center;

		width: 100%;
		height: 100%;

		padding: 16px;
	}

	&__wrapper {
		position: relative;

		display: flex;
		flex-direction: column;
		overflow: hidden;

		background: white;
		border-radius: 8px;

		width: 100%;
		height: 100%;
	}

	&__header {
		display: grid;
		grid-template-columns: 1fr auto 1fr;
		align-items: center;

		padding: 15px 20px;
		border-bottom: 1px solid #eee;

		font-size: clamp(12px, 1.5vw, 16px);
	}

	&__controls-left {
		justify-self: start;

		button {
			padding: 5px clamp(8px, 1.5vw, 15px);
			border-radius: 4px;
			border: 1px solid #ddd;
			background: white;
			cursor: pointer;

			&:hover {
				background: #f0f0f0;
			}
		}
	}

	&__controls-center {
		display: flex;
		gap: clamp(5px, 1vw, 10px);
		align-items: center;
		justify-content: center;

		button {
			padding: 5px 15px;
			border-radius: 4px;
			border: 1px solid #ddd;
			background: white;
			cursor: pointer;

			&:hover {
				background: #f0f0f0;
			}
		}

		span {
			text-align: center;
			font-weight: 500;
		}
	}

	&__close {
		justify-self: end;
		width: 24px;
		height: 24px;
		border: none;
		background: transparent;
		cursor: pointer;
		padding: 0;
		position: relative;

		&::before,
		&::after {
			content: '';
			position: absolute;
			width: 2px;
			height: 16px;
			background-color: #666;
			top: 50%;
			left: 50%;
		}

		&::before {
			transform: translate(-50%, -50%) rotate(45deg);
		}

		&::after {
			transform: translate(-50%, -50%) rotate(-45deg);
		}

		&:hover::before,
		&:hover::after {
			background-color: #333;
		}
	}

	&__content {
		flex: 1;
		overflow-y: auto;
		padding: 20px;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 20px;
	}

	&__page {
		box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
		background: white;

		&:not(:last-child) {
			margin-bottom: 20px;
		}

		canvas {
			max-width: 100%;
			height: auto !important;
		}
	}

	&__loader,
	&__error {
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background: white;
		padding: 20px;
		border-radius: 8px;
		box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
		text-align: center;
	}

	&__error {
		color: #dc3545;
	}
}

.document-modal__page-container {
	position: relative;
}

.react-pdf__Page__annotations.annotationLayer {
	a {
		cursor: pointer;
		touch-action: manipulation;
		pointer-events: auto !important;
	}
}

.annotationLayer {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}
  • Вопрос задан
  • 21 просмотр
Подписаться 1 Простой Комментировать
Решения вопроса 1
sanManjiro
@sanManjiro Автор вопроса
Решил проблему сам, оставлю решение, проблема заключается в том, что все ссылки аннотаций размещаются поверх через абсолютную позицию, из-за этого, если нарушаются пропорции экрана, т.е. ширины, все ссылки съезжают, я убрал из компонента TextLayer как scss, так и пропст в , после добавил ref в div, ref={documentWrapperRef}, и в Page добавил вот такую запись width={documentWrapperRef.current?.getBoundingClientRect().width || undefined}. Все работает отлично, вот рабочий код:
'use client';
import { useRef } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';

// COMPONENTS
import { useModalControl } from '@components/modals/document-modal/hooks/useModalControl';
import { usePdfViewer } from '@components/modals/document-modal/hooks/usePdfViewer';
import { PdfControls } from './components/PdfControls';

// STYLES
import 'react-pdf/dist/Page/AnnotationLayer.css';
import style from './DocumentModal.module.scss';

// Установите worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();

interface DocumentModalProps {
	isOpen: boolean;
	onClose: () => void;
	documentUrl: string;
}

export const DocumentModal = ({ isOpen, onClose, documentUrl }: DocumentModalProps) => {
	const dialogRef = useModalControl(isOpen);
	const documentWrapperRef = useRef<HTMLDivElement>(null);
	const {
		scale,
		isLoading,
		error,
		numPages,
		handleZoomIn,
		handleZoomOut,
		handleLoadSuccess,
		handleLoadError,
		handleDownload,
	} = usePdfViewer();

	if (!isOpen) return null;

	return (
		<dialog ref={dialogRef} className={style['document-modal']} onClose={onClose}>
			<div
				className={style['document-modal__overlay']}
				onClick={e => {
					if (e.target === e.currentTarget) {
						onClose();
					}
				}}
			>
				<div className={style['document-modal__wrapper']}>
					<PdfControls
						scale={scale}
						onZoomIn={handleZoomIn}
						onZoomOut={handleZoomOut}
						onDownload={() => handleDownload(documentUrl)}
						onClose={onClose}
					/>

					<div className={style['document-modal__content']} ref={documentWrapperRef}>
						{isLoading && <div className={style['document-modal__loader']}>Загрузка документа...</div>}
						{error && <div className={style['document-modal__error']}>{error}</div>}

						<Document
							file={documentUrl}
							onLoadSuccess={handleLoadSuccess}
							onLoadError={handleLoadError}
							loading={null}
							noData={null}
							externalLinkTarget='_blank'
						>
							{Array.from(new Array(numPages), (el, index) => (
								<Page
									key={`page_${index + 1}`}
									pageNumber={index + 1}
									renderAnnotationLayer={true}
									scale={scale}
									className={style['document-modal__page']}
									loading={null}
									width={documentWrapperRef.current?.getBoundingClientRect().width || undefined}
								/>
							))}
						</Document>
					</div>
				</div>
			</div>
		</dialog>
	);
};
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы