mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
make clicking on comments open the comments sidebar and more (#6174)
make clicking on comments open the comments sidebar and add a button to create comments into the empty state of the comments sidebar <img width="2056" height="1081" alt="Screenshot 2026-04-23 at 12 39 50 PM" src="https://github.com/user-attachments/assets/6f15484d-d04f-4900-92c6-b2cc397d6d08" /> <img width="627" height="396" alt="Screenshot 2026-04-23 at 12 40 06 PM" src="https://github.com/user-attachments/assets/509e5526-0082-4fc6-a98f-829bb4c1baf2" />
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Group } from "@mantine/core";
|
import { Group } from "@mantine/core";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useEffect, useState, useRef, useCallback } from "react";
|
import { useEffect, useState, useRef, useCallback, useMemo } from "react";
|
||||||
import { useAnnotation } from "@embedpdf/plugin-annotation/react";
|
import { useAnnotation } from "@embedpdf/plugin-annotation/react";
|
||||||
import type { TrackedAnnotation } from "@embedpdf/plugin-annotation";
|
import type { TrackedAnnotation } from "@embedpdf/plugin-annotation";
|
||||||
import {
|
import {
|
||||||
@@ -47,7 +47,7 @@ function AnnotationSelectionMenuInner({
|
|||||||
}: AnnotationSelectionMenuProps & { documentId: string }) {
|
}: AnnotationSelectionMenuProps & { documentId: string }) {
|
||||||
const annotation = context?.annotation;
|
const annotation = context?.annotation;
|
||||||
const pageIndex = context?.pageIndex;
|
const pageIndex = context?.pageIndex;
|
||||||
const { provides } = useAnnotation(documentId);
|
const { state, provides } = useAnnotation(documentId);
|
||||||
const { scrollActions, requestCommentFocus } = useViewer();
|
const { scrollActions, requestCommentFocus } = useViewer();
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const [menuPosition, setMenuPosition] = useState<{
|
const [menuPosition, setMenuPosition] = useState<{
|
||||||
@@ -65,17 +65,52 @@ function AnnotationSelectionMenuInner({
|
|||||||
wrapperRef,
|
wrapperRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Read isInSidebar from live EmbedPDF state rather than annotation.object, which can be
|
||||||
|
// stale after updateAnnotation() is called while the annotation is selected.
|
||||||
|
// Also checks non-empty contents: customData.isComment is not persisted to PDF, but
|
||||||
|
// contents is a standard PDF field and survives save/reload.
|
||||||
|
const isInSidebar = useMemo(() => {
|
||||||
|
const annId = (annotation?.object as AnnotationObject | undefined)?.id;
|
||||||
|
if (!annId) return false;
|
||||||
|
for (const tracked of Object.values(state.byUid)) {
|
||||||
|
const obj = tracked.object;
|
||||||
|
if (obj.id !== annId) continue;
|
||||||
|
const { type } = obj;
|
||||||
|
// TEXT and CARET are standalone comment annotations — they use CommentButton,
|
||||||
|
// not AttachCommentButton, so isInSidebar is irrelevant for them.
|
||||||
|
if (
|
||||||
|
type === PdfAnnotationSubtype.TEXT ||
|
||||||
|
type === PdfAnnotationSubtype.CARET
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
// customData is a runtime field EmbedPDF adds but doesn't declare in its TS types
|
||||||
|
const customData = (
|
||||||
|
obj as unknown as { customData?: Record<string, unknown> }
|
||||||
|
).customData;
|
||||||
|
const isExplicit = customData?.isComment === true;
|
||||||
|
// customData (incl. toolId/isComment) is not persisted to PDF; contents is.
|
||||||
|
// Any non-TEXT/FreeText/CARET annotation with non-empty contents has a comment.
|
||||||
|
const hasContents =
|
||||||
|
type !== PdfAnnotationSubtype.FREETEXT &&
|
||||||
|
!obj.inReplyToId &&
|
||||||
|
(obj.contents ?? "").trim().length > 0;
|
||||||
|
return isExplicit || hasContents;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [state, annotation?.object]);
|
||||||
|
|
||||||
// Auto-open the comments sidebar when a comment annotation is selected
|
// Auto-open the comments sidebar when a comment annotation is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const annObj = annotation?.object as AnnotationObject | undefined;
|
const annObj = annotation?.object as AnnotationObject | undefined;
|
||||||
const annId = annObj?.id;
|
const annId = annObj?.id;
|
||||||
if (!selected || !annId || pageIndex === undefined) return;
|
if (!selected || !annId || pageIndex === undefined) return;
|
||||||
const toolId = annObj?.customData?.toolId;
|
|
||||||
const annType = annObj?.type;
|
const annType = annObj?.type;
|
||||||
|
// TEXT (type 1) = textComment; CARET (type 14) = insertText/replaceText.
|
||||||
|
// These are always comment annotations regardless of toolId (lost after reload).
|
||||||
const isComment =
|
const isComment =
|
||||||
(annType === PdfAnnotationSubtype.TEXT && toolId === "textComment") ||
|
annType === PdfAnnotationSubtype.TEXT ||
|
||||||
(annType === PdfAnnotationSubtype.CARET &&
|
annType === PdfAnnotationSubtype.CARET ||
|
||||||
(toolId === "insertText" || toolId === "replaceText"));
|
isInSidebar;
|
||||||
if (!isComment) return;
|
if (!isComment) return;
|
||||||
requestCommentFocus(
|
requestCommentFocus(
|
||||||
documentId,
|
documentId,
|
||||||
@@ -83,7 +118,7 @@ function AnnotationSelectionMenuInner({
|
|||||||
annId,
|
annId,
|
||||||
(annObj?.contents ?? "").trim().length > 0,
|
(annObj?.contents ?? "").trim().length > 0,
|
||||||
);
|
);
|
||||||
}, [selected, annotation?.object]);
|
}, [selected, annotation?.object, isInSidebar]);
|
||||||
|
|
||||||
// Click outside to deselect
|
// Click outside to deselect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -170,6 +205,7 @@ function AnnotationSelectionMenuInner({
|
|||||||
<Group gap="sm" wrap="nowrap" justify="center">
|
<Group gap="sm" wrap="nowrap" justify="center">
|
||||||
<AnnotationTypeButtons
|
<AnnotationTypeButtons
|
||||||
{...handlers}
|
{...handlers}
|
||||||
|
isInSidebar={isInSidebar}
|
||||||
annotation={annotation}
|
annotation={annotation}
|
||||||
documentId={documentId}
|
documentId={documentId}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
|
|||||||
@@ -58,9 +58,13 @@ export function AnnotationTypeButtons(props: AnnotationTypeButtonsProps) {
|
|||||||
onCommentColorChange,
|
onCommentColorChange,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const attachCommentButton = (
|
// When a comment is attached, show the same chat-bubble "View comment" button used by
|
||||||
|
// standalone comment annotations. When no comment, show the "Add comment" attach button.
|
||||||
|
const attachCommentButton = isInSidebar ? (
|
||||||
|
<CommentButton hasContent={hasCommentContent} onClick={onViewComment} />
|
||||||
|
) : (
|
||||||
<AttachCommentButton
|
<AttachCommentButton
|
||||||
isInSidebar={isInSidebar}
|
isInSidebar={false}
|
||||||
onView={onViewComment}
|
onView={onViewComment}
|
||||||
onAdd={onAddToSidebar}
|
onAdd={onAddToSidebar}
|
||||||
/>
|
/>
|
||||||
@@ -221,10 +225,6 @@ export function AnnotationTypeButtons(props: AnnotationTypeButtonsProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{attachCommentButton}
|
{attachCommentButton}
|
||||||
<CommentButton
|
|
||||||
hasContent={hasCommentContent}
|
|
||||||
onClick={onViewComment}
|
|
||||||
/>
|
|
||||||
<LinkButton
|
<LinkButton
|
||||||
firstLinkTarget={firstLinkTarget}
|
firstLinkTarget={firstLinkTarget}
|
||||||
onGoToLink={onGoToLink}
|
onGoToLink={onGoToLink}
|
||||||
|
|||||||
@@ -22,13 +22,37 @@ import EditIcon from "@mui/icons-material/Edit";
|
|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import { useAnnotation } from "@embedpdf/plugin-annotation/react";
|
import { useAnnotation } from "@embedpdf/plugin-annotation/react";
|
||||||
import { getSidebarAnnotationsWithRepliesGroupedByPage } from "@embedpdf/plugin-annotation";
|
import { getSidebarAnnotationsWithRepliesGroupedByPage } from "@embedpdf/plugin-annotation";
|
||||||
import { PdfAnnotationSubtype, PdfAnnotationReplyType } from "@embedpdf/models";
|
import {
|
||||||
|
PdfAnnotationSubtype,
|
||||||
|
PdfAnnotationReplyType,
|
||||||
|
type PdfAnnotationObject,
|
||||||
|
type PdfTextAnnoObject,
|
||||||
|
} from "@embedpdf/models";
|
||||||
import { useCommentAuthor } from "@app/contexts/CommentAuthorContext";
|
import { useCommentAuthor } from "@app/contexts/CommentAuthorContext";
|
||||||
import { useViewer } from "@app/contexts/ViewerContext";
|
import { useViewer } from "@app/contexts/ViewerContext";
|
||||||
|
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||||
|
import { useAnnotation as useAnnotationContext } from "@app/contexts/AnnotationContext";
|
||||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||||
|
|
||||||
const SIDEBAR_WIDTH = "18rem";
|
const SIDEBAR_WIDTH = "18rem";
|
||||||
|
|
||||||
|
/** PDF subtypes that are inherently standalone comment annotations (not linked to other annotations). */
|
||||||
|
const STANDALONE_COMMENT_SUBTYPES = new Set([
|
||||||
|
PdfAnnotationSubtype.TEXT,
|
||||||
|
PdfAnnotationSubtype.FREETEXT,
|
||||||
|
PdfAnnotationSubtype.CARET,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isStandaloneCommentType(type: number | undefined): boolean {
|
||||||
|
return (
|
||||||
|
type !== undefined &&
|
||||||
|
STANDALONE_COMMENT_SUBTYPES.has(type as PdfAnnotationSubtype)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANNOTATE_PANEL_ID = "annotate" as const;
|
||||||
|
const TEXT_COMMENT_TOOL_ID = "textComment" as const;
|
||||||
|
|
||||||
/** Format annotation date for display (e.g. "Mar 11, 6:05 PM"). */
|
/** Format annotation date for display (e.g. "Mar 11, 6:05 PM"). */
|
||||||
function formatCommentDate(obj: any): string {
|
function formatCommentDate(obj: any): string {
|
||||||
const raw =
|
const raw =
|
||||||
@@ -143,8 +167,20 @@ function isCommentAnnotation(ann: any): boolean {
|
|||||||
return true;
|
return true;
|
||||||
// Any annotation explicitly added to comments via the "Add comment" button
|
// Any annotation explicitly added to comments via the "Add comment" button
|
||||||
if (ann?.customData?.isComment === true) return true;
|
if (ann?.customData?.isComment === true) return true;
|
||||||
// CARET (type 14) = insertText/replaceText; TEXT (type 1) = textComment
|
const type = ann?.type;
|
||||||
if (!toolId && (ann?.type === 14 || ann?.type === 1)) return true;
|
// Standalone comment types (TEXT, FREETEXT, CARET) without a toolId are always comments
|
||||||
|
if (!toolId && isStandaloneCommentType(type)) return true;
|
||||||
|
// Non-standalone annotations with non-empty contents: customData (including isComment and
|
||||||
|
// toolId) is NOT persisted to PDF, but `contents` is a standard PDF field and survives
|
||||||
|
// save/reload. Exclude standalone comment types (TEXT, FREETEXT, CARET) which use
|
||||||
|
// `contents` for their own annotation text. Exclude replies.
|
||||||
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
!isStandaloneCommentType(type) &&
|
||||||
|
!ann?.inReplyToId &&
|
||||||
|
(ann?.contents ?? "").trim().length > 0
|
||||||
|
)
|
||||||
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +249,8 @@ export function CommentsSidebar({
|
|||||||
} = useViewer() ?? {};
|
} = useViewer() ?? {};
|
||||||
const scrollViewportRef = useRef<HTMLDivElement | null>(null);
|
const scrollViewportRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { state, provides } = useAnnotation(documentId);
|
const { state, provides } = useAnnotation(documentId);
|
||||||
|
const { handleToolSelectForced } = useToolWorkflow();
|
||||||
|
const { activateAnnotationToolRef } = useAnnotationContext();
|
||||||
const [draftContents, setDraftContents] = useState<Record<string, string>>(
|
const [draftContents, setDraftContents] = useState<Record<string, string>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@@ -324,8 +362,8 @@ export function CommentsSidebar({
|
|||||||
const all = getSidebarAnnotationsWithRepliesGroupedByPage(state) ?? {};
|
const all = getSidebarAnnotationsWithRepliesGroupedByPage(state) ?? {};
|
||||||
const filtered: typeof all = {};
|
const filtered: typeof all = {};
|
||||||
for (const [page, entries] of Object.entries(all)) {
|
for (const [page, entries] of Object.entries(all)) {
|
||||||
const commentEntries = (entries as typeof entries).filter((e) =>
|
const commentEntries = entries.filter((e) =>
|
||||||
isCommentAnnotation((e as any).annotation?.object),
|
isCommentAnnotation(e.annotation.object),
|
||||||
);
|
);
|
||||||
if (commentEntries.length > 0) {
|
if (commentEntries.length > 0) {
|
||||||
filtered[Number(page)] = commentEntries;
|
filtered[Number(page)] = commentEntries;
|
||||||
@@ -341,12 +379,11 @@ export function CommentsSidebar({
|
|||||||
// state is AnnotationDocumentState — selectedUids are keys in byUid, and may equal id.
|
// state is AnnotationDocumentState — selectedUids are keys in byUid, and may equal id.
|
||||||
const selectedAnnotationIds = useMemo(() => {
|
const selectedAnnotationIds = useMemo(() => {
|
||||||
const selectedUids: string[] = state?.selectedUids ?? [];
|
const selectedUids: string[] = state?.selectedUids ?? [];
|
||||||
const byUid: Record<string, any> = (state as any)?.byUid ?? {};
|
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
for (const uid of selectedUids) {
|
for (const uid of selectedUids) {
|
||||||
// uid itself may be the annotation id
|
// uid itself may be the annotation id
|
||||||
ids.add(uid);
|
ids.add(uid);
|
||||||
const annId = byUid[uid]?.object?.id;
|
const annId = state.byUid[uid]?.object.id;
|
||||||
if (annId) ids.add(annId);
|
if (annId) ids.add(annId);
|
||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
@@ -383,12 +420,12 @@ export function CommentsSidebar({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const isLinkedAnnotation = (ann: any) => {
|
const isLinkedAnnotation = (ann: any) => {
|
||||||
const toolId = ann?.customData?.toolId ?? ann?.customData?.annotationToolId;
|
const type = ann?.type;
|
||||||
|
if (isStandaloneCommentType(type)) return false;
|
||||||
|
if (ann?.inReplyToId) return false;
|
||||||
return (
|
return (
|
||||||
ann?.customData?.isComment === true &&
|
ann?.customData?.isComment === true ||
|
||||||
toolId !== "textComment" &&
|
(type !== undefined && (ann?.contents ?? "").trim().length > 0)
|
||||||
toolId !== "insertText" &&
|
|
||||||
toolId !== "replaceText"
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -408,7 +445,13 @@ export function CommentsSidebar({
|
|||||||
const { pageIndex, id, ann } = deleteModal;
|
const { pageIndex, id, ann } = deleteModal;
|
||||||
const existing = (ann?.customData ?? {}) as Record<string, unknown>;
|
const existing = (ann?.customData ?? {}) as Record<string, unknown>;
|
||||||
const { isComment: _removed, ...rest } = existing;
|
const { isComment: _removed, ...rest } = existing;
|
||||||
provides.updateAnnotation(pageIndex, id, { customData: rest } as any);
|
// Also clear contents: the contents field is the persisted signal for
|
||||||
|
// post-reload linked annotations, so clearing it removes the annotation
|
||||||
|
// from the sidebar (contents is not visually rendered on ink/shape/markup types).
|
||||||
|
provides.updateAnnotation(pageIndex, id, {
|
||||||
|
customData: rest,
|
||||||
|
contents: "",
|
||||||
|
} as unknown as Partial<PdfAnnotationObject>);
|
||||||
setDeleteModal(null);
|
setDeleteModal(null);
|
||||||
}, [deleteModal, provides]);
|
}, [deleteModal, provides]);
|
||||||
|
|
||||||
@@ -443,7 +486,7 @@ export function CommentsSidebar({
|
|||||||
origin: { x: 0, y: 0 },
|
origin: { x: 0, y: 0 },
|
||||||
size: { width: 1, height: 1 },
|
size: { width: 1, height: 1 },
|
||||||
};
|
};
|
||||||
provides.createAnnotation(pageIndex, {
|
const reply: PdfTextAnnoObject = {
|
||||||
type: PdfAnnotationSubtype.TEXT,
|
type: PdfAnnotationSubtype.TEXT,
|
||||||
id: `reply-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
id: `reply-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
@@ -452,7 +495,8 @@ export function CommentsSidebar({
|
|||||||
inReplyToId: parentId,
|
inReplyToId: parentId,
|
||||||
replyType: PdfAnnotationReplyType.Reply,
|
replyType: PdfAnnotationReplyType.Reply,
|
||||||
author: displayName,
|
author: displayName,
|
||||||
} as any);
|
};
|
||||||
|
provides.createAnnotation(pageIndex, reply);
|
||||||
setReplyDrafts((prev) => ({ ...prev, [key]: "" }));
|
setReplyDrafts((prev) => ({ ...prev, [key]: "" }));
|
||||||
},
|
},
|
||||||
[provides, replyDrafts, displayName],
|
[provides, replyDrafts, displayName],
|
||||||
@@ -476,6 +520,13 @@ export function CommentsSidebar({
|
|||||||
[provides, displayName],
|
[provides, displayName],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAddComment = useCallback(() => {
|
||||||
|
handleToolSelectForced(ANNOTATE_PANEL_ID);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
activateAnnotationToolRef.current?.(TEXT_COMMENT_TOOL_ID);
|
||||||
|
});
|
||||||
|
}, [handleToolSelectForced, activateAnnotationToolRef]);
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -510,19 +561,49 @@ export function CommentsSidebar({
|
|||||||
height="1.25rem"
|
height="1.25rem"
|
||||||
style={{ color: "var(--mantine-color-dimmed)", flexShrink: 0 }}
|
style={{ color: "var(--mantine-color-dimmed)", flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
<Text fw={600} size="sm" tt="uppercase" lts={0.5}>
|
<Text fw={600} size="sm" tt="uppercase" lts={0.5} style={{ flex: 1 }}>
|
||||||
{t("viewer.comments.title", "Comments")}
|
{t("viewer.comments.title", "Comments")}
|
||||||
</Text>
|
</Text>
|
||||||
|
{totalCount > 0 && (
|
||||||
|
<Tooltip label={t("viewer.comments.addComment", "Add comment")}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
onClick={handleAddComment}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="add" width="1.25rem" height="1.25rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
<ScrollArea style={{ flex: 1 }}>
|
||||||
<Stack p="sm" gap="md">
|
<Stack p="sm" gap="md">
|
||||||
{totalCount === 0 ? (
|
{totalCount === 0 ? (
|
||||||
<Text size="sm" c="dimmed">
|
<Stack align="center" gap="sm" py="lg">
|
||||||
{t(
|
<LocalIcon
|
||||||
"viewer.comments.hint",
|
icon="comment"
|
||||||
"Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.",
|
width="2rem"
|
||||||
)}
|
height="2rem"
|
||||||
</Text>
|
style={{ color: "var(--mantine-color-dimmed)" }}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
|
{t(
|
||||||
|
"viewer.comments.hint",
|
||||||
|
"Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
onClick={handleAddComment}
|
||||||
|
leftSection={
|
||||||
|
<LocalIcon icon="add" width="1rem" height="1rem" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("viewer.comments.addComment", "Add comment")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
pageNumbers.map((pageIndex) => {
|
pageNumbers.map((pageIndex) => {
|
||||||
const entries = byPage[pageIndex] ?? [];
|
const entries = byPage[pageIndex] ?? [];
|
||||||
|
|||||||
@@ -27,6 +27,32 @@ export type AnnotationType =
|
|||||||
| "stamp"
|
| "stamp"
|
||||||
| "unknown";
|
| "unknown";
|
||||||
|
|
||||||
|
const TEXT_MARKUP_SUBTYPES = [
|
||||||
|
PdfAnnotationSubtype.HIGHLIGHT,
|
||||||
|
PdfAnnotationSubtype.UNDERLINE,
|
||||||
|
PdfAnnotationSubtype.SQUIGGLY,
|
||||||
|
PdfAnnotationSubtype.STRIKEOUT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SHAPE_SUBTYPES = [
|
||||||
|
PdfAnnotationSubtype.SQUARE,
|
||||||
|
PdfAnnotationSubtype.CIRCLE,
|
||||||
|
PdfAnnotationSubtype.POLYGON,
|
||||||
|
];
|
||||||
|
|
||||||
|
const LINE_SUBTYPES = [
|
||||||
|
PdfAnnotationSubtype.LINE,
|
||||||
|
PdfAnnotationSubtype.POLYLINE,
|
||||||
|
];
|
||||||
|
|
||||||
|
const STROKE_COLOR_SUBTYPES = [
|
||||||
|
PdfAnnotationSubtype.LINE,
|
||||||
|
PdfAnnotationSubtype.SQUARE,
|
||||||
|
PdfAnnotationSubtype.CIRCLE,
|
||||||
|
PdfAnnotationSubtype.POLYGON,
|
||||||
|
PdfAnnotationSubtype.POLYLINE,
|
||||||
|
];
|
||||||
|
|
||||||
export type FirstLinkTarget =
|
export type FirstLinkTarget =
|
||||||
| { type: "uri"; uri: string }
|
| { type: "uri"; uri: string }
|
||||||
| { type: "goto"; pageIndex: number };
|
| { type: "goto"; pageIndex: number };
|
||||||
@@ -134,17 +160,36 @@ export function useAnnotationMenuHandlers({
|
|||||||
const toolId = (annotation?.object as AnnotationObject | undefined)
|
const toolId = (annotation?.object as AnnotationObject | undefined)
|
||||||
?.customData?.toolId;
|
?.customData?.toolId;
|
||||||
|
|
||||||
if (type !== undefined && [9, 10, 11, 12].includes(type))
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
TEXT_MARKUP_SUBTYPES.includes(type as PdfAnnotationSubtype)
|
||||||
|
)
|
||||||
return "textMarkup";
|
return "textMarkup";
|
||||||
if (type === 15)
|
if (type === PdfAnnotationSubtype.INK)
|
||||||
return toolId === "inkHighlighter" ? "inkHighlighter" : "ink";
|
return toolId === "inkHighlighter" ? "inkHighlighter" : "ink";
|
||||||
if (type === 1 && toolId === "textComment") return "comment";
|
// TEXT and CARET fall back to type alone after save/reload (customData.toolId is absent).
|
||||||
if (type === 14 && (toolId === "insertText" || toolId === "replaceText"))
|
if (
|
||||||
|
type === PdfAnnotationSubtype.TEXT &&
|
||||||
|
(!toolId || toolId === "textComment")
|
||||||
|
)
|
||||||
return "comment";
|
return "comment";
|
||||||
if (type === 3) return "note";
|
if (
|
||||||
if (type !== undefined && [5, 6, 7].includes(type)) return "shape";
|
type === PdfAnnotationSubtype.CARET &&
|
||||||
if (type !== undefined && [4, 8].includes(type)) return "line";
|
(!toolId || toolId === "insertText" || toolId === "replaceText")
|
||||||
if (type === 13) return "stamp";
|
)
|
||||||
|
return "comment";
|
||||||
|
if (type === PdfAnnotationSubtype.FREETEXT) return "note";
|
||||||
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
SHAPE_SUBTYPES.includes(type as PdfAnnotationSubtype)
|
||||||
|
)
|
||||||
|
return "shape";
|
||||||
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
LINE_SUBTYPES.includes(type as PdfAnnotationSubtype)
|
||||||
|
)
|
||||||
|
return "line";
|
||||||
|
if (type === PdfAnnotationSubtype.STAMP) return "stamp";
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}, [annotation]);
|
}, [annotation]);
|
||||||
|
|
||||||
@@ -174,8 +219,12 @@ export function useAnnotationMenuHandlers({
|
|||||||
const currentColor = (() => {
|
const currentColor = (() => {
|
||||||
if (!obj) return "#000000";
|
if (!obj) return "#000000";
|
||||||
const type = obj.type;
|
const type = obj.type;
|
||||||
if (type === 3) return obj.textColor || obj.color || "#000000";
|
if (type === PdfAnnotationSubtype.FREETEXT)
|
||||||
if (type !== undefined && [4, 5, 6, 7, 8].includes(type))
|
return obj.textColor || obj.color || "#000000";
|
||||||
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
STROKE_COLOR_SUBTYPES.includes(type as PdfAnnotationSubtype)
|
||||||
|
)
|
||||||
return obj.strokeColor || obj.color || "#000000";
|
return obj.strokeColor || obj.color || "#000000";
|
||||||
return obj.color || obj.strokeColor || "#000000";
|
return obj.color || obj.strokeColor || "#000000";
|
||||||
})();
|
})();
|
||||||
@@ -247,17 +296,23 @@ export function useAnnotationMenuHandlers({
|
|||||||
patch.contents = obj?.contents ?? "";
|
patch.contents = obj?.contents ?? "";
|
||||||
} else {
|
} else {
|
||||||
patch.color = color;
|
patch.color = color;
|
||||||
if (type !== undefined && [9, 10, 11, 12].includes(type)) {
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
TEXT_MARKUP_SUBTYPES.includes(type as PdfAnnotationSubtype)
|
||||||
|
) {
|
||||||
patch.strokeColor = color;
|
patch.strokeColor = color;
|
||||||
patch.fillColor = color;
|
patch.fillColor = color;
|
||||||
patch.opacity = obj?.opacity ?? 1;
|
patch.opacity = obj?.opacity ?? 1;
|
||||||
}
|
}
|
||||||
if (type !== undefined && [4, 8].includes(type)) {
|
if (
|
||||||
|
type !== undefined &&
|
||||||
|
LINE_SUBTYPES.includes(type as PdfAnnotationSubtype)
|
||||||
|
) {
|
||||||
patch.strokeColor = color;
|
patch.strokeColor = color;
|
||||||
patch.strokeWidth = obj?.strokeWidth ?? obj?.lineWidth ?? 2;
|
patch.strokeWidth = obj?.strokeWidth ?? obj?.lineWidth ?? 2;
|
||||||
patch.lineWidth = obj?.lineWidth ?? obj?.strokeWidth ?? 2;
|
patch.lineWidth = obj?.lineWidth ?? obj?.strokeWidth ?? 2;
|
||||||
}
|
}
|
||||||
if (type === 15) {
|
if (type === PdfAnnotationSubtype.INK) {
|
||||||
patch.strokeColor = color;
|
patch.strokeColor = color;
|
||||||
patch.strokeWidth = obj?.strokeWidth ?? obj?.thickness ?? 2;
|
patch.strokeWidth = obj?.strokeWidth ?? obj?.thickness ?? 2;
|
||||||
patch.opacity = obj?.opacity ?? 1;
|
patch.opacity = obj?.opacity ?? 1;
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import React, { createContext, useContext, ReactNode, useRef } from "react";
|
import React, { createContext, useContext, ReactNode, useRef } from "react";
|
||||||
import type { AnnotationAPI } from "@app/components/viewer/viewerTypes";
|
import type {
|
||||||
|
AnnotationAPI,
|
||||||
|
AnnotationToolId,
|
||||||
|
} from "@app/components/viewer/viewerTypes";
|
||||||
|
|
||||||
interface AnnotationContextValue {
|
interface AnnotationContextValue {
|
||||||
annotationApiRef: React.RefObject<AnnotationAPI | null>;
|
annotationApiRef: React.RefObject<AnnotationAPI | null>;
|
||||||
|
/** Ref to the panel-level activateAnnotationTool function — updates React state so buttons highlight correctly. Populated by Annotate.tsx. */
|
||||||
|
activateAnnotationToolRef: React.RefObject<
|
||||||
|
((toolId: AnnotationToolId) => void) | null
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnnotationContext = createContext<AnnotationContextValue | undefined>(
|
const AnnotationContext = createContext<AnnotationContextValue | undefined>(
|
||||||
@@ -13,9 +20,13 @@ export const AnnotationProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const annotationApiRef = useRef<AnnotationAPI>(null);
|
const annotationApiRef = useRef<AnnotationAPI>(null);
|
||||||
|
const activateAnnotationToolRef = useRef<
|
||||||
|
((toolId: AnnotationToolId) => void) | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const value: AnnotationContextValue = {
|
const value: AnnotationContextValue = {
|
||||||
annotationApiRef,
|
annotationApiRef,
|
||||||
|
activateAnnotationToolRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useNavigation } from "@app/contexts/NavigationContext";
|
|||||||
import { useFileSelection } from "@app/contexts/FileContext";
|
import { useFileSelection } from "@app/contexts/FileContext";
|
||||||
import { BaseToolProps } from "@app/types/tool";
|
import { BaseToolProps } from "@app/types/tool";
|
||||||
import { useSignature } from "@app/contexts/SignatureContext";
|
import { useSignature } from "@app/contexts/SignatureContext";
|
||||||
|
import { useAnnotation as useAnnotationContext } from "@app/contexts/AnnotationContext";
|
||||||
import { ViewerContext, useViewer } from "@app/contexts/ViewerContext";
|
import { ViewerContext, useViewer } from "@app/contexts/ViewerContext";
|
||||||
import type {
|
import type {
|
||||||
AnnotationToolId,
|
AnnotationToolId,
|
||||||
@@ -85,6 +86,7 @@ const Annotate = (_props: BaseToolProps) => {
|
|||||||
placementPreviewSize,
|
placementPreviewSize,
|
||||||
setPlacementPreviewSize,
|
setPlacementPreviewSize,
|
||||||
} = useSignature();
|
} = useSignature();
|
||||||
|
const { activateAnnotationToolRef } = useAnnotationContext();
|
||||||
const viewerContext = useContext(ViewerContext);
|
const viewerContext = useContext(ViewerContext);
|
||||||
const viewerContextRef = useRef(viewerContext);
|
const viewerContextRef = useRef(viewerContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -375,6 +377,9 @@ const Annotate = (_props: BaseToolProps) => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep ref in sync so external callers (e.g. CommentsSidebar) get the latest closure
|
||||||
|
activateAnnotationToolRef.current = activateAnnotationTool;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// push style updates to EmbedPDF when sliders/colors change
|
// push style updates to EmbedPDF when sliders/colors change
|
||||||
if (activeTool === "stamp") {
|
if (activeTool === "stamp") {
|
||||||
|
|||||||
Reference in New Issue
Block a user