@anaralabs/lector 3.5.8 → 3.5.10

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.
@@ -24,40 +24,40 @@ import { Virtualizer } from '@tanstack/react-virtual';
24
24
  declare interface Annotation {
25
25
  id: string;
26
26
  pageNumber: number;
27
- highlights: Array<{
28
- height: number;
29
- left: number;
30
- top: number;
31
- width: number;
32
- pageNumber: number;
33
- }>;
27
+ highlights: HighlightRect_alias_2[];
28
+ underlines?: HighlightRect_alias_2[];
34
29
  color: string;
35
30
  borderColor: string;
31
+ comment?: string;
36
32
  createdAt: Date;
37
33
  updatedAt: Date;
38
- comment?: string;
39
34
  metadata?: Record<string, unknown>;
35
+ isCommentPending?: boolean;
40
36
  }
41
37
  export { Annotation }
42
38
  export { Annotation as Annotation_alias_1 }
43
39
 
44
- export declare const AnnotationHighlightLayer: ({ className, style, renderTooltipContent, renderHoverTooltipContent, tooltipClassName, highlightClassName, focusedAnnotationId, focusedHoverAnnotationId, onAnnotationClick, hoverTooltipClassName, tooltipBubbleSize, }: AnnotationHighlightLayerProps) => JSX.Element;
40
+ export declare const AnnotationHighlightLayer: ({ className, style, renderTooltipContent, renderHoverTooltipContent, tooltipClassName, highlightClassName, underlineClassName, commentIconPosition, commmentIcon, commentIconClassName, focusedAnnotationId, focusedHoverAnnotationId, onAnnotationClick, onAnnotationTooltipClose, hoverTooltipClassName, }: AnnotationHighlightLayerProps) => JSX.Element;
45
41
 
46
42
  declare interface AnnotationHighlightLayerProps {
47
43
  className?: string;
48
- style?: React.CSSProperties;
49
- renderTooltipContent: (props: AnnotationTooltipContentProps) => React.ReactNode;
44
+ style?: React_2.CSSProperties;
45
+ renderTooltipContent: (props: AnnotationTooltipContentProps) => React_2.ReactNode;
50
46
  renderHoverTooltipContent: (props: {
51
47
  annotation: Annotation;
52
48
  onClose: () => void;
53
- }) => React.ReactNode;
49
+ }) => React_2.ReactNode;
54
50
  focusedAnnotationId?: string;
51
+ commmentIcon?: React_2.ReactNode;
55
52
  focusedHoverAnnotationId?: string;
56
53
  onAnnotationClick?: (annotation: Annotation) => void;
54
+ onAnnotationTooltipClose?: (annotation: Annotation) => void;
57
55
  tooltipClassName?: string;
58
56
  hoverTooltipClassName?: string;
59
57
  highlightClassName?: string;
60
- tooltipBubbleSize?: number;
58
+ commentIconPosition?: 'highlight' | 'page';
59
+ underlineClassName?: string;
60
+ commentIconClassName?: string;
61
61
  }
62
62
 
63
63
  /**
@@ -97,7 +97,7 @@ declare interface AnnotationState {
97
97
  setAnnotations: (annotations: Annotation[]) => void;
98
98
  }
99
99
 
100
- export declare const AnnotationTooltip: ({ annotation, children, renderTooltipContent, 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, hoverIsOpen: controlledHoverIsOpen, }: AnnotationTooltipProps) => JSX.Element;
101
101
 
102
102
  export declare type AnnotationTooltipContentProps = {
103
103
  annotation: Annotation;
@@ -115,7 +115,6 @@ declare interface AnnotationTooltipProps {
115
115
  className?: string;
116
116
  hoverClassName?: string;
117
117
  hoverIsOpen?: boolean;
118
- tooltipBubbleSize: number;
119
118
  renderHoverTooltipContent?: (props: {
120
119
  annotation: Annotation;
121
120
  onClose: () => void;
@@ -149,6 +148,7 @@ export declare const clamp: (value: number, min: number, max: number) => number;
149
148
 
150
149
  declare type CollapsibleSelection = {
151
150
  highlights: HighlightRect[];
151
+ underlines?: HighlightRect[];
152
152
  text: string;
153
153
  isCollapsed: boolean;
154
154
  };
@@ -205,6 +205,14 @@ declare type HighlightRect = {
205
205
  export { HighlightRect }
206
206
  export { HighlightRect as HighlightRect_alias_1 }
207
207
 
208
+ export declare interface HighlightRect_alias_2 {
209
+ height: number;
210
+ left: number;
211
+ top: number;
212
+ width: number;
213
+ pageNumber: number;
214
+ }
215
+
208
216
  export declare interface InitialPDFState {
209
217
  pdfDocumentProxy: PDFDocumentProxy;
210
218
  pageProxies: PDFPageProxy[];
@@ -470,6 +478,12 @@ export declare const useSelectionDimensions: () => {
470
478
  isCollapsed: boolean;
471
479
  } | undefined;
472
480
  getSelection: () => CollapsibleSelection;
481
+ getAnnotationDimension: () => {
482
+ highlights: HighlightRect[];
483
+ underlines: HighlightRect[];
484
+ text: string;
485
+ isCollapsed: boolean;
486
+ } | undefined;
473
487
  };
474
488
 
475
489
  export declare const ZoomIn: ({ ...props }: HTMLProps<HTMLButtonElement>) => JSX.Element;
package/dist/index.js CHANGED
@@ -276,7 +276,6 @@ var AnnotationTooltip = ({
276
276
  className,
277
277
  hoverClassName,
278
278
  isOpen: controlledIsOpen,
279
- tooltipBubbleSize,
280
279
  hoverIsOpen: controlledHoverIsOpen
281
280
  }) => {
282
281
  const viewportRef = usePdf((state) => state.viewportRef);
@@ -345,7 +344,7 @@ var AnnotationTooltip = ({
345
344
  setHoverIsOpen(false);
346
345
  }, [setHoverIsOpen]);
347
346
  return /* @__PURE__ */ jsxs(Fragment, { children: [
348
- /* @__PURE__ */ jsxs(
347
+ /* @__PURE__ */ jsx(
349
348
  "div",
350
349
  {
351
350
  ref: (node) => {
@@ -357,18 +356,7 @@ var AnnotationTooltip = ({
357
356
  onMouseLeave: handleMouseLeave,
358
357
  ...getReferenceProps(),
359
358
  ...getHoverReferenceProps(),
360
- children: [
361
- !isOpen && !hoverIsOpen && annotation.comment && /* @__PURE__ */ jsx("div", { className: "rounded-full", style: {
362
- position: "absolute",
363
- top: annotation.highlights[0]?.top ? annotation.highlights[0]?.top - tooltipBubbleSize / 2 : 0,
364
- left: annotation.highlights[0]?.left ? annotation.highlights[0]?.left - tooltipBubbleSize / 2 : 0,
365
- backgroundColor: annotation.borderColor,
366
- height: `${tooltipBubbleSize}px`,
367
- width: `${tooltipBubbleSize}px`,
368
- zIndex: 50
369
- } }),
370
- children
371
- ]
359
+ children
372
360
  }
373
361
  ),
374
362
  isOpen && viewportRef.current && createPortal(
@@ -449,63 +437,147 @@ var AnnotationHighlightLayer = ({
449
437
  renderHoverTooltipContent,
450
438
  tooltipClassName,
451
439
  highlightClassName,
440
+ underlineClassName,
441
+ commentIconPosition,
442
+ commmentIcon,
443
+ commentIconClassName,
452
444
  focusedAnnotationId,
453
445
  focusedHoverAnnotationId,
454
446
  onAnnotationClick,
455
- hoverTooltipClassName,
456
- tooltipBubbleSize = 6
447
+ onAnnotationTooltipClose,
448
+ hoverTooltipClassName
457
449
  }) => {
458
450
  const { annotations } = useAnnotations();
459
451
  const pageNumber = usePDFPageNumber();
460
452
  const pageAnnotations = annotations.filter(
461
453
  (annotation) => annotation.pageNumber === pageNumber
462
454
  );
463
- return /* @__PURE__ */ jsx("div", { className, style, children: pageAnnotations.map((annotation) => /* @__PURE__ */ jsx(
464
- AnnotationTooltip,
465
- {
466
- annotation,
467
- className: tooltipClassName,
468
- hoverClassName: hoverTooltipClassName,
469
- isOpen: focusedAnnotationId === annotation.id,
470
- tooltipBubbleSize,
471
- hoverIsOpen: focusedHoverAnnotationId === annotation.id,
472
- onOpenChange: (open) => {
473
- if (open && onAnnotationClick) {
474
- onAnnotationClick(annotation);
455
+ const getCommentIconPosition = (highlights) => {
456
+ if (!highlights.length) return { top: 0, right: 10 };
457
+ const sortedHighlights = [...highlights].sort((a, b) => {
458
+ const topDiff = a.top - b.top;
459
+ return Math.abs(topDiff) < 3 ? a.left - b.left : topDiff;
460
+ });
461
+ const lines = [];
462
+ let currentLine = [];
463
+ sortedHighlights.forEach((highlight) => {
464
+ if (currentLine.length === 0) {
465
+ currentLine.push(highlight);
466
+ } else {
467
+ const firstInLine = currentLine[0];
468
+ if (Math.abs(highlight.top - firstInLine.top) <= 3) {
469
+ currentLine.push(highlight);
470
+ } else {
471
+ lines.push([...currentLine]);
472
+ currentLine = [highlight];
475
473
  }
476
- },
477
- renderTooltipContent,
478
- hoverTooltipContent: renderHoverTooltipContent({
474
+ }
475
+ });
476
+ if (currentLine.length > 0) {
477
+ lines.push(currentLine);
478
+ }
479
+ const PAGE_WIDTH = 600;
480
+ const hasLongLine = lines.some((line) => {
481
+ if (line.length === 0) return false;
482
+ const rightmost2 = Math.max(...line.map((h) => h.left + h.width));
483
+ return rightmost2 > PAGE_WIDTH * 0.8;
484
+ });
485
+ const firstHighlight = highlights[0];
486
+ const firstLine = lines[0] || [];
487
+ const leftmost = Math.min(...firstLine.map((h) => h.left));
488
+ const rightmost = Math.max(...firstLine.map((h) => h.left + h.width));
489
+ const lineCenter = leftmost + (rightmost - leftmost) / 2;
490
+ const shouldPositionRight = hasLongLine || lineCenter > PAGE_WIDTH * 0.5;
491
+ const rightPosition = commentIconPosition === "highlight" ? { left: rightmost + 8 } : { right: 10 };
492
+ const leftPosition = commentIconPosition === "highlight" ? { left: leftmost - 18 } : { left: 20 };
493
+ return {
494
+ top: firstHighlight.top + firstHighlight.height / 2 - 6,
495
+ ...shouldPositionRight ? rightPosition : leftPosition
496
+ };
497
+ };
498
+ return /* @__PURE__ */ jsx("div", { className, style, children: pageAnnotations.map((annotation) => {
499
+ return /* @__PURE__ */ jsx(
500
+ AnnotationTooltip,
501
+ {
479
502
  annotation,
480
- onClose: () => {
481
- }
482
- }),
483
- children: /* @__PURE__ */ jsx(
484
- "div",
485
- {
486
- style: { cursor: "pointer" },
487
- onClick: () => onAnnotationClick?.(annotation),
488
- children: annotation.highlights.map((highlight, index) => /* @__PURE__ */ jsx(
489
- "div",
490
- {
491
- className: highlightClassName,
492
- style: {
493
- position: "absolute",
494
- top: highlight.top,
495
- left: highlight.left,
496
- width: highlight.width,
497
- height: highlight.height,
498
- backgroundColor: annotation.color
499
- },
500
- "data-highlight-id": annotation.id
501
- },
502
- index
503
- ))
504
- }
505
- )
506
- },
507
- annotation.id
508
- )) });
503
+ className: tooltipClassName,
504
+ hoverClassName: hoverTooltipClassName,
505
+ isOpen: focusedAnnotationId === annotation.id,
506
+ hoverIsOpen: focusedHoverAnnotationId === annotation.id,
507
+ onOpenChange: (open) => {
508
+ if (open && onAnnotationClick) {
509
+ onAnnotationClick(annotation);
510
+ } else if (!open && onAnnotationTooltipClose) {
511
+ onAnnotationTooltipClose(annotation);
512
+ }
513
+ },
514
+ renderTooltipContent,
515
+ hoverTooltipContent: renderHoverTooltipContent({
516
+ annotation,
517
+ onClose: () => {
518
+ }
519
+ }),
520
+ children: /* @__PURE__ */ jsxs(
521
+ "div",
522
+ {
523
+ style: { cursor: "pointer" },
524
+ onClick: () => onAnnotationClick?.(annotation),
525
+ children: [
526
+ annotation.highlights.map((highlight, index) => /* @__PURE__ */ jsx(
527
+ "div",
528
+ {
529
+ className: highlightClassName,
530
+ style: {
531
+ position: "absolute",
532
+ top: highlight.top,
533
+ left: highlight.left,
534
+ width: highlight.width,
535
+ height: highlight.height,
536
+ backgroundColor: annotation.color
537
+ },
538
+ "data-highlight-id": annotation.id
539
+ },
540
+ `highlight-${index}`
541
+ )),
542
+ annotation.comment && annotation.underlines?.map((rect, index) => /* @__PURE__ */ jsx(
543
+ "div",
544
+ {
545
+ className: underlineClassName,
546
+ style: {
547
+ position: "absolute",
548
+ top: rect.top,
549
+ left: rect.left,
550
+ width: rect.width,
551
+ height: rect.height,
552
+ backgroundColor: annotation.borderColor,
553
+ borderBottom: "1.5px"
554
+ },
555
+ "data-comment-id": annotation.id
556
+ },
557
+ `underline-${index}`
558
+ )),
559
+ annotation.comment && commmentIcon && /* @__PURE__ */ jsx(
560
+ "div",
561
+ {
562
+ className: commentIconClassName,
563
+ style: {
564
+ position: "absolute",
565
+ ...getCommentIconPosition(annotation.highlights),
566
+ color: "gray",
567
+ cursor: "pointer",
568
+ zIndex: 10
569
+ },
570
+ "data-comment-icon-id": annotation.id,
571
+ children: commmentIcon
572
+ }
573
+ )
574
+ ]
575
+ }
576
+ )
577
+ },
578
+ annotation.id
579
+ );
580
+ }) });
509
581
  };
510
582
 
511
583
  // ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
@@ -1049,48 +1121,341 @@ var CanvasLayer = ({
1049
1121
  };
1050
1122
 
1051
1123
  // src/hooks/useSelectionDimensions.tsx
1052
- var MERGE_THRESHOLD = 5;
1124
+ var MERGE_THRESHOLD = 2;
1053
1125
  var shouldMergeRects = (rect1, rect2) => {
1054
1126
  const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
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;
1056
- return verticalOverlap && horizontallyClose;
1127
+ 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;
1128
+ return verticalOverlap && horizontallyConnected;
1057
1129
  };
1058
- var mergeRects = (rect1, rect2) => {
1059
- return {
1060
- left: Math.min(rect1.left, rect2.left),
1061
- top: Math.min(rect1.top, rect2.top),
1062
- width: Math.max(rect1.left + rect1.width, rect2.left + rect2.width) - Math.min(rect1.left, rect2.left),
1063
- height: Math.max(rect1.top + rect1.height, rect2.top + rect2.height) - Math.min(rect1.top, rect2.top),
1064
- pageNumber: rect1.pageNumber
1065
- };
1130
+ var consolidateHighlightRects = (rects) => {
1131
+ if (rects.length <= 1) return rects;
1132
+ const sorted = [...rects].sort((a, b) => {
1133
+ const topDiff = a.top - b.top;
1134
+ return Math.abs(topDiff) < 2 ? a.left - b.left : topDiff;
1135
+ });
1136
+ const result = [];
1137
+ let currentGroup = [sorted[0]];
1138
+ for (let i = 1; i < sorted.length; i++) {
1139
+ const currentRect = sorted[i];
1140
+ const lastInGroup = currentGroup[currentGroup.length - 1];
1141
+ const sameLineThreshold = Math.max(lastInGroup.height, currentRect.height) * 0.5;
1142
+ const onSameLine = Math.abs(lastInGroup.top - currentRect.top) < sameLineThreshold;
1143
+ const adjacent = Math.abs(lastInGroup.left + lastInGroup.width - currentRect.left) <= MERGE_THRESHOLD;
1144
+ const overlapping = lastInGroup.left < currentRect.left + currentRect.width && currentRect.left < lastInGroup.left + lastInGroup.width;
1145
+ if (onSameLine && (adjacent || overlapping)) {
1146
+ currentGroup.push(currentRect);
1147
+ } else {
1148
+ if (currentGroup.length === 1) {
1149
+ result.push(currentGroup[0]);
1150
+ } else {
1151
+ result.push(mergeRectGroup(currentGroup));
1152
+ }
1153
+ currentGroup = [currentRect];
1154
+ }
1155
+ }
1156
+ if (currentGroup.length === 1) {
1157
+ result.push(currentGroup[0]);
1158
+ } else {
1159
+ result.push(mergeRectGroup(currentGroup));
1160
+ }
1161
+ return result;
1066
1162
  };
1067
1163
  var consolidateRects = (rects) => {
1068
1164
  if (rects.length <= 1) return rects;
1069
- const sortedRects = [...rects].sort((a, b) => a.top - b.top);
1070
- let hasChanges;
1071
- do {
1072
- hasChanges = false;
1073
- let currentRect = sortedRects[0];
1074
- const tempResult = [];
1075
- for (let i = 1; i < sortedRects.length; i++) {
1076
- const sorted = sortedRects[i];
1077
- if (!currentRect || !sorted) continue;
1078
- if (shouldMergeRects(currentRect, sorted)) {
1079
- currentRect = mergeRects(currentRect, sorted);
1080
- hasChanges = true;
1081
- } else {
1082
- tempResult.push(currentRect);
1083
- currentRect = sorted;
1165
+ const result = [];
1166
+ const visited = /* @__PURE__ */ new Set();
1167
+ for (let i = 0; i < rects.length; i++) {
1168
+ if (visited.has(i)) continue;
1169
+ const currentRect = rects[i];
1170
+ if (!currentRect) continue;
1171
+ const currentGroup = [currentRect];
1172
+ visited.add(i);
1173
+ let foundNew = true;
1174
+ while (foundNew) {
1175
+ foundNew = false;
1176
+ for (let j = 0; j < rects.length; j++) {
1177
+ if (visited.has(j)) continue;
1178
+ const candidateRect = rects[j];
1179
+ if (!candidateRect) continue;
1180
+ const shouldMergeWithGroup = currentGroup.some(
1181
+ (groupRect) => doRectsOverlap(groupRect, candidateRect)
1182
+ );
1183
+ if (shouldMergeWithGroup) {
1184
+ currentGroup.push(candidateRect);
1185
+ visited.add(j);
1186
+ foundNew = true;
1187
+ }
1084
1188
  }
1085
1189
  }
1086
- if (currentRect) tempResult.push(currentRect);
1087
- sortedRects.length = 0;
1088
- sortedRects.push(...tempResult);
1089
- } while (hasChanges);
1090
- return sortedRects;
1190
+ result.push(mergeRectGroup(currentGroup));
1191
+ }
1192
+ return result;
1193
+ };
1194
+ var doRectsOverlap = (rect1, rect2) => {
1195
+ const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
1196
+ const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height;
1197
+ const closeEnough = shouldMergeRects(rect1, rect2);
1198
+ return horizontalOverlap && verticalOverlap || closeEnough;
1199
+ };
1200
+ var mergeRectGroup = (rects) => {
1201
+ if (rects.length === 1) {
1202
+ const rect = rects[0];
1203
+ if (!rect) throw new Error("Invalid rect in group");
1204
+ return rect;
1205
+ }
1206
+ const firstRect = rects[0];
1207
+ if (!firstRect) throw new Error("Invalid first rect in group");
1208
+ let minLeft = firstRect.left;
1209
+ let minTop = firstRect.top;
1210
+ let maxRight = firstRect.left + firstRect.width;
1211
+ let maxBottom = firstRect.top + firstRect.height;
1212
+ rects.forEach((rect) => {
1213
+ if (!rect) return;
1214
+ minLeft = Math.min(minLeft, rect.left);
1215
+ minTop = Math.min(minTop, rect.top);
1216
+ maxRight = Math.max(maxRight, rect.left + rect.width);
1217
+ maxBottom = Math.max(maxBottom, rect.top + rect.height);
1218
+ });
1219
+ return {
1220
+ left: minLeft,
1221
+ top: minTop,
1222
+ width: maxRight - minLeft,
1223
+ height: maxBottom - minTop,
1224
+ pageNumber: firstRect.pageNumber
1225
+ };
1091
1226
  };
1092
1227
  var useSelectionDimensions = () => {
1093
1228
  const store = PDFStore.useContext();
1229
+ const getAnnotationDimension = () => {
1230
+ const selection = window.getSelection();
1231
+ if (!selection || selection.isCollapsed) return;
1232
+ const range = selection.getRangeAt(0);
1233
+ const highlightRects = [];
1234
+ const underlineRects = [];
1235
+ const textLayerMapHighlight = /* @__PURE__ */ new Map();
1236
+ const textLayerMapUnderline = /* @__PURE__ */ new Map();
1237
+ const clientRects = Array.from(range.getClientRects()).filter(
1238
+ (rect) => rect.width > 2 && rect.height > 2
1239
+ );
1240
+ clientRects.forEach((clientRect) => {
1241
+ let element = document.elementFromPoint(
1242
+ clientRect.left + 1,
1243
+ clientRect.top + clientRect.height / 2
1244
+ );
1245
+ if (!element) {
1246
+ element = document.elementFromPoint(
1247
+ clientRect.left + clientRect.width / 2,
1248
+ clientRect.top + clientRect.height / 2
1249
+ );
1250
+ }
1251
+ if (!element) {
1252
+ element = document.elementFromPoint(
1253
+ clientRect.right - 1,
1254
+ clientRect.top + clientRect.height / 2
1255
+ );
1256
+ }
1257
+ if (!element) {
1258
+ element = document.elementFromPoint(
1259
+ clientRect.left + clientRect.width / 2,
1260
+ clientRect.top + 1
1261
+ );
1262
+ }
1263
+ if (!element) {
1264
+ element = document.elementFromPoint(
1265
+ clientRect.left + clientRect.width / 2,
1266
+ clientRect.bottom - 1
1267
+ );
1268
+ }
1269
+ const textLayer = element?.closest(".textLayer");
1270
+ if (!textLayer) return;
1271
+ const isSuperOrSubScript = (el) => {
1272
+ if (!el) return false;
1273
+ if (el.tagName.toLowerCase() === "sup" || el.tagName.toLowerCase() === "sub") {
1274
+ return true;
1275
+ }
1276
+ const classes = el.className;
1277
+ if (typeof classes === "string") {
1278
+ const superSubClasses = ["superscript", "subscript", "sup", "sub"];
1279
+ if (superSubClasses.some((c) => classes.includes(c))) {
1280
+ return true;
1281
+ }
1282
+ }
1283
+ const elementRect = el.getBoundingClientRect();
1284
+ if (elementRect.height < 6 && elementRect.width < 15) {
1285
+ const textContent = el.textContent?.trim() || "";
1286
+ if (textContent.length <= 2 && /^[\d\w]{1,2}$/.test(textContent)) {
1287
+ const parentRect = el.parentElement?.getBoundingClientRect();
1288
+ if (parentRect && parentRect.height > elementRect.height * 2) {
1289
+ const elementCenter = elementRect.top + elementRect.height / 2;
1290
+ const parentCenter = parentRect.top + parentRect.height / 2;
1291
+ const verticalOffset = Math.abs(elementCenter - parentCenter);
1292
+ if (verticalOffset > parentRect.height * 0.4) {
1293
+ return true;
1294
+ }
1295
+ }
1296
+ }
1297
+ }
1298
+ return false;
1299
+ };
1300
+ const pageNumber = parseInt(
1301
+ textLayer.getAttribute("data-page-number") || "1",
1302
+ 10
1303
+ );
1304
+ const textLayerRect = textLayer.getBoundingClientRect();
1305
+ const zoom = store.getState().zoom;
1306
+ const highlightRect = {
1307
+ width: clientRect.width / zoom,
1308
+ height: clientRect.height / zoom,
1309
+ top: (clientRect.top - textLayerRect.top) / zoom,
1310
+ left: (clientRect.left - textLayerRect.left) / zoom,
1311
+ pageNumber
1312
+ };
1313
+ if (!textLayerMapHighlight.has(pageNumber)) {
1314
+ textLayerMapHighlight.set(pageNumber, []);
1315
+ }
1316
+ textLayerMapHighlight.get(pageNumber)?.push(highlightRect);
1317
+ const shouldCreateUnderline = !isSuperOrSubScript(element);
1318
+ if (shouldCreateUnderline) {
1319
+ const baselineOffset = clientRect.height * 0.85;
1320
+ const underlineHeight = 2;
1321
+ const underlineRect = {
1322
+ width: clientRect.width / zoom,
1323
+ height: underlineHeight / zoom,
1324
+ top: (clientRect.top - textLayerRect.top + baselineOffset) / zoom,
1325
+ left: (clientRect.left - textLayerRect.left) / zoom,
1326
+ pageNumber
1327
+ };
1328
+ if (!textLayerMapUnderline.has(pageNumber)) {
1329
+ textLayerMapUnderline.set(pageNumber, []);
1330
+ }
1331
+ textLayerMapUnderline.get(pageNumber)?.push(underlineRect);
1332
+ }
1333
+ });
1334
+ textLayerMapHighlight.forEach((rects) => {
1335
+ if (rects.length > 0) {
1336
+ const consolidated = consolidateHighlightRects(rects);
1337
+ highlightRects.push(...consolidated);
1338
+ }
1339
+ });
1340
+ textLayerMapUnderline.forEach((rects) => {
1341
+ if (rects.length > 0) {
1342
+ const lineGroups = groupRectsByLine(rects);
1343
+ lineGroups.forEach((group) => {
1344
+ if (group.length === 0) return;
1345
+ group.sort((a, b) => a.left - b.left);
1346
+ let i = 0;
1347
+ while (i < group.length) {
1348
+ const startRect = group[i];
1349
+ if (!startRect) {
1350
+ i++;
1351
+ continue;
1352
+ }
1353
+ let endIndex = i;
1354
+ while (endIndex + 1 < group.length) {
1355
+ const currentRect = group[endIndex];
1356
+ const nextRect = group[endIndex + 1];
1357
+ if (!currentRect || !nextRect) break;
1358
+ const gap = nextRect.left - (currentRect.left + currentRect.width);
1359
+ const maxGapAllowed = Math.max(MERGE_THRESHOLD, currentRect.height * 0.3);
1360
+ if (gap <= maxGapAllowed) {
1361
+ endIndex++;
1362
+ } else {
1363
+ break;
1364
+ }
1365
+ }
1366
+ const endRect = group[endIndex];
1367
+ if (!endRect) {
1368
+ i++;
1369
+ continue;
1370
+ }
1371
+ const zoom = store.getState().zoom;
1372
+ const lineRect = {
1373
+ width: endRect.left + endRect.width - startRect.left,
1374
+ height: 2 / zoom,
1375
+ // Fixed 2px thickness
1376
+ top: startRect.top,
1377
+ left: startRect.left,
1378
+ pageNumber: startRect.pageNumber
1379
+ };
1380
+ underlineRects.push(lineRect);
1381
+ i = endIndex + 1;
1382
+ }
1383
+ });
1384
+ }
1385
+ });
1386
+ if (underlineRects.length === 0 && highlightRects.length > 0) {
1387
+ highlightRects.forEach((highlightRect) => {
1388
+ const baselineOffset = highlightRect.height * 0.85;
1389
+ const underlineHeight = 1.5;
1390
+ const underlineRect = {
1391
+ width: highlightRect.width,
1392
+ height: underlineHeight,
1393
+ top: highlightRect.top + baselineOffset,
1394
+ left: highlightRect.left,
1395
+ pageNumber: highlightRect.pageNumber
1396
+ };
1397
+ underlineRects.push(underlineRect);
1398
+ });
1399
+ }
1400
+ return {
1401
+ highlights: highlightRects.sort((a, b) => a.pageNumber - b.pageNumber),
1402
+ underlines: consolidateUnderlines(underlineRects).sort((a, b) => a.pageNumber - b.pageNumber),
1403
+ text: range.toString().trim(),
1404
+ isCollapsed: false
1405
+ };
1406
+ };
1407
+ const groupRectsByLine = (rects) => {
1408
+ const VERTICAL_TOLERANCE = 3;
1409
+ const groups = [];
1410
+ rects.forEach((rect) => {
1411
+ const centerY = rect.top + rect.height / 2;
1412
+ let foundGroup = false;
1413
+ for (const group of groups) {
1414
+ if (group.length === 0) continue;
1415
+ const firstRect = group[0];
1416
+ if (!firstRect) continue;
1417
+ const groupCenterY = firstRect.top + firstRect.height / 2;
1418
+ if (Math.abs(centerY - groupCenterY) <= VERTICAL_TOLERANCE) {
1419
+ group.push(rect);
1420
+ foundGroup = true;
1421
+ break;
1422
+ }
1423
+ }
1424
+ if (!foundGroup) {
1425
+ groups.push([rect]);
1426
+ }
1427
+ });
1428
+ return groups;
1429
+ };
1430
+ const consolidateUnderlines = (underlines) => {
1431
+ if (underlines.length <= 1) return underlines;
1432
+ const consolidated = [];
1433
+ const sorted = [...underlines].sort((a, b) => {
1434
+ const pageCompare = a.pageNumber - b.pageNumber;
1435
+ if (pageCompare !== 0) return pageCompare;
1436
+ const topCompare = a.top - b.top;
1437
+ return Math.abs(topCompare) < 1 ? a.left - b.left : topCompare;
1438
+ });
1439
+ let current = sorted[0];
1440
+ for (let i = 1; i < sorted.length; i++) {
1441
+ const next = sorted[i];
1442
+ const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < 1;
1443
+ 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);
1444
+ if (horizontallyConnected) {
1445
+ const newWidth = Math.max(current.left + current.width, next.left + next.width) - Math.min(current.left, next.left);
1446
+ current = {
1447
+ ...current,
1448
+ left: Math.min(current.left, next.left),
1449
+ width: newWidth
1450
+ };
1451
+ } else {
1452
+ consolidated.push(current);
1453
+ current = next;
1454
+ }
1455
+ }
1456
+ consolidated.push(current);
1457
+ return consolidated;
1458
+ };
1094
1459
  const getDimension = () => {
1095
1460
  const selection = window.getSelection();
1096
1461
  if (!selection || selection.isCollapsed) return;
@@ -1127,7 +1492,8 @@ var useSelectionDimensions = () => {
1127
1492
  });
1128
1493
  textLayerMap.forEach((rects) => {
1129
1494
  if (rects.length > 0) {
1130
- highlights.push(...consolidateRects(rects));
1495
+ const consolidated = consolidateRects(rects);
1496
+ highlights.push(...consolidated);
1131
1497
  }
1132
1498
  });
1133
1499
  return {
@@ -1137,7 +1503,7 @@ var useSelectionDimensions = () => {
1137
1503
  };
1138
1504
  };
1139
1505
  const getSelection = () => getDimension();
1140
- return { getDimension, getSelection };
1506
+ return { getDimension, getSelection, getAnnotationDimension };
1141
1507
  };
1142
1508
  var SelectionTooltip = ({ children }) => {
1143
1509
  const [isOpen, setIsOpen] = useState(false);
@@ -1522,24 +1888,171 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
1522
1888
  }) });
1523
1889
  });
1524
1890
  HighlightLayer.displayName = "HighlightLayer";
1891
+ var createTextSelectionManager = () => {
1892
+ const textLayers = /* @__PURE__ */ new Map();
1893
+ let selectionChangeAbortController = null;
1894
+ let isPointerDown = false;
1895
+ let prevRange = null;
1896
+ let isFirefox;
1897
+ const removeGlobalSelectionListener = (textLayerDiv) => {
1898
+ textLayers.delete(textLayerDiv);
1899
+ if (textLayers.size === 0) {
1900
+ selectionChangeAbortController?.abort();
1901
+ selectionChangeAbortController = null;
1902
+ }
1903
+ };
1904
+ const enableGlobalSelectionListener = () => {
1905
+ if (selectionChangeAbortController) {
1906
+ return;
1907
+ }
1908
+ selectionChangeAbortController = new AbortController();
1909
+ const { signal } = selectionChangeAbortController;
1910
+ const reset = (endDiv, textLayer) => {
1911
+ if (endDiv.parentNode !== textLayer) {
1912
+ textLayer.appendChild(endDiv);
1913
+ }
1914
+ endDiv.style.width = "";
1915
+ endDiv.style.height = "";
1916
+ textLayer.classList.remove("selecting");
1917
+ };
1918
+ document.addEventListener("pointerdown", () => {
1919
+ isPointerDown = true;
1920
+ }, { signal });
1921
+ document.addEventListener("pointerup", () => {
1922
+ isPointerDown = false;
1923
+ textLayers.forEach(reset);
1924
+ }, { signal });
1925
+ window.addEventListener("blur", () => {
1926
+ isPointerDown = false;
1927
+ textLayers.forEach(reset);
1928
+ }, { signal });
1929
+ document.addEventListener("keyup", () => {
1930
+ if (!isPointerDown) {
1931
+ textLayers.forEach(reset);
1932
+ }
1933
+ }, { signal });
1934
+ document.addEventListener("selectionchange", () => {
1935
+ const selection = document.getSelection();
1936
+ if (!selection || selection.rangeCount === 0) {
1937
+ textLayers.forEach(reset);
1938
+ return;
1939
+ }
1940
+ const activeTextLayers = /* @__PURE__ */ new Set();
1941
+ for (let i = 0; i < selection.rangeCount; i++) {
1942
+ const range = selection.getRangeAt(i);
1943
+ for (const textLayerDiv of textLayers.keys()) {
1944
+ if (!activeTextLayers.has(textLayerDiv) && range.intersectsNode(textLayerDiv)) {
1945
+ activeTextLayers.add(textLayerDiv);
1946
+ }
1947
+ }
1948
+ }
1949
+ for (const [textLayerDiv, endDiv] of textLayers) {
1950
+ if (activeTextLayers.has(textLayerDiv)) {
1951
+ textLayerDiv.classList.add("selecting");
1952
+ } else {
1953
+ reset(endDiv, textLayerDiv);
1954
+ }
1955
+ }
1956
+ if (isFirefox === void 0) {
1957
+ const firstTextLayer = textLayers.keys().next().value;
1958
+ if (firstTextLayer) {
1959
+ isFirefox = getComputedStyle(firstTextLayer).getPropertyValue("-moz-user-select") === "none";
1960
+ }
1961
+ }
1962
+ if (isFirefox) {
1963
+ return;
1964
+ }
1965
+ try {
1966
+ const range = selection.getRangeAt(0);
1967
+ const modifyStart = prevRange && (range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 || range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0);
1968
+ let anchor = modifyStart ? range.startContainer : range.endContainer;
1969
+ if (anchor.nodeType === Node.TEXT_NODE) {
1970
+ anchor = anchor.parentNode;
1971
+ }
1972
+ const parentTextLayer = anchor.parentElement?.closest(".textLayer");
1973
+ const endDiv = textLayers.get(parentTextLayer);
1974
+ if (endDiv && parentTextLayer) {
1975
+ endDiv.style.width = parentTextLayer.style.width;
1976
+ endDiv.style.height = parentTextLayer.style.height;
1977
+ const insertTarget = modifyStart ? anchor : anchor.nextSibling;
1978
+ if (anchor.parentElement && insertTarget) {
1979
+ anchor.parentElement.insertBefore(endDiv, insertTarget);
1980
+ }
1981
+ }
1982
+ prevRange = range.cloneRange();
1983
+ } catch {
1984
+ }
1985
+ }, { signal });
1986
+ };
1987
+ const bindMouseEvents2 = (textLayerDiv, endOfContent) => {
1988
+ if (textLayerDiv._textSelectionBound) {
1989
+ return;
1990
+ }
1991
+ textLayerDiv._textSelectionBound = true;
1992
+ textLayers.set(textLayerDiv, endOfContent);
1993
+ enableGlobalSelectionListener();
1994
+ const handleMouseDown = () => {
1995
+ textLayerDiv.classList.add("selecting");
1996
+ };
1997
+ textLayerDiv.addEventListener("mousedown", handleMouseDown);
1998
+ textLayerDiv._cleanupTextSelection = () => {
1999
+ textLayerDiv.removeEventListener("mousedown", handleMouseDown);
2000
+ removeGlobalSelectionListener(textLayerDiv);
2001
+ delete textLayerDiv._textSelectionBound;
2002
+ };
2003
+ };
2004
+ return bindMouseEvents2;
2005
+ };
2006
+ var bindMouseEvents = createTextSelectionManager();
1525
2007
  var useTextLayer = () => {
1526
2008
  const textContainerRef = useRef(null);
2009
+ const textLayerRef = useRef(null);
2010
+ const isRenderingRef = useRef(false);
1527
2011
  const pageNumber = usePDFPageNumber();
1528
2012
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
1529
2013
  useEffect(() => {
1530
- if (!textContainerRef.current) {
2014
+ const textContainer = textContainerRef.current;
2015
+ if (!textContainer || isRenderingRef.current) {
1531
2016
  return;
1532
2017
  }
2018
+ isRenderingRef.current = true;
2019
+ textContainer.innerHTML = "";
2020
+ if (textLayerRef.current) {
2021
+ textLayerRef.current.cancel();
2022
+ textLayerRef.current = null;
2023
+ }
1533
2024
  const textLayer = new TextLayer({
1534
2025
  textContentSource: pdfPageProxy.streamTextContent(),
1535
- container: textContainerRef.current,
2026
+ container: textContainer,
1536
2027
  viewport: pdfPageProxy.getViewport({ scale: 1 })
1537
2028
  });
1538
- void textLayer.render();
2029
+ textLayerRef.current = textLayer;
2030
+ textLayer.render().then(() => {
2031
+ if (textLayerRef.current === textLayer && textContainer) {
2032
+ const endOfContent = document.createElement("div");
2033
+ endOfContent.className = "endOfContent";
2034
+ textContainer.appendChild(endOfContent);
2035
+ bindMouseEvents(textContainer, endOfContent);
2036
+ }
2037
+ }).catch((error) => {
2038
+ if (error.name !== "AbortException") {
2039
+ console.error("TextLayer rendering error:", error);
2040
+ }
2041
+ }).finally(() => {
2042
+ isRenderingRef.current = false;
2043
+ });
1539
2044
  return () => {
1540
- textLayer.cancel();
2045
+ isRenderingRef.current = false;
2046
+ if (textLayerRef.current) {
2047
+ textLayerRef.current.cancel();
2048
+ textLayerRef.current = null;
2049
+ }
2050
+ if (textContainer?._cleanupTextSelection) {
2051
+ textContainer._cleanupTextSelection();
2052
+ delete textContainer._cleanupTextSelection;
2053
+ }
1541
2054
  };
1542
- }, [pdfPageProxy]);
2055
+ }, [pdfPageProxy.pageNumber]);
1543
2056
  return {
1544
2057
  textContainerRef,
1545
2058
  pageNumber: pdfPageProxy.pageNumber
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.5.8",
3
+ "version": "3.5.10",
4
4
  "description": "Headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",