@anaralabs/lector 3.5.7 → 3.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,10 +46,7 @@ export declare const AnnotationHighlightLayer: ({ className, style, renderToolti
46
46
  declare interface AnnotationHighlightLayerProps {
47
47
  className?: string;
48
48
  style?: React.CSSProperties;
49
- renderTooltipContent: (props: {
50
- annotation: Annotation;
51
- onClose: () => void;
52
- }) => React.ReactNode;
49
+ renderTooltipContent: (props: AnnotationTooltipContentProps) => React.ReactNode;
53
50
  renderHoverTooltipContent: (props: {
54
51
  annotation: Annotation;
55
52
  onClose: () => void;
@@ -100,12 +97,18 @@ declare interface AnnotationState {
100
97
  setAnnotations: (annotations: Annotation[]) => void;
101
98
  }
102
99
 
103
- export declare const AnnotationTooltip: ({ annotation, children, tooltipContent, hoverTooltipContent, onOpenChange, className, hoverClassName, isOpen: controlledIsOpen, tooltipBubbleSize, hoverIsOpen: controlledHoverIsOpen, }: AnnotationTooltipProps) => JSX.Element;
100
+ export declare const AnnotationTooltip: ({ annotation, children, renderTooltipContent, hoverTooltipContent, onOpenChange, className, hoverClassName, isOpen: controlledIsOpen, tooltipBubbleSize, hoverIsOpen: controlledHoverIsOpen, }: AnnotationTooltipProps) => JSX.Element;
101
+
102
+ export declare type AnnotationTooltipContentProps = {
103
+ annotation: Annotation;
104
+ onClose: () => void;
105
+ setPosition?: (position: 'top' | 'bottom' | undefined) => void;
106
+ };
104
107
 
105
108
  declare interface AnnotationTooltipProps {
106
109
  annotation: Annotation;
107
110
  children: React.ReactNode;
108
- tooltipContent: React.ReactNode;
111
+ renderTooltipContent: (props: AnnotationTooltipContentProps) => React.ReactNode;
109
112
  hoverTooltipContent?: React.ReactNode;
110
113
  onOpenChange?: (open: boolean) => void;
111
114
  isOpen?: boolean;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { AnnotationTooltipContentProps } from './_tsup-dts-rollup.js';
1
2
  export { AnnotationTooltip } from './_tsup-dts-rollup.js';
2
3
  export { AnnotationHighlightLayer } from './_tsup-dts-rollup.js';
3
4
  export { AnnotationLayer } from './_tsup-dts-rollup.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, forwardRef, useRef, useCallback, useContext, useState, useEffect, cloneElement, createRef, useLayoutEffect } from 'react';
3
+ import React, { createContext, forwardRef, useRef, useState, useCallback, useContext, useEffect, cloneElement, createRef, useLayoutEffect } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions, inline, flip } from '@floating-ui/react';
6
6
  import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual';
@@ -270,7 +270,7 @@ var useAnnotationTooltip = ({
270
270
  var AnnotationTooltip = ({
271
271
  annotation,
272
272
  children,
273
- tooltipContent,
273
+ renderTooltipContent,
274
274
  hoverTooltipContent,
275
275
  onOpenChange,
276
276
  className,
@@ -282,6 +282,7 @@ var AnnotationTooltip = ({
282
282
  const viewportRef = usePdf((state) => state.viewportRef);
283
283
  const closeTimeoutRef = useRef(null);
284
284
  const isMouseInTooltipRef = useRef(false);
285
+ const [triggeredPosition, setTriggeredPosition] = useState();
285
286
  const {
286
287
  isOpen: uncontrolledIsOpen,
287
288
  setIsOpen,
@@ -292,6 +293,7 @@ var AnnotationTooltip = ({
292
293
  } = useAnnotationTooltip({
293
294
  annotation,
294
295
  onOpenChange,
296
+ position: triggeredPosition,
295
297
  isOpen: controlledIsOpen
296
298
  });
297
299
  const {
@@ -383,7 +385,11 @@ var AnnotationTooltip = ({
383
385
  zIndex: 50
384
386
  },
385
387
  ...getFloatingProps(),
386
- children: tooltipContent
388
+ children: renderTooltipContent({
389
+ annotation,
390
+ onClose: () => setIsOpen(false),
391
+ setPosition: (position) => setTriggeredPosition(position)
392
+ })
387
393
  }
388
394
  ),
389
395
  viewportRef.current
@@ -468,11 +474,7 @@ var AnnotationHighlightLayer = ({
468
474
  onAnnotationClick(annotation);
469
475
  }
470
476
  },
471
- tooltipContent: renderTooltipContent({
472
- annotation,
473
- onClose: () => {
474
- }
475
- }),
477
+ renderTooltipContent,
476
478
  hoverTooltipContent: renderHoverTooltipContent({
477
479
  annotation,
478
480
  onClose: () => {
@@ -1053,39 +1055,69 @@ var shouldMergeRects = (rect1, rect2) => {
1053
1055
  const horizontallyClose = 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;
1054
1056
  return verticalOverlap && horizontallyClose;
1055
1057
  };
1056
- var mergeRects = (rect1, rect2) => {
1057
- return {
1058
- left: Math.min(rect1.left, rect2.left),
1059
- top: Math.min(rect1.top, rect2.top),
1060
- width: Math.max(rect1.left + rect1.width, rect2.left + rect2.width) - Math.min(rect1.left, rect2.left),
1061
- height: Math.max(rect1.top + rect1.height, rect2.top + rect2.height) - Math.min(rect1.top, rect2.top),
1062
- pageNumber: rect1.pageNumber
1063
- };
1064
- };
1065
1058
  var consolidateRects = (rects) => {
1066
1059
  if (rects.length <= 1) return rects;
1067
- const sortedRects = [...rects].sort((a, b) => a.top - b.top);
1068
- let hasChanges;
1069
- do {
1070
- hasChanges = false;
1071
- let currentRect = sortedRects[0];
1072
- const tempResult = [];
1073
- for (let i = 1; i < sortedRects.length; i++) {
1074
- const sorted = sortedRects[i];
1075
- if (!currentRect || !sorted) continue;
1076
- if (shouldMergeRects(currentRect, sorted)) {
1077
- currentRect = mergeRects(currentRect, sorted);
1078
- hasChanges = true;
1079
- } else {
1080
- tempResult.push(currentRect);
1081
- currentRect = sorted;
1060
+ const result = [];
1061
+ const visited = /* @__PURE__ */ new Set();
1062
+ for (let i = 0; i < rects.length; i++) {
1063
+ if (visited.has(i)) continue;
1064
+ const currentRect = rects[i];
1065
+ if (!currentRect) continue;
1066
+ const currentGroup = [currentRect];
1067
+ visited.add(i);
1068
+ let foundNew = true;
1069
+ while (foundNew) {
1070
+ foundNew = false;
1071
+ for (let j = 0; j < rects.length; j++) {
1072
+ if (visited.has(j)) continue;
1073
+ const candidateRect = rects[j];
1074
+ if (!candidateRect) continue;
1075
+ const shouldMergeWithGroup = currentGroup.some(
1076
+ (groupRect) => doRectsOverlap(groupRect, candidateRect)
1077
+ );
1078
+ if (shouldMergeWithGroup) {
1079
+ currentGroup.push(candidateRect);
1080
+ visited.add(j);
1081
+ foundNew = true;
1082
+ }
1082
1083
  }
1083
1084
  }
1084
- if (currentRect) tempResult.push(currentRect);
1085
- sortedRects.length = 0;
1086
- sortedRects.push(...tempResult);
1087
- } while (hasChanges);
1088
- return sortedRects;
1085
+ result.push(mergeRectGroup(currentGroup));
1086
+ }
1087
+ return result;
1088
+ };
1089
+ var doRectsOverlap = (rect1, rect2) => {
1090
+ const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
1091
+ const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height;
1092
+ const closeEnough = shouldMergeRects(rect1, rect2);
1093
+ return horizontalOverlap && verticalOverlap || closeEnough;
1094
+ };
1095
+ var mergeRectGroup = (rects) => {
1096
+ if (rects.length === 1) {
1097
+ const rect = rects[0];
1098
+ if (!rect) throw new Error("Invalid rect in group");
1099
+ return rect;
1100
+ }
1101
+ const firstRect = rects[0];
1102
+ if (!firstRect) throw new Error("Invalid first rect in group");
1103
+ let minLeft = firstRect.left;
1104
+ let minTop = firstRect.top;
1105
+ let maxRight = firstRect.left + firstRect.width;
1106
+ let maxBottom = firstRect.top + firstRect.height;
1107
+ rects.forEach((rect) => {
1108
+ if (!rect) return;
1109
+ minLeft = Math.min(minLeft, rect.left);
1110
+ minTop = Math.min(minTop, rect.top);
1111
+ maxRight = Math.max(maxRight, rect.left + rect.width);
1112
+ maxBottom = Math.max(maxBottom, rect.top + rect.height);
1113
+ });
1114
+ return {
1115
+ left: minLeft,
1116
+ top: minTop,
1117
+ width: maxRight - minLeft,
1118
+ height: maxBottom - minTop,
1119
+ pageNumber: firstRect.pageNumber
1120
+ };
1089
1121
  };
1090
1122
  var useSelectionDimensions = () => {
1091
1123
  const store = PDFStore.useContext();
@@ -1125,7 +1157,8 @@ var useSelectionDimensions = () => {
1125
1157
  });
1126
1158
  textLayerMap.forEach((rects) => {
1127
1159
  if (rects.length > 0) {
1128
- highlights.push(...consolidateRects(rects));
1160
+ const consolidated = consolidateRects(rects);
1161
+ highlights.push(...consolidated);
1129
1162
  }
1130
1163
  });
1131
1164
  return {
@@ -1520,24 +1553,171 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
1520
1553
  }) });
1521
1554
  });
1522
1555
  HighlightLayer.displayName = "HighlightLayer";
1556
+ var createTextSelectionManager = () => {
1557
+ const textLayers = /* @__PURE__ */ new Map();
1558
+ let selectionChangeAbortController = null;
1559
+ let isPointerDown = false;
1560
+ let prevRange = null;
1561
+ let isFirefox;
1562
+ const removeGlobalSelectionListener = (textLayerDiv) => {
1563
+ textLayers.delete(textLayerDiv);
1564
+ if (textLayers.size === 0) {
1565
+ selectionChangeAbortController?.abort();
1566
+ selectionChangeAbortController = null;
1567
+ }
1568
+ };
1569
+ const enableGlobalSelectionListener = () => {
1570
+ if (selectionChangeAbortController) {
1571
+ return;
1572
+ }
1573
+ selectionChangeAbortController = new AbortController();
1574
+ const { signal } = selectionChangeAbortController;
1575
+ const reset = (endDiv, textLayer) => {
1576
+ if (endDiv.parentNode !== textLayer) {
1577
+ textLayer.appendChild(endDiv);
1578
+ }
1579
+ endDiv.style.width = "";
1580
+ endDiv.style.height = "";
1581
+ textLayer.classList.remove("selecting");
1582
+ };
1583
+ document.addEventListener("pointerdown", () => {
1584
+ isPointerDown = true;
1585
+ }, { signal });
1586
+ document.addEventListener("pointerup", () => {
1587
+ isPointerDown = false;
1588
+ textLayers.forEach(reset);
1589
+ }, { signal });
1590
+ window.addEventListener("blur", () => {
1591
+ isPointerDown = false;
1592
+ textLayers.forEach(reset);
1593
+ }, { signal });
1594
+ document.addEventListener("keyup", () => {
1595
+ if (!isPointerDown) {
1596
+ textLayers.forEach(reset);
1597
+ }
1598
+ }, { signal });
1599
+ document.addEventListener("selectionchange", () => {
1600
+ const selection = document.getSelection();
1601
+ if (!selection || selection.rangeCount === 0) {
1602
+ textLayers.forEach(reset);
1603
+ return;
1604
+ }
1605
+ const activeTextLayers = /* @__PURE__ */ new Set();
1606
+ for (let i = 0; i < selection.rangeCount; i++) {
1607
+ const range = selection.getRangeAt(i);
1608
+ for (const textLayerDiv of textLayers.keys()) {
1609
+ if (!activeTextLayers.has(textLayerDiv) && range.intersectsNode(textLayerDiv)) {
1610
+ activeTextLayers.add(textLayerDiv);
1611
+ }
1612
+ }
1613
+ }
1614
+ for (const [textLayerDiv, endDiv] of textLayers) {
1615
+ if (activeTextLayers.has(textLayerDiv)) {
1616
+ textLayerDiv.classList.add("selecting");
1617
+ } else {
1618
+ reset(endDiv, textLayerDiv);
1619
+ }
1620
+ }
1621
+ if (isFirefox === void 0) {
1622
+ const firstTextLayer = textLayers.keys().next().value;
1623
+ if (firstTextLayer) {
1624
+ isFirefox = getComputedStyle(firstTextLayer).getPropertyValue("-moz-user-select") === "none";
1625
+ }
1626
+ }
1627
+ if (isFirefox) {
1628
+ return;
1629
+ }
1630
+ try {
1631
+ const range = selection.getRangeAt(0);
1632
+ const modifyStart = prevRange && (range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 || range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0);
1633
+ let anchor = modifyStart ? range.startContainer : range.endContainer;
1634
+ if (anchor.nodeType === Node.TEXT_NODE) {
1635
+ anchor = anchor.parentNode;
1636
+ }
1637
+ const parentTextLayer = anchor.parentElement?.closest(".textLayer");
1638
+ const endDiv = textLayers.get(parentTextLayer);
1639
+ if (endDiv && parentTextLayer) {
1640
+ endDiv.style.width = parentTextLayer.style.width;
1641
+ endDiv.style.height = parentTextLayer.style.height;
1642
+ const insertTarget = modifyStart ? anchor : anchor.nextSibling;
1643
+ if (anchor.parentElement && insertTarget) {
1644
+ anchor.parentElement.insertBefore(endDiv, insertTarget);
1645
+ }
1646
+ }
1647
+ prevRange = range.cloneRange();
1648
+ } catch {
1649
+ }
1650
+ }, { signal });
1651
+ };
1652
+ const bindMouseEvents2 = (textLayerDiv, endOfContent) => {
1653
+ if (textLayerDiv._textSelectionBound) {
1654
+ return;
1655
+ }
1656
+ textLayerDiv._textSelectionBound = true;
1657
+ textLayers.set(textLayerDiv, endOfContent);
1658
+ enableGlobalSelectionListener();
1659
+ const handleMouseDown = () => {
1660
+ textLayerDiv.classList.add("selecting");
1661
+ };
1662
+ textLayerDiv.addEventListener("mousedown", handleMouseDown);
1663
+ textLayerDiv._cleanupTextSelection = () => {
1664
+ textLayerDiv.removeEventListener("mousedown", handleMouseDown);
1665
+ removeGlobalSelectionListener(textLayerDiv);
1666
+ delete textLayerDiv._textSelectionBound;
1667
+ };
1668
+ };
1669
+ return bindMouseEvents2;
1670
+ };
1671
+ var bindMouseEvents = createTextSelectionManager();
1523
1672
  var useTextLayer = () => {
1524
1673
  const textContainerRef = useRef(null);
1674
+ const textLayerRef = useRef(null);
1675
+ const isRenderingRef = useRef(false);
1525
1676
  const pageNumber = usePDFPageNumber();
1526
1677
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
1527
1678
  useEffect(() => {
1528
- if (!textContainerRef.current) {
1679
+ const textContainer = textContainerRef.current;
1680
+ if (!textContainer || isRenderingRef.current) {
1529
1681
  return;
1530
1682
  }
1683
+ isRenderingRef.current = true;
1684
+ textContainer.innerHTML = "";
1685
+ if (textLayerRef.current) {
1686
+ textLayerRef.current.cancel();
1687
+ textLayerRef.current = null;
1688
+ }
1531
1689
  const textLayer = new TextLayer({
1532
1690
  textContentSource: pdfPageProxy.streamTextContent(),
1533
- container: textContainerRef.current,
1691
+ container: textContainer,
1534
1692
  viewport: pdfPageProxy.getViewport({ scale: 1 })
1535
1693
  });
1536
- void textLayer.render();
1694
+ textLayerRef.current = textLayer;
1695
+ textLayer.render().then(() => {
1696
+ if (textLayerRef.current === textLayer && textContainer) {
1697
+ const endOfContent = document.createElement("div");
1698
+ endOfContent.className = "endOfContent";
1699
+ textContainer.appendChild(endOfContent);
1700
+ bindMouseEvents(textContainer, endOfContent);
1701
+ }
1702
+ }).catch((error) => {
1703
+ if (error.name !== "AbortException") {
1704
+ console.error("TextLayer rendering error:", error);
1705
+ }
1706
+ }).finally(() => {
1707
+ isRenderingRef.current = false;
1708
+ });
1537
1709
  return () => {
1538
- textLayer.cancel();
1710
+ isRenderingRef.current = false;
1711
+ if (textLayerRef.current) {
1712
+ textLayerRef.current.cancel();
1713
+ textLayerRef.current = null;
1714
+ }
1715
+ if (textContainer?._cleanupTextSelection) {
1716
+ textContainer._cleanupTextSelection();
1717
+ delete textContainer._cleanupTextSelection;
1718
+ }
1539
1719
  };
1540
- }, [pdfPageProxy]);
1720
+ }, [pdfPageProxy.pageNumber]);
1541
1721
  return {
1542
1722
  textContainerRef,
1543
1723
  pageNumber: pdfPageProxy.pageNumber
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.5.7",
3
+ "version": "3.5.9",
4
4
  "description": "Headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",