UNPKG

@anaralabs/lector

Version:

Headless PDF viewer for React

1,542 lines (1,527 loc) 161 kB
'use client'; import React, { createContext, forwardRef, useRef, useState, useCallback, useContext, useEffect, cloneElement, createRef, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions, inline, flip } from '@floating-ui/react'; import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual'; import { create, useStore, createStore } from 'zustand'; import { jsx, Fragment, jsxs } from 'react/jsx-runtime'; import { AnnotationLayer, TextLayer, getDocument } from 'pdfjs-dist'; import { useDebounce } from 'use-debounce'; import { v4 } from 'uuid'; import { Slot } from '@radix-ui/react-slot'; // src/components/annotation-tooltip.tsx // src/lib/clamp.ts var clamp = (value, min, max) => { return Math.min(Math.max(value, min), max); }; // src/lib/zoom.ts var getFitWidthZoom = (containerWidth, viewports, zoomOptions) => { const { minZoom, maxZoom } = zoomOptions; const maxPageWidth = Math.max(...viewports.map((viewport) => viewport.width)); const targetZoom = containerWidth / maxPageWidth; const clampedZoom = Math.min(Math.max(targetZoom, minZoom), maxZoom); return clampedZoom; }; var createZustandContext = (getStore) => { const Context = React.createContext(null); const Provider = (props) => { const [store] = React.useState(() => getStore(props.initialValue)); return /* @__PURE__ */ jsx(Context.Provider, { value: store, children: props.children }); }; return { useContext: () => React.useContext(Context), Context, Provider }; }; // src/internal.ts var PDFStore = createZustandContext( (initialState) => { return createStore((set, get) => ({ pdfDocumentProxy: initialState.pdfDocumentProxy, zoom: initialState.zoom, isZoomFitWidth: initialState.isZoomFitWidth ?? false, zoomOptions: { minZoom: initialState.zoomOptions?.minZoom ?? 0.5, maxZoom: initialState.zoomOptions?.maxZoom ?? 10 }, viewportRef: createRef(), viewports: initialState.viewports, updateZoom: (zoom, isZoomFitWidth = false) => { const { minZoom, maxZoom } = get().zoomOptions; set((state) => { if (typeof zoom === "function") { const newZoom2 = clamp(zoom(state.zoom), minZoom, maxZoom); return { zoom: newZoom2, isZoomFitWidth }; } const newZoom = clamp(zoom, minZoom, maxZoom); return { zoom: newZoom, isZoomFitWidth }; }); }, zoomFitWidth: () => { const { viewportRef, zoomOptions, viewports } = get(); if (!viewportRef.current) return; const clampedZoom = getFitWidthZoom( viewportRef.current.clientWidth, viewports, zoomOptions ); set({ zoom: clampedZoom, isZoomFitWidth: true }); return clampedZoom; }, currentPage: 1, setCurrentPage: (val) => { set({ currentPage: val }); }, isPinching: false, setIsPinching: (val) => { set({ isPinching: val }); }, virtualizer: null, setVirtualizer: (val) => { set({ virtualizer: val }); }, pageProxies: initialState.pageProxies, getPdfPageProxy: (pageNumber) => { const proxy = get().pageProxies[pageNumber - 1]; if (!proxy) throw new Error(`Page ${pageNumber} does not exist`); return proxy; }, textContent: [], setTextContent: (val) => { set({ textContent: val }); }, highlights: [], setHighlight: (val) => { set({ highlights: val }); }, customSelectionRects: [], setCustomSelectionRects: (val) => { set({ customSelectionRects: val }); }, coloredHighlights: [], addColoredHighlight: (value) => set((prevState) => ({ coloredHighlights: [...prevState.coloredHighlights, value] })), deleteColoredHighlight: (uuid) => set((prevState) => ({ coloredHighlights: prevState.coloredHighlights.filter( (rect) => rect.uuid !== uuid ) })) })); } ); var usePdf = (selector) => useStore(PDFStore.useContext(), selector); // src/hooks/useAnnotationTooltip.ts var useAnnotationTooltip = ({ annotation, onOpenChange, position = "top", isOpen: controlledIsOpen }) => { const isNewAnnotation = Date.now() - new Date(annotation.createdAt).getTime() < 1e3; const [isPositionCalculated, setIsPositionCalculated] = useState(false); const [isOpen, setIsOpen] = useState(false); const viewportRef = usePdf((state) => state.viewportRef); const scale = usePdf((state) => state.zoom); const effectiveIsOpen = (isOpen && isPositionCalculated || controlledIsOpen) ?? false; const { refs, floatingStyles, context } = useFloating({ placement: position, open: effectiveIsOpen, onOpenChange: (open) => { setIsOpen(open); onOpenChange?.(open); }, whileElementsMounted: autoUpdate, middleware: [ inline(), offset(10), flip({ crossAxis: false, fallbackAxisSideDirection: "end" }), shift({ padding: 8 }) ] }); const dismiss = useDismiss(context); const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]); const updateTooltipPosition = useCallback(() => { if (!annotation.highlights.length) { setIsPositionCalculated(false); return; } const highlightRects = annotation.highlights; let minLeft = Infinity; let maxRight = -Infinity; let minTop = Infinity; let maxBottom = -Infinity; const viewportElement = viewportRef.current; if (!viewportElement) { setIsPositionCalculated(false); return; } const pageElement = viewportElement.querySelector(`[data-page-number="${annotation.pageNumber}"]`); if (!pageElement) { setIsPositionCalculated(false); return; } refs.setReference({ getBoundingClientRect() { const pageRect = pageElement.getBoundingClientRect(); highlightRects.forEach((highlight) => { const scaledLeft = highlight.left * scale; const scaledWidth = highlight.width * scale; const scaledTop = highlight.top * scale; const scaledHeight = highlight.height * scale; const left = pageRect.left + scaledLeft; const right = left + scaledWidth; const top = pageRect.top + scaledTop; const bottom = top + scaledHeight; minLeft = Math.min(minLeft, left); maxRight = Math.max(maxRight, right); minTop = Math.min(minTop, top); maxBottom = Math.max(maxBottom, bottom); }); const width = maxRight - minLeft; const height = maxBottom - minTop; const centerX = minLeft + width / 2; const centerY = minTop + height / 2; const rect = { width, height, x: centerX - width / 2, y: centerY - height / 2, top: centerY - height / 2, right: centerX + width / 2, bottom: centerY + height / 2, left: centerX - width / 2 }; return rect; }, getClientRects() { return [this.getBoundingClientRect()]; }, contextElement: viewportRef.current || void 0 }); setIsPositionCalculated(true); if (isNewAnnotation) { setIsOpen(true); } }, [annotation.highlights, annotation.pageNumber, refs, viewportRef, scale, isNewAnnotation]); useEffect(() => { const viewport = viewportRef.current; setIsPositionCalculated(false); requestAnimationFrame(() => { updateTooltipPosition(); }); const handleScroll = () => { requestAnimationFrame(updateTooltipPosition); }; const handleResize = () => { requestAnimationFrame(updateTooltipPosition); }; if (viewport) { viewport.addEventListener("scroll", handleScroll, { passive: true }); } window.addEventListener("resize", handleResize, { passive: true }); return () => { if (viewport) { viewport.removeEventListener("scroll", handleScroll); } window.removeEventListener("resize", handleResize); }; }, [updateTooltipPosition, viewportRef, scale, controlledIsOpen]); return { isOpen: effectiveIsOpen, setIsOpen, refs, floatingStyles, getFloatingProps, getReferenceProps }; }; var AnnotationTooltip = ({ annotation, children, renderTooltipContent, hoverTooltipContent, onOpenChange, className, focusedOpenId, focusedHoverOpenId, hoverClassName, isOpen: controlledIsOpen, hoverIsOpen: controlledHoverIsOpen }) => { const viewportRef = usePdf((state) => state.viewportRef); const closeTimeoutRef = useRef(null); const isMouseInTooltipRef = useRef(false); const [triggeredPosition, setTriggeredPosition] = useState(); const { isOpen: uncontrolledIsOpen, setIsOpen, refs, floatingStyles, getFloatingProps, getReferenceProps } = useAnnotationTooltip({ annotation, onOpenChange, position: triggeredPosition, isOpen: controlledIsOpen }); const { isOpen: uncontrolledHoverIsOpen, setIsOpen: setHoverIsOpen, refs: hoverRefs, floatingStyles: hoverFloatingStyles, getFloatingProps: getHoverFloatingProps, getReferenceProps: getHoverReferenceProps } = useAnnotationTooltip({ position: "bottom", annotation, isOpen: controlledHoverIsOpen }); const isOpen = controlledIsOpen ?? uncontrolledIsOpen; const hoverIsOpen = controlledHoverIsOpen || uncontrolledHoverIsOpen; const handleClick = useCallback(() => { if (controlledIsOpen === void 0) { setIsOpen(!isOpen); } }, [controlledIsOpen, isOpen, setIsOpen]); const handleMouseEnter = useCallback(() => { if (focusedOpenId && focusedOpenId !== annotation.id) return; if (focusedHoverOpenId && focusedHoverOpenId !== annotation.id) return; if (hoverTooltipContent) { if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } setHoverIsOpen(true); } }, [hoverTooltipContent, setHoverIsOpen]); const closeTooltip = useCallback(() => { if (!isMouseInTooltipRef.current) { setHoverIsOpen(false); } }, [setHoverIsOpen]); const handleMouseLeave = useCallback(() => { if (!hoverTooltipContent) return; closeTimeoutRef.current = setTimeout(closeTooltip, 100); }, [hoverTooltipContent, closeTooltip]); const handleTooltipMouseEnter = useCallback(() => { if (focusedOpenId && focusedOpenId !== annotation.id) return; if (focusedHoverOpenId && focusedHoverOpenId !== annotation.id) return; isMouseInTooltipRef.current = true; if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } }, []); const handleTooltipMouseLeave = useCallback(() => { isMouseInTooltipRef.current = false; setHoverIsOpen(false); }, [setHoverIsOpen]); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "div", { ref: (node) => { refs.setReference(node); hoverRefs.setReference(node); }, onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...getReferenceProps(), ...getHoverReferenceProps(), children } ), isOpen && viewportRef.current && createPortal( /* @__PURE__ */ jsx( "div", { ref: refs.setFloating, className, "data-annotation-tooltip": "click", style: { ...floatingStyles, position: "absolute", pointerEvents: "auto", zIndex: 50 }, ...getFloatingProps(), children: renderTooltipContent({ annotation, onClose: () => setIsOpen(false), setPosition: (position) => setTriggeredPosition(position) }) } ), viewportRef.current ), !isOpen && hoverIsOpen && annotation.comment && hoverTooltipContent && viewportRef.current && createPortal( /* @__PURE__ */ jsx( "div", { ref: hoverRefs.setFloating, className: hoverClassName, "data-annotation-tooltip": "hover", style: { ...hoverFloatingStyles, position: "absolute", pointerEvents: "auto", zIndex: 51 }, onMouseEnter: handleTooltipMouseEnter, onMouseLeave: handleTooltipMouseLeave, ...getHoverFloatingProps(), children: hoverTooltipContent } ), viewportRef.current ) ] }); }; var useAnnotations = create((set) => ({ annotations: [], addAnnotation: (annotation) => set((state) => ({ annotations: [ ...state.annotations, annotation ] })), updateAnnotation: (id, updates) => set((state) => ({ annotations: state.annotations.map( (annotation) => annotation.id === id ? { ...annotation, ...updates } : annotation ) })), deleteAnnotation: (id) => set((state) => ({ annotations: state.annotations.filter((annotation) => annotation.id !== id) })), setAnnotations: (annotations) => set({ annotations }) })); var PDFPageNumberContext = createContext(0); var usePDFPageNumber = () => { return useContext(PDFPageNumberContext); }; var AnnotationHighlightLayer = ({ className, style, renderTooltipContent, renderHoverTooltipContent, tooltipClassName, highlightClassName, underlineClassName, commentIconPosition, commmentIcon, commentIconClassName, focusedAnnotationId, focusedHoverAnnotationId, onAnnotationClick, onAnnotationTooltipClose, hoverTooltipClassName }) => { const { annotations } = useAnnotations(); const pageNumber = usePDFPageNumber(); const pageAnnotations = annotations.filter( (annotation) => annotation.pageNumber === pageNumber ); const getCommentIconPosition = (highlights) => { if (!highlights.length) return { top: 0, right: 10 }; const sortedHighlights = [...highlights].sort((a, b) => { const topDiff = a.top - b.top; return Math.abs(topDiff) < 3 ? a.left - b.left : topDiff; }); const lines = []; let currentLine = []; sortedHighlights.forEach((highlight) => { if (currentLine.length === 0) { currentLine.push(highlight); } else { const firstInLine = currentLine[0]; if (Math.abs(highlight.top - firstInLine.top) <= 3) { currentLine.push(highlight); } else { lines.push([...currentLine]); currentLine = [highlight]; } } }); if (currentLine.length > 0) { lines.push(currentLine); } const PAGE_WIDTH = 600; const hasLongLine = lines.some((line) => { if (line.length === 0) return false; const rightmost2 = Math.max(...line.map((h) => h.left + h.width)); return rightmost2 > PAGE_WIDTH * 0.8; }); const firstHighlight = highlights[0]; const firstLine = lines[0] || []; const leftmost = Math.min(...firstLine.map((h) => h.left)); const rightmost = Math.max(...firstLine.map((h) => h.left + h.width)); const lineCenter = leftmost + (rightmost - leftmost) / 2; const shouldPositionRight = hasLongLine || lineCenter > PAGE_WIDTH * 0.5; const rightPosition = commentIconPosition === "highlight" ? { left: rightmost + 8 } : { right: 10 }; const leftPosition = commentIconPosition === "highlight" ? { left: leftmost - 18 } : { left: 20 }; return { top: firstHighlight.top + firstHighlight.height / 2 - 6, ...shouldPositionRight ? rightPosition : leftPosition }; }; return /* @__PURE__ */ jsx("div", { className, style, children: pageAnnotations.map((annotation) => { return /* @__PURE__ */ jsx( AnnotationTooltip, { annotation, className: tooltipClassName, hoverClassName: hoverTooltipClassName, focusedOpenId: focusedAnnotationId, focusedHoverOpenId: focusedHoverAnnotationId, isOpen: focusedAnnotationId === annotation.id, hoverIsOpen: focusedHoverAnnotationId === annotation.id, onOpenChange: (open) => { if (open && onAnnotationClick) { onAnnotationClick(annotation); } else if (!open && onAnnotationTooltipClose) { onAnnotationTooltipClose(annotation); } }, renderTooltipContent, hoverTooltipContent: renderHoverTooltipContent({ annotation, onClose: () => { } }), children: /* @__PURE__ */ jsxs( "div", { style: { cursor: "pointer" }, onClick: () => onAnnotationClick?.(annotation), children: [ annotation.highlights.map((highlight, index) => /* @__PURE__ */ jsx( "div", { className: highlightClassName, style: { position: "absolute", top: highlight.top, left: highlight.left, width: highlight.width, height: highlight.height, backgroundColor: annotation.color }, "data-highlight-id": annotation.id }, `highlight-${index}` )), annotation.comment && annotation.underlines?.map((rect, index) => /* @__PURE__ */ jsx( "div", { className: underlineClassName, style: { position: "absolute", top: rect.top, left: rect.left, width: rect.width, height: 1.1, backgroundColor: annotation.borderColor }, "data-comment-id": annotation.id }, `underline-${index}` )), annotation.comment && commmentIcon && /* @__PURE__ */ jsx( "div", { className: commentIconClassName, style: { position: "absolute", ...getCommentIconPosition(annotation.highlights), color: "gray", cursor: "pointer", zIndex: 10 }, "data-comment-icon-id": annotation.id, children: commmentIcon } ) ] } ) }, annotation.id ); }) }); }; // ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs function r(e) { var t, f, n = ""; if ("string" == typeof e || "number" == typeof e) n += e; else if ("object" == typeof e) if (Array.isArray(e)) { var o = e.length; for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f); } else for (f in e) e[f] && (n && (n += " "), n += f); return n; } function clsx() { for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t); return n; } var clsx_default = clsx; // src/lib/cancellable.ts var cancellable = (promise) => { let isCancelled = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then( (value) => { if (!isCancelled) { resolve(value); } }, (error) => { if (!isCancelled) { reject(error); } } ); }); return { promise: wrappedPromise, cancel() { isCancelled = true; } }; }; // src/hooks/pages/usePdfJump.tsx var usePdfJump = () => { const virtualizer = usePdf((state) => state.virtualizer); const setHighlight = usePdf((state) => state.setHighlight); const jumpToPage = (pageIndex, options) => { if (!virtualizer) return; const defaultOptions = { align: "start", behavior: "smooth" }; const finalOptions = { ...defaultOptions, ...options }; virtualizer.scrollToIndex(pageIndex - 1, finalOptions); }; const jumpToOffset = (offset3) => { if (!virtualizer) return; virtualizer.scrollToOffset(offset3, { align: "start", behavior: "smooth" }); }; const scrollToHighlightRects = (rects, type, align = "start", additionalOffset = 0) => { if (!virtualizer) return; const firstPage = Math.min(...rects.map((rect) => rect.pageNumber)); const pageOffset = virtualizer.getOffsetForIndex(firstPage - 1, "start"); if (pageOffset === null) return; const targetRect = rects.find((rect) => rect.pageNumber === firstPage); if (!targetRect) return; const isNumber = pageOffset?.[0] != null; if (!isNumber) return; const pageStart = pageOffset[0] ?? 0; let rectTop; let rectHeight; if (type === "percent") { const estimatePageHeight = virtualizer.options.estimateSize( firstPage - 1 ); rectTop = targetRect.top / 100 * estimatePageHeight; rectHeight = targetRect.height / 100 * estimatePageHeight; } else { rectTop = targetRect.top; rectHeight = targetRect.height; } let scrollOffset; if (align === "center") { const viewportHeight = virtualizer.scrollElement?.clientHeight || 0; const rectCenter = pageStart + rectTop + rectHeight / 2; scrollOffset = rectCenter - viewportHeight / 2; } else { scrollOffset = pageStart + rectTop; } scrollOffset += additionalOffset; const adjustedOffset = Math.max(0, scrollOffset); virtualizer.scrollToOffset(adjustedOffset, { align: "start", // Always use start when we've calculated our own centering behavior: "smooth" }); }; const jumpToHighlightRects = (rects, type, align = "start", additionalOffset = 0) => { if (!virtualizer) return; setHighlight(rects); scrollToHighlightRects(rects, type, align, additionalOffset); }; return { jumpToPage, jumpToOffset, jumpToHighlightRects, scrollToHighlightRects }; }; var LinkService = class { _pdfDocumentProxy; externalLinkEnabled = true; isInPresentationMode = false; _currentPageNumber = 0; _pageNavigationCallback; get pdfDocumentProxy() { if (!this._pdfDocumentProxy) { throw new Error("pdfDocumentProxy is not set"); } return this._pdfDocumentProxy; } constructor() { } get pagesCount() { return this._pdfDocumentProxy?.numPages || 0; } get page() { return this._currentPageNumber; } set page(value) { this._currentPageNumber = value; if (this._pageNavigationCallback) { this._pageNavigationCallback(value); } } // Required for link annotations to work setDocument(pdfDocument) { this._pdfDocumentProxy = pdfDocument; } setViewer() { } getDestinationHash(dest) { if (!dest) return ""; const destRef = dest[0]; if (dest.length > 1 && typeof dest[1] === "object" && dest[1] !== null && "url" in dest[1]) { const urlDest = dest[1]; return urlDest.url; } if (destRef && typeof destRef === "object") { if ("num" in destRef) { const numRef = destRef; return `#page=${numRef.num + 1}`; } if ("gen" in destRef) { const genRef = destRef; const refNum = genRef.num ?? 0; return `#page=${refNum + 1}`; } } if (typeof destRef === "number") { return `#page=${destRef + 1}`; } return `#dest-${String(dest)}`; } getAnchorUrl(hash) { if (hash.startsWith("http://") || hash.startsWith("https://")) { return hash; } return `#${hash}`; } addLinkAttributes(link, url, newWindow) { if (!link) return; const isExternalLink = url.startsWith("http://") || url.startsWith("https://"); if (isExternalLink && this.externalLinkEnabled) { link.href = url; link.target = newWindow === false ? "" : "_blank"; link.rel = "noopener noreferrer"; } else if (!isExternalLink) { link.href = url; link.target = ""; } else { link.href = "#"; link.target = ""; } } async goToDestination(dest) { let explicitDest; if (typeof dest === "string") { explicitDest = await this.pdfDocumentProxy.getDestination(dest); } else if (Array.isArray(dest)) { explicitDest = dest; } else { explicitDest = await dest; } if (!explicitDest) { return; } if (explicitDest.length > 1 && typeof explicitDest[1] === "object" && explicitDest[1] !== null && "url" in explicitDest[1]) { return; } const destRef = explicitDest[0]; let pageIndex; if (destRef && typeof destRef === "object") { if ("num" in destRef) { try { const refProxy = destRef; pageIndex = await this.pdfDocumentProxy.getPageIndex(refProxy); } catch (_error) { return; } } else { return; } } else if (typeof destRef === "number") { pageIndex = destRef; } else { return; } const pageNumber = pageIndex + 1; if (this._pageNavigationCallback) { this._pageNavigationCallback(pageNumber); } } executeNamedAction() { } navigateTo(dest) { this.goToDestination(dest); } get rotation() { return 0; } set rotation(value) { } goToPage(pageNumber) { if (pageNumber >= 1 && pageNumber <= this.pagesCount) { if (this._pageNavigationCallback) { this._pageNavigationCallback(pageNumber); } } } setHash(hash) { if (hash.startsWith("#page=")) { const pageNumber = parseInt(hash.substring(6), 10); if (!isNaN(pageNumber)) { this.goToPage(pageNumber); } } } executeSetOCGState() { } // Method to register navigation callback registerPageNavigationCallback(callback) { this._pageNavigationCallback = callback; } // Method to unregister navigation callback unregisterPageNavigationCallback() { this._pageNavigationCallback = void 0; } }; var defaultLinkService = new LinkService(); var PDFLinkServiceContext = createContext(defaultLinkService); var usePDFLinkService = () => { return useContext(PDFLinkServiceContext); }; var useVisibility = ({ elementRef }) => { const [visible, setVisible] = useState(false); useEffect(() => { if (!elementRef.current) { return; } const observer = new IntersectionObserver(([entry]) => { setVisible(entry?.isIntersecting ?? false); }); observer.observe(elementRef.current); return () => { observer.disconnect(); }; }, [elementRef]); return { visible }; }; // src/hooks/layers/useAnnotationLayer.tsx var defaultAnnotationLayerParams = { renderForms: true, externalLinksEnabled: true, jumpOptions: { behavior: "smooth", align: "start" } }; var useAnnotationLayer = (params) => { const mergedParams = { ...defaultAnnotationLayerParams, ...params }; const annotationLayerRef = useRef(null); const annotationLayerObjectRef = useRef(null); const linkService = usePDFLinkService(); const { visible } = useVisibility({ elementRef: annotationLayerRef }); const pageNumber = usePDFPageNumber(); const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber)); const pdfDocumentProxy = usePdf((state) => state.pdfDocumentProxy); useEffect(() => { linkService.externalLinkEnabled = mergedParams.externalLinksEnabled; }, [linkService, mergedParams.externalLinksEnabled]); const { jumpToPage } = usePdfJump(); useEffect(() => { if (!jumpToPage) return; const handlePageNavigation = (pageNumber2) => { jumpToPage(pageNumber2, mergedParams.jumpOptions); }; linkService.registerPageNavigationCallback(handlePageNavigation); return () => { linkService.unregisterPageNavigationCallback(); }; }, [jumpToPage, linkService, mergedParams.jumpOptions]); useEffect(() => { const style = document.createElement("style"); style.textContent = ` .annotationLayer { position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; opacity: 1; z-index: 3; } .annotationLayer section { position: absolute; } .annotationLayer .linkAnnotation > a, .annotationLayer .buttonWidgetAnnotation.pushButton > a { position: absolute; font-size: 1em; top: 0; left: 0; width: 100%; height: 100%; background: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7") 0 0 repeat; cursor: pointer; } .annotationLayer .linkAnnotation > a:hover, .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover { opacity: 0.2; background: rgba(255, 255, 0, 1); box-shadow: 0 2px 10px rgba(255, 255, 0, 1); } `; document.head.appendChild(style); return () => { document.head.removeChild(style); }; }, []); useEffect(() => { if (!annotationLayerRef.current) return; const element = annotationLayerRef.current; const handleLinkClick = (e) => { if (!e.target || !(e.target instanceof HTMLAnchorElement)) return; const target = e.target; const href = target.getAttribute("href") || ""; if (href.startsWith("#page=")) { e.preventDefault(); const pageNumber2 = parseInt(href.substring(6), 10); if (!isNaN(pageNumber2)) { linkService.goToPage(pageNumber2); } } }; element.addEventListener("click", handleLinkClick); return () => { element.removeEventListener("click", handleLinkClick); }; }, [linkService]); useEffect(() => { if (!annotationLayerRef.current) { return; } if (visible) { annotationLayerRef.current.style.contentVisibility = "visible"; } else { annotationLayerRef.current.style.contentVisibility = "hidden"; } }, [visible]); useEffect(() => { if (!annotationLayerRef.current || !pdfPageProxy || !pdfDocumentProxy) { return; } if (linkService._pdfDocumentProxy !== pdfDocumentProxy) { linkService.setDocument(pdfDocumentProxy); } annotationLayerRef.current.innerHTML = ""; annotationLayerRef.current.className = "annotationLayer"; const viewport = pdfPageProxy.getViewport({ scale: 1 }); const annotationLayerConfig = { div: annotationLayerRef.current, viewport, page: pdfPageProxy, linkService, accessibilityManager: void 0, annotationCanvasMap: void 0, annotationEditorUIManager: void 0, structTreeLayer: void 0 }; const annotationLayer = new AnnotationLayer(annotationLayerConfig); annotationLayerObjectRef.current = annotationLayer; const { cancel } = cancellable( (async () => { try { const annotations = await pdfPageProxy.getAnnotations(); await annotationLayer.render({ ...annotationLayerConfig, ...mergedParams, annotations, linkService }); } catch (_error) { } })() ); return () => { cancel(); }; }, [pdfPageProxy, pdfDocumentProxy, mergedParams, linkService]); return { annotationLayerRef }; }; var AnnotationLayer2 = ({ renderForms = true, externalLinksEnabled = true, jumpOptions = { behavior: "smooth", align: "start" }, className, style, ...props }) => { const { annotationLayerRef } = useAnnotationLayer({ renderForms, externalLinksEnabled, jumpOptions }); return /* @__PURE__ */ jsx( "div", { className: clsx_default("annotationLayer", className), style: { ...style, position: "absolute", top: 0, left: 0 }, ...props, ref: annotationLayerRef } ); }; var useDpr = () => { const [dpr, setDPR] = useState( !window ? 1 : Math.min(window.devicePixelRatio, 2) ); useEffect(() => { if (!window) { return; } const handleDPRChange = () => { setDPR(window.devicePixelRatio); }; const windowMatch = window.matchMedia( `screen and (min-resolution: ${dpr}dppx)` ); windowMatch.addEventListener("change", handleDPRChange); return () => { windowMatch.removeEventListener("change", handleDPRChange); }; }, []); return dpr; }; // src/hooks/layers/useCanvasLayer.tsx var useCanvasLayer = ({ background }) => { const canvasRef = useRef(null); const pageNumber = usePDFPageNumber(); const dpr = useDpr(); const bouncyZoom = usePdf((state) => state.zoom); const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber)); const [zoom] = useDebounce(bouncyZoom, 100); useLayoutEffect(() => { if (!canvasRef.current) { return; } const viewport = pdfPageProxy.getViewport({ scale: 1 }); const canvas = canvasRef.current; const scale = dpr * zoom; canvas.height = viewport.height * scale; canvas.width = viewport.width * scale; canvas.style.height = `${viewport.height}px`; canvas.style.width = `${viewport.width}px`; const canvasContext = canvas.getContext("2d"); canvasContext.scale(scale, scale); const renderingTask = pdfPageProxy.render({ canvasContext, viewport, background }); renderingTask.promise.catch((error) => { if (error.name === "RenderingCancelledException") { return; } throw error; }); return () => { void renderingTask.cancel(); }; }, [pdfPageProxy, dpr, zoom]); return { canvasRef }; }; var CanvasLayer = ({ style, background, ...props }) => { const { canvasRef } = useCanvasLayer({ background }); return /* @__PURE__ */ jsx( "canvas", { style: { ...style, position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }, ...props, ref: canvasRef } ); }; // src/hooks/useSelectionDimensions.tsx var MERGE_THRESHOLD = 2; var shouldMergeRects = (rect1, rect2) => { const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height); const horizontallyConnected = Math.abs(rect1.left + rect1.width - rect2.left) <= MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) <= MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width; return verticalOverlap && horizontallyConnected; }; var consolidateHighlightRects = (rects) => { if (rects.length <= 1) return rects; const consolidated = []; const sorted = [...rects].sort((a, b) => { const pageCompare = a.pageNumber - b.pageNumber; if (pageCompare !== 0) return pageCompare; const topDiff = a.top - b.top; return Math.abs(topDiff) < 2 ? a.left - b.left : topDiff; }); let current = sorted[0]; if (!current) return rects; for (let i = 1; i < sorted.length; i++) { const next = sorted[i]; if (!next) continue; const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < Math.max(current.height, next.height) * 0.5; const horizontallyConnected = samePageAndLine && // Adjacent (touching or very close) (Math.abs(current.left + current.width - next.left) <= MERGE_THRESHOLD || Math.abs(next.left + next.width - current.left) <= MERGE_THRESHOLD || // Overlapping current.left < next.left + next.width && next.left < current.left + current.width || // Very close (small gap) Math.abs(current.left + current.width - next.left) <= current.height * 0.2); if (horizontallyConnected) { const newLeft = Math.min(current.left, next.left); const newRight = Math.max(current.left + current.width, next.left + next.width); const newTop = Math.min(current.top, next.top); const newBottom = Math.max(current.top + current.height, next.top + next.height); current = { left: newLeft, top: newTop, width: newRight - newLeft, height: newBottom - newTop, pageNumber: current.pageNumber }; } else { consolidated.push(current); current = next; } } if (current) { consolidated.push(current); } return consolidated; }; var consolidateRects = (rects) => { if (rects.length <= 1) return rects; const result = []; const visited = /* @__PURE__ */ new Set(); for (let i = 0; i < rects.length; i++) { if (visited.has(i)) continue; const currentRect = rects[i]; if (!currentRect) continue; const currentGroup = [currentRect]; visited.add(i); let foundNew = true; while (foundNew) { foundNew = false; for (let j = 0; j < rects.length; j++) { if (visited.has(j)) continue; const candidateRect = rects[j]; if (!candidateRect) continue; const shouldMergeWithGroup = currentGroup.some( (groupRect) => doRectsOverlap(groupRect, candidateRect) ); if (shouldMergeWithGroup) { currentGroup.push(candidateRect); visited.add(j); foundNew = true; } } } result.push(mergeRectGroup(currentGroup)); } return result; }; var doRectsOverlap = (rect1, rect2) => { const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width; const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height; const closeEnough = shouldMergeRects(rect1, rect2); return horizontalOverlap && verticalOverlap || closeEnough; }; var mergeRectGroup = (rects) => { if (rects.length === 1) { const rect = rects[0]; if (!rect) throw new Error("Invalid rect in group"); return rect; } const firstRect = rects[0]; if (!firstRect) throw new Error("Invalid first rect in group"); let minLeft = firstRect.left; let minTop = firstRect.top; let maxRight = firstRect.left + firstRect.width; let maxBottom = firstRect.top + firstRect.height; rects.forEach((rect) => { if (!rect) return; minLeft = Math.min(minLeft, rect.left); minTop = Math.min(minTop, rect.top); maxRight = Math.max(maxRight, rect.left + rect.width); maxBottom = Math.max(maxBottom, rect.top + rect.height); }); return { left: minLeft, top: minTop, width: maxRight - minLeft, height: maxBottom - minTop, pageNumber: firstRect.pageNumber }; }; var useSelectionDimensions = () => { const store = PDFStore.useContext(); const getAnnotationDimension = () => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; const range = selection.getRangeAt(0); const highlightRects = []; const underlineRects = []; const textLayerMapHighlight = /* @__PURE__ */ new Map(); const textLayerMapUnderline = /* @__PURE__ */ new Map(); const clientRects = Array.from(range.getClientRects()).filter( (rect) => rect.width > 2 && rect.height > 2 ); clientRects.forEach((clientRect) => { let element = document.elementFromPoint( clientRect.left + 1, clientRect.top + clientRect.height / 2 ); if (!element) { element = document.elementFromPoint( clientRect.left + clientRect.width / 2, clientRect.top + clientRect.height / 2 ); } if (!element) { element = document.elementFromPoint( clientRect.right - 1, clientRect.top + clientRect.height / 2 ); } if (!element) { element = document.elementFromPoint( clientRect.left + clientRect.width / 2, clientRect.top + 1 ); } if (!element) { element = document.elementFromPoint( clientRect.left + clientRect.width / 2, clientRect.bottom - 1 ); } const textLayer = element?.closest(".textLayer"); if (!textLayer) return; const isSuperOrSubScript = (el) => { if (!el) return false; if (el.tagName.toLowerCase() === "sup" || el.tagName.toLowerCase() === "sub") { return true; } const classes = el.className; if (typeof classes === "string") { const superSubClasses = ["superscript", "subscript", "sup", "sub"]; if (superSubClasses.some((c) => classes.includes(c))) { return true; } } const elementRect = el.getBoundingClientRect(); if (elementRect.height < 6 && elementRect.width < 15) { const textContent = el.textContent?.trim() || ""; if (textContent.length <= 2 && /^[\d\w]{1,2}$/.test(textContent)) { const parentRect = el.parentElement?.getBoundingClientRect(); if (parentRect && parentRect.height > elementRect.height * 2) { const elementCenter = elementRect.top + elementRect.height / 2; const parentCenter = parentRect.top + parentRect.height / 2; const verticalOffset = Math.abs(elementCenter - parentCenter); if (verticalOffset > parentRect.height * 0.4) { return true; } } } } return false; }; const pageNumber = parseInt( textLayer.getAttribute("data-page-number") || "1", 10 ); const textLayerRect = textLayer.getBoundingClientRect(); const zoom = store.getState().zoom; const highlightRect = { width: clientRect.width / zoom, height: clientRect.height / zoom, top: (clientRect.top - textLayerRect.top) / zoom, left: (clientRect.left - textLayerRect.left) / zoom, pageNumber }; if (!textLayerMapHighlight.has(pageNumber)) { textLayerMapHighlight.set(pageNumber, []); } textLayerMapHighlight.get(pageNumber)?.push(highlightRect); const shouldCreateUnderline = !isSuperOrSubScript(element); if (shouldCreateUnderline) { const baselineOffset = clientRect.height * 0.85; const underlineHeight = 2; const underlineRect = { width: clientRect.width / zoom, height: underlineHeight / zoom, top: (clientRect.top - textLayerRect.top + baselineOffset) / zoom, left: (clientRect.left - textLayerRect.left) / zoom, pageNumber }; if (!textLayerMapUnderline.has(pageNumber)) { textLayerMapUnderline.set(pageNumber, []); } textLayerMapUnderline.get(pageNumber)?.push(underlineRect); } }); textLayerMapHighlight.forEach((rects) => { if (rects.length > 0) { if (rects.length === 1) { highlightRects.push(...rects); } else { const consolidated = consolidateHighlightRects(rects); highlightRects.push(...consolidated); } } }); textLayerMapUnderline.forEach((rects) => { if (rects.length > 0) { const lineGroups = groupRectsByLine(rects); lineGroups.forEach((group) => { if (group.length === 0) return; group.sort((a, b) => a.left - b.left); let i = 0; while (i < group.length) { const startRect = group[i]; if (!startRect) { i++; continue; } let endIndex = i; while (endIndex + 1 < group.length) { const currentRect = group[endIndex]; const nextRect = group[endIndex + 1]; if (!currentRect || !nextRect) break; const gap = nextRect.left - (currentRect.left + currentRect.width); const maxGapAllowed = Math.max(MERGE_THRESHOLD, currentRect.height * 0.3); if (gap <= maxGapAllowed) { endIndex++; } else { break; } } const endRect = group[endIndex]; if (!endRect) { i++; continue; } const lineRect = { width: endRect.left + endRect.width - startRect.left, height: 1.5, top: startRect.top, left: startRect.left, pageNumber: startRect.pageNumber }; underlineRects.push(lineRect); i = endIndex + 1; } }); } }); if (underlineRects.length === 0 && highlightRects.length > 0) { highlightRects.forEach((highlightRect) => { const baselineOffset = highlightRect.height * 0.85; const underlineHeight = 1.5; const underlineRect = { width: highlightRect.width, height: underlineHeight, top: highlightRect.top + baselineOffset, left: highlightRect.left, pageNumber: highlightRect.pageNumber }; underlineRects.push(underlineRect); }); } return { highlights: highlightRects.sort((a, b) => a.pageNumber - b.pageNumber), underlines: consolidateUnderlines(underlineRects).sort((a, b) => a.pageNumber - b.pageNumber), text: range.toString().trim(), isCollapsed: false }; }; const groupRectsByLine = (rects) => { const VERTICAL_TOLERANCE = 3; const groups = []; rects.forEach((rect) => { const centerY = rect.top + rect.height / 2; let foundGroup = false; for (const group of groups) { if (group.length === 0) continue; const firstRect = group[0]; if (!firstRect) continue; const groupCenterY = firstRect.top + firstRect.height / 2; if (Math.abs(centerY - groupCenterY) <= VERTICAL_TOLERANCE) { group.push(rect); foundGroup = true; break; } } if (!foundGroup) { groups.push([rect]); } }); return groups; }; const consolidateUnderlines = (underlines) => { if (underlines.length <= 1) return underlines; const consolidated = []; const sorted = [...underlines].sort((a, b) => { const pageCompare = a.pageNumber - b.pageNumber; if (pageCompare !== 0) return pageCompare; const topCompare = a.top - b.top; return Math.abs(topCompare) < 1 ? a.left - b.left : topCompare; }); let current = sorted[0]; for (let i = 1; i < sorted.length; i++) { const next = sorted[i]; const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < 1; const horizontallyConnected = samePageAndLine && (Math.abs(current.left + current.width - next.left) <= MERGE_THRESHOLD || current.left < next.left + next.width && next.left < current.left + current.width); if (horizontallyConnected) { const newWidth = Math.max(current.left + current.width, next.left + next.width) - Math.min(current.left, next.left); current = { ...current, left: Math.min(current.left, next.left), width: newWidth }; } else { consolidated.push(current); current = next; } } consolidated.push(current); return consolidated; }; const getDimension = () => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; const range = selection.getRangeAt(0); const highlights = []; const textLayerMap = /* @__PURE__ */ new Map(); const clientRects = Array.from(range.getClientRects()).filter( (rect) => rect.width > 2 && rect.height > 2 ); clientRects.forEach((clientRect) => { const element = document.elementFromPoint( clientRect.left + 1, clientRect.top + clientRect.height / 2 ); const textLayer = element?.closest(".textLayer"); if (!textLayer) return; const pageNumber = parseInt( textLayer.getAttribute("data-page-number") || "1", 10 ); const textLayerRect = textLayer.getBoundingClientRect(); const zoom = store.getState().zoom; const rect = { width: clientRect.width / zoom, height: clientRect.height / zoom, top: (clientRect.top - textLayerRect.top) / zoom, left: (clientRect.left - textLayerRect.left) / zoom, pageNumber }; if (!textLayerMap.has(pageNumber)) { textLayerMap.set(pageNumber, []); } textLayerMap.get(pageNumber)?.push(rect); }); textLayerMap.forEach((rects) => { if (rects.length > 0) { const consolidated = consolidateRects(rects); highlights.push(...consolidated); } }); return { highlights: highlights.sort((a, b) => a.pageNumber - b.pageNumber), text: range.toString().trim(), isCollapsed: false }; }; const getSelection = () => getDimension(); return { getDimension, getSelection, getAnnotationDimension }; }; var SelectionTooltip = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const lastSelectionRef = useRef(null); const viewportRef = usePdf((state) => state.viewportRef); const { refs, floatingStyles, context } = useFloating({ placement: "bottom", open: isOpen, onOpenChange: setIsOpen, whileElementsMounted: autoUpdate, middleware: [offset(10), shift({ padding: 8 })] }); const dismiss = useDismiss(context); const { getFloatingProps } = useInteractions([dismiss]); const updateTooltipPosition = useCallback(() => { const selection = document.getSelection(); if (!selection || selection.isCollapsed) { setIsOpen(false); lastSelecti