import { useRedaction as useEmbedPdfRedaction, RedactionSelectionMenuProps } from '@embedpdf/plugin-redaction/react';
import { PdfAnnotationSubtype } from '@embedpdf/models';
import { ActionIcon, Tooltip, Button, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { createPortal } from 'react-dom';
import { useEffect, useState, useRef, useCallback } from 'react';
import DeleteIcon from '@mui/icons-material/Delete';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { useRedaction } from '@app/contexts/RedactionContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
export type { RedactionSelectionMenuProps };
export function RedactionSelectionMenu(props: any) {
const activeDocumentId = useActiveDocumentId();
// Don't render until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return (
);
}
function RedactionSelectionMenuInner({
documentId,
context,
selected,
menuWrapperProps,
}: RedactionSelectionMenuProps & { documentId: string }) {
const item = context?.type === 'redaction'
? context.item
: (context?.type === 'annotation' ? (context as any).annotation?.object : null);
const isRedaction = context?.type === 'redaction' || (context?.type === 'annotation' && item?.type === PdfAnnotationSubtype.REDACT);
const pageIndex = context?.pageIndex;
const { t } = useTranslation();
const { provides } = useEmbedPdfRedaction(documentId);
const { setRedactionsApplied } = useRedaction();
const wrapperRef = useRef(null);
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
// Merge refs - menuWrapperProps.ref is a callback ref
const setRef = useCallback((node: HTMLDivElement | null) => {
wrapperRef.current = node;
// Call the EmbedPDF ref callback
menuWrapperProps?.ref?.(node);
}, [menuWrapperProps]);
const handleRemove = useCallback(() => {
if (provides?.removePending && item && pageIndex !== undefined) {
provides.removePending(pageIndex, item.id);
}
}, [provides, item, pageIndex]);
const handleApply = useCallback(() => {
if (provides?.commitPending && item && pageIndex !== undefined) {
provides.commitPending(pageIndex, item.id);
// Mark redactions as applied (but not yet saved) so the Save Changes button stays enabled
// This ensures the button doesn't become disabled when pendingCount decreases
setRedactionsApplied(true);
}
}, [provides, item, pageIndex, setRedactionsApplied]);
// Calculate position for portal based on wrapper element
useEffect(() => {
if (!selected || !isRedaction || !item || !wrapperRef.current) {
setMenuPosition(null);
return;
}
const updatePosition = () => {
const wrapper = wrapperRef.current;
if (!wrapper) {
setMenuPosition(null);
return;
}
const wrapperRect = wrapper.getBoundingClientRect();
// Position menu below the wrapper, centered
// Use getBoundingClientRect which gives viewport-relative coordinates
// Since we're using fixed positioning in the portal, we don't need to add scroll offsets
setMenuPosition({
top: wrapperRect.bottom + 8,
left: wrapperRect.left + wrapperRect.width / 2,
});
};
updatePosition();
// Update position on scroll/resize
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}, [selected, item]);
// Early return AFTER all hooks have been called
if (!selected || !isRedaction || !item) return null;
const menuContent = menuPosition ? (
}
styles={{
root: { flexShrink: 0, whiteSpace: 'nowrap' },
}}
>
Apply (permanent)
) : null;
return (
<>
{/* Invisible wrapper that provides positioning - uses EmbedPDF's menuWrapperProps */}
{typeof document !== 'undefined' && menuContent
? createPortal(menuContent, document.body)
: null}
>
);
}