Wire HtmlViewerAdapter into ViewerShell for HTML/Markdown documents
UploadDropzone accepts HTML/MD files; ViewerShell routes by representationType; PendingSelection uses SelectionCapture union.
This commit is contained in:
parent
0cd2ca5d04
commit
589b8356b4
5 changed files with 208 additions and 95 deletions
|
|
@ -30,7 +30,7 @@ import {
|
||||||
restoreFromStorage,
|
restoreFromStorage,
|
||||||
type Engine,
|
type Engine,
|
||||||
} from "@engine/index";
|
} from "@engine/index";
|
||||||
import type { PdfSelectionCapture } from "@citation-evidence/evidence-anchor";
|
import type { SelectionCapture } from "@citation-evidence/evidence-anchor";
|
||||||
import { createPdfByteStore, type PdfByteStore } from "@source/index";
|
import { createPdfByteStore, type PdfByteStore } from "@source/index";
|
||||||
import { useContext as useReactContext } from "react";
|
import { useContext as useReactContext } from "react";
|
||||||
import { SessionInternalContext } from "./SessionContextInternal";
|
import { SessionInternalContext } from "./SessionContextInternal";
|
||||||
|
|
@ -60,7 +60,7 @@ function activeDocumentKeyFor(sessionId: SessionId | null): string {
|
||||||
* `null` means "no selection waiting for a comment".
|
* `null` means "no selection waiting for a comment".
|
||||||
*/
|
*/
|
||||||
export interface PendingSelection {
|
export interface PendingSelection {
|
||||||
readonly capture: PdfSelectionCapture;
|
readonly capture: SelectionCapture;
|
||||||
readonly selectors: readonly Selector[];
|
readonly selectors: readonly Selector[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,10 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
const pendingOrder = useMemo<number>(() => {
|
const pendingOrder = useMemo<number>(() => {
|
||||||
if (!pending) return Number.POSITIVE_INFINITY;
|
if (!pending) return Number.POSITIVE_INFINITY;
|
||||||
const c = pending.capture;
|
const c = pending.capture;
|
||||||
return c.page * 1000 + (c.boundingRect?.y ?? 0);
|
if (c.kind === "pdf") {
|
||||||
|
return c.page * 1000 + (c.boundingRect?.y ?? 0);
|
||||||
|
}
|
||||||
|
return c.startOffset;
|
||||||
}, [pending]);
|
}, [pending]);
|
||||||
|
|
||||||
// Find the insert position for the pending capture form: first index
|
// Find the insert position for the pending capture form: first index
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* UploadDropzone — drag-drop + file-picker for uploading PDFs into the
|
* UploadDropzone — drag-drop + file-picker for uploading documents into the
|
||||||
* active session.
|
* active session (PDF, HTML, Markdown).
|
||||||
*
|
|
||||||
* On every successful drop:
|
|
||||||
* 1. read each File as bytes,
|
|
||||||
* 2. run the source-layer `ingestPdfFromFile` (mints the blob URL
|
|
||||||
* via the session's `PdfByteStore`),
|
|
||||||
* 3. register the resulting `{document, representation}` with the
|
|
||||||
* engine,
|
|
||||||
* 4. activate the most-recently-uploaded document.
|
|
||||||
*
|
|
||||||
* Failures (non-PDFs, ingest errors) are surfaced inline above the
|
|
||||||
* dropzone; the caller doesn't need a separate toast for them.
|
|
||||||
*
|
|
||||||
* ── Extraction decision (CWORK-WP-0001 T03) ──────────────────────────
|
|
||||||
* This component lives in `citation-work` rather than the umbrella app.
|
|
||||||
* Its only dependencies are `@source` (PDF ingestion) and the local
|
|
||||||
* engine/session hooks — both inside the review-workspace boundary — so
|
|
||||||
* hosting it here lets the package render a complete review shell,
|
|
||||||
* including the upload affordance, without any umbrella-local imports.
|
|
||||||
* `ReviewShell` still exposes an `upload` slot so a consumer may swap in
|
|
||||||
* its own affordance; when the slot is omitted, this default is used.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { ingestPdfFromFile } from "@source/index";
|
import {
|
||||||
|
ingestHtmlFromFile,
|
||||||
|
ingestMarkdownFromFile,
|
||||||
|
ingestPdfFromFile,
|
||||||
|
} from "@source/index";
|
||||||
import {
|
import {
|
||||||
useActiveDocumentId,
|
useActiveDocumentId,
|
||||||
useEngine,
|
useEngine,
|
||||||
usePdfByteStore,
|
usePdfByteStore,
|
||||||
} from "./EngineContext";
|
} from "./EngineContext";
|
||||||
|
|
||||||
|
type UploadFormat = "pdf" | "html" | "markdown" | "unsupported";
|
||||||
|
|
||||||
interface UploadEntry {
|
interface UploadEntry {
|
||||||
readonly file: File;
|
readonly file: File;
|
||||||
status: "queued" | "uploading" | "done" | "error";
|
status: "queued" | "uploading" | "done" | "error";
|
||||||
|
|
@ -43,6 +29,22 @@ export interface UploadDropzoneProps {
|
||||||
readonly onUploaded?: (documentId: import("@shared/ids").DocumentId) => void;
|
readonly onUploaded?: (documentId: import("@shared/ids").DocumentId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectFormat(file: File): UploadFormat {
|
||||||
|
const name = file.name.toLowerCase();
|
||||||
|
if (file.type === "application/pdf" || name.endsWith(".pdf")) return "pdf";
|
||||||
|
if (file.type === "text/html" || name.endsWith(".html") || name.endsWith(".htm")) {
|
||||||
|
return "html";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
file.type === "text/markdown"
|
||||||
|
|| name.endsWith(".md")
|
||||||
|
|| name.endsWith(".markdown")
|
||||||
|
) {
|
||||||
|
return "markdown";
|
||||||
|
}
|
||||||
|
return "unsupported";
|
||||||
|
}
|
||||||
|
|
||||||
export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
const engine = useEngine();
|
const engine = useEngine();
|
||||||
const byteStore = usePdfByteStore();
|
const byteStore = usePdfByteStore();
|
||||||
|
|
@ -55,14 +57,15 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
async (files: readonly File[]) => {
|
async (files: readonly File[]) => {
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
const initial: UploadEntry[] = files.map((file) => {
|
const initial: UploadEntry[] = files.map((file) => {
|
||||||
const isPdf =
|
const format = detectFormat(file);
|
||||||
file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
|
if (format === "unsupported") {
|
||||||
if (isPdf) return { file, status: "queued" };
|
return {
|
||||||
return {
|
file,
|
||||||
file,
|
status: "error",
|
||||||
status: "error",
|
error: "Unsupported format (PDF, HTML, or Markdown only)",
|
||||||
error: "Not a PDF (only application/pdf accepted)",
|
};
|
||||||
};
|
}
|
||||||
|
return { file, status: "queued" };
|
||||||
});
|
});
|
||||||
setEntries((prev) => [...prev, ...initial]);
|
setEntries((prev) => [...prev, ...initial]);
|
||||||
|
|
||||||
|
|
@ -72,14 +75,16 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
entry.status = "uploading";
|
entry.status = "uploading";
|
||||||
setEntries((prev) => [...prev]);
|
setEntries((prev) => [...prev]);
|
||||||
try {
|
try {
|
||||||
const { document, representation } = await ingestPdfFromFile(
|
const format = detectFormat(entry.file);
|
||||||
entry.file,
|
const ingested = await (format === "html"
|
||||||
byteStore,
|
? ingestHtmlFromFile(entry.file, byteStore)
|
||||||
);
|
: format === "markdown"
|
||||||
engine.documents.register({ document, representation });
|
? ingestMarkdownFromFile(entry.file, byteStore)
|
||||||
|
: ingestPdfFromFile(entry.file, byteStore));
|
||||||
|
engine.documents.register(ingested);
|
||||||
entry.status = "done";
|
entry.status = "done";
|
||||||
lastDocumentId = document.id;
|
lastDocumentId = ingested.document.id;
|
||||||
onUploaded?.(document.id);
|
onUploaded?.(ingested.document.id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
entry.status = "error";
|
entry.status = "error";
|
||||||
entry.error = err instanceof Error ? err.message : String(err);
|
entry.error = err instanceof Error ? err.message : String(err);
|
||||||
|
|
@ -118,7 +123,6 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files ? Array.from(e.target.files) : [];
|
const files = e.target.files ? Array.from(e.target.files) : [];
|
||||||
void processFiles(files);
|
void processFiles(files);
|
||||||
// Reset so the same filename can be picked again.
|
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
},
|
},
|
||||||
[processFiles],
|
[processFiles],
|
||||||
|
|
@ -131,7 +135,7 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={onDragLeave}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="PDF upload"
|
aria-label="Document upload"
|
||||||
style={{
|
style={{
|
||||||
border: `2px dashed ${isOver ? "#0050b3" : "#bbb"}`,
|
border: `2px dashed ${isOver ? "#0050b3" : "#bbb"}`,
|
||||||
background: isOver ? "#e8f0ff" : "#fafafa",
|
background: isOver ? "#e8f0ff" : "#fafafa",
|
||||||
|
|
@ -142,7 +146,7 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>Drop PDF files here</div>
|
<div>Drop PDF, HTML, or Markdown files here</div>
|
||||||
<div style={{ margin: "6px 0", color: "#888" }}>or</div>
|
<div style={{ margin: "6px 0", color: "#888" }}>or</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -156,12 +160,12 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Choose PDF…
|
Choose file…
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/pdf,.pdf"
|
accept="application/pdf,.pdf,text/html,.html,.htm,text/markdown,.md,.markdown"
|
||||||
multiple
|
multiple
|
||||||
onChange={onPicked}
|
onChange={onPicked}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
|
|
@ -195,4 +199,4 @@ export function UploadDropzone({ onUploaded }: UploadDropzoneProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
47
src/work/ViewerShell.dom.test.tsx
Normal file
47
src/work/ViewerShell.dom.test.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { createEngine, type Engine } from "@engine/index";
|
||||||
|
import { ingestHtmlFromFile } from "@source/index";
|
||||||
|
import { EngineProvider, useActiveDocumentId, usePdfByteStore } from "./EngineContext";
|
||||||
|
import { ViewerShell } from "./ViewerShell";
|
||||||
|
|
||||||
|
function BootstrapHtmlDoc({
|
||||||
|
engine,
|
||||||
|
file,
|
||||||
|
}: {
|
||||||
|
readonly engine: Engine;
|
||||||
|
readonly file: File;
|
||||||
|
}) {
|
||||||
|
const byteStore = usePdfByteStore();
|
||||||
|
const { setId } = useActiveDocumentId();
|
||||||
|
useEffect(() => {
|
||||||
|
void ingestHtmlFromFile(file, byteStore).then(({ document, representation }) => {
|
||||||
|
engine.documents.register({ document, representation });
|
||||||
|
setId(document.id);
|
||||||
|
});
|
||||||
|
}, [byteStore, engine, file, setId]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ViewerShell (dom)", () => {
|
||||||
|
it("renders HtmlViewerAdapter content for html-dom documents", async () => {
|
||||||
|
const engine = createEngine();
|
||||||
|
const file = new File(
|
||||||
|
["<!DOCTYPE html><html><body><p>Dom viewer smoke test</p></body></html>"],
|
||||||
|
"sample.html",
|
||||||
|
{ type: "text/html" },
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EngineProvider engine={engine}>
|
||||||
|
<BootstrapHtmlDoc engine={engine} file={file} />
|
||||||
|
<ViewerShell />
|
||||||
|
</EngineProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByText("Dom viewer smoke test")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* ViewerShell — the centre pane.
|
* ViewerShell — the centre pane.
|
||||||
*
|
*
|
||||||
* Hosts the viewer adapter (currently the T02 PDF spike) and shows whatever
|
* Hosts the format-appropriate viewer adapter (PDF or HTML/Markdown) and
|
||||||
* is active. `work/` consumes only the adapter's public surface
|
* surfaces selection + highlight activation events to the review shell.
|
||||||
* (`PdfSpikeViewer`) — it never touches PDF.js or react-pdf-highlighter-plus
|
|
||||||
* directly. When the PDF library is swapped (or the spike is replaced),
|
|
||||||
* only the adapter module changes; this shell stays the same.
|
|
||||||
*
|
|
||||||
* The annotation toolbar lived here in earlier iterations; CE-WP-0005-iter4
|
|
||||||
* moved it into the evidence sidebar so the capture form appears in the
|
|
||||||
* sidebar's document-flow position. The viewer now only renders the PDF
|
|
||||||
* and surfaces the activate/click events.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { PdfSpikeViewer, type StoredAnnotation } from "@citation-evidence/evidence-anchor";
|
import {
|
||||||
import { resolvePdfViewerUrl } from "@source/pdf/viewer-url";
|
HtmlViewerAdapter,
|
||||||
|
PdfSpikeViewer,
|
||||||
|
type StoredAnnotation,
|
||||||
|
} from "@citation-evidence/evidence-anchor";
|
||||||
|
import { resolveDomViewerHtml, resolvePdfViewerUrl } from "@source/index";
|
||||||
import type { AnnotationId } from "@shared/ids";
|
import type { AnnotationId } from "@shared/ids";
|
||||||
import {
|
import {
|
||||||
useActiveDocument,
|
useActiveDocument,
|
||||||
|
|
@ -41,8 +37,6 @@ export function ViewerShell() {
|
||||||
const [hideXfaLayer] = useDebugFlag("hideXfaLayer");
|
const [hideXfaLayer] = useDebugFlag("hideXfaLayer");
|
||||||
const activeEvidenceId = useLastActivatedEvidence();
|
const activeEvidenceId = useLastActivatedEvidence();
|
||||||
|
|
||||||
// The viewer needs to re-fetch its highlight list whenever annotations
|
|
||||||
// change. The tick is included in the memo deps so the list re-resolves.
|
|
||||||
const annotationTick = useEngineEventTick("AnnotationCreated");
|
const annotationTick = useEngineEventTick("AnnotationCreated");
|
||||||
const annotationUpdateTick = useEngineEventTick("AnnotationUpdated");
|
const annotationUpdateTick = useEngineEventTick("AnnotationUpdated");
|
||||||
|
|
||||||
|
|
@ -55,19 +49,28 @@ export function ViewerShell() {
|
||||||
}));
|
}));
|
||||||
}, [document, engine, annotationTick, annotationUpdateTick]);
|
}, [document, engine, annotationTick, annotationUpdateTick]);
|
||||||
|
|
||||||
// The annotation id that visually represents the "active" focus —
|
|
||||||
// derived from the active evidence's first annotation.
|
|
||||||
const activeAnnotationId = useMemo<AnnotationId | null>(() => {
|
const activeAnnotationId = useMemo<AnnotationId | null>(() => {
|
||||||
if (!activeEvidenceId) return null;
|
if (!activeEvidenceId) return null;
|
||||||
const item = engine.evidence.get(activeEvidenceId);
|
const item = engine.evidence.get(activeEvidenceId);
|
||||||
return item?.annotationIds[0] ?? null;
|
return item?.annotationIds[0] ?? null;
|
||||||
}, [activeEvidenceId, engine]);
|
}, [activeEvidenceId, engine]);
|
||||||
|
|
||||||
const fileUrl = useMemo(() => {
|
const pdfUrl = useMemo(() => {
|
||||||
if (!document) return null;
|
if (!document) return null;
|
||||||
return resolvePdfViewerUrl(document, byteStore);
|
return resolvePdfViewerUrl(document, byteStore);
|
||||||
}, [document, byteStore]);
|
}, [document, byteStore]);
|
||||||
|
|
||||||
|
const domHtml = useMemo(() => {
|
||||||
|
if (!document || !representation) return null;
|
||||||
|
if (
|
||||||
|
representation.representationType !== "html-dom"
|
||||||
|
&& representation.representationType !== "markdown-rendered"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolveDomViewerHtml(document, representation, byteStore);
|
||||||
|
}, [document, representation, byteStore]);
|
||||||
|
|
||||||
const scrollRequestKey =
|
const scrollRequestKey =
|
||||||
scrollToId !== null ? `${scrollToId}:${scrollVersion}` : null;
|
scrollToId !== null ? `${scrollToId}:${scrollVersion}` : null;
|
||||||
|
|
||||||
|
|
@ -80,14 +83,19 @@ export function ViewerShell() {
|
||||||
);
|
);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
engine.evidence.activate(item.id, "citation-card");
|
engine.evidence.activate(item.id, "citation-card");
|
||||||
// Re-trigger scroll so a click on the highlight also keeps it
|
|
||||||
// centred in the viewport.
|
|
||||||
scrollTo(annotationId as AnnotationId);
|
scrollTo(annotationId as AnnotationId);
|
||||||
},
|
},
|
||||||
[document, engine, scrollTo],
|
[document, engine, scrollTo],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!document || !representation || !fileUrl) {
|
const handleSelectionCaptured = useCallback(
|
||||||
|
(capture: import("@citation-evidence/evidence-anchor").SelectionCapture, selectors: import("@shared/selector").Selector[]) => {
|
||||||
|
setPending({ capture, selectors });
|
||||||
|
},
|
||||||
|
[setPending],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!document || !representation) {
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -99,7 +107,46 @@ export function ViewerShell() {
|
||||||
fontFamily: "system-ui, sans-serif",
|
fontFamily: "system-ui, sans-serif",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upload a PDF on the left to begin.
|
Upload a document on the left to begin.
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPdf = representation.representationType === "pdf-text";
|
||||||
|
const isDom =
|
||||||
|
representation.representationType === "html-dom"
|
||||||
|
|| representation.representationType === "markdown-rendered";
|
||||||
|
|
||||||
|
if (isPdf && !pdfUrl) {
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#666",
|
||||||
|
fontFamily: "system-ui, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
PDF bytes not available — re-upload the document.
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDom && !domHtml) {
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#666",
|
||||||
|
fontFamily: "system-ui, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Document bytes not available — re-upload the file.
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -115,33 +162,45 @@ export function ViewerShell() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ flex: 1, overflow: "hidden", position: "relative" }}>
|
<div style={{ flex: 1, overflow: "hidden", position: "relative" }}>
|
||||||
<PdfSpikeViewer
|
{isPdf && pdfUrl ? (
|
||||||
// Re-key on document + debug flags only — scroll requests must
|
<PdfSpikeViewer
|
||||||
// not remount the viewer (that re-fetches the PDF blob).
|
key={[
|
||||||
key={[
|
document.id,
|
||||||
document.id,
|
debugTextLayer ? "d" : "n",
|
||||||
debugTextLayer ? "d" : "n",
|
hideCanvas ? "hc" : "",
|
||||||
hideCanvas ? "hc" : "",
|
hideTextLayer ? "ht" : "",
|
||||||
hideTextLayer ? "ht" : "",
|
hideAnnotationLayer ? "ha" : "",
|
||||||
hideAnnotationLayer ? "ha" : "",
|
hideXfaLayer ? "hx" : "",
|
||||||
hideXfaLayer ? "hx" : "",
|
].join("#")}
|
||||||
].join("#")}
|
pdfUrl={pdfUrl}
|
||||||
pdfUrl={fileUrl}
|
representation={representation}
|
||||||
storedAnnotations={annotations}
|
storedAnnotations={annotations}
|
||||||
{...(scrollToId ? { scrollToAnnotationId: scrollToId } : {})}
|
{...(scrollToId ? { scrollToAnnotationId: scrollToId } : {})}
|
||||||
{...(scrollRequestKey ? { scrollRequestKey } : {})}
|
{...(scrollRequestKey ? { scrollRequestKey } : {})}
|
||||||
activeAnnotationId={activeAnnotationId}
|
activeAnnotationId={activeAnnotationId}
|
||||||
onHighlightClicked={handleHighlightClicked}
|
onHighlightClicked={handleHighlightClicked}
|
||||||
debugTextLayer={debugTextLayer}
|
debugTextLayer={debugTextLayer}
|
||||||
hideCanvas={hideCanvas}
|
hideCanvas={hideCanvas}
|
||||||
hideTextLayer={hideTextLayer}
|
hideTextLayer={hideTextLayer}
|
||||||
hideAnnotationLayer={hideAnnotationLayer}
|
hideAnnotationLayer={hideAnnotationLayer}
|
||||||
hideXfaLayer={hideXfaLayer}
|
hideXfaLayer={hideXfaLayer}
|
||||||
onSelectionCaptured={(capture, selectors) => {
|
onSelectionCaptured={handleSelectionCaptured}
|
||||||
setPending({ capture, selectors });
|
/>
|
||||||
}}
|
) : null}
|
||||||
/>
|
{isDom && domHtml ? (
|
||||||
|
<HtmlViewerAdapter
|
||||||
|
key={document.id}
|
||||||
|
html={domHtml}
|
||||||
|
representation={representation}
|
||||||
|
storedAnnotations={annotations}
|
||||||
|
{...(scrollToId ? { scrollToAnnotationId: scrollToId } : {})}
|
||||||
|
{...(scrollRequestKey ? { scrollRequestKey } : {})}
|
||||||
|
activeAnnotationId={activeAnnotationId}
|
||||||
|
onHighlightClicked={handleHighlightClicked}
|
||||||
|
onSelectionCaptured={handleSelectionCaptured}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue