@anaralabs/lector 3.14.11 → 3.14.12

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.
@@ -267,6 +267,13 @@ export declare function createRecolorCanvasFactory(mapRef: RenderColorMapRef): {
267
267
  };
268
268
  };
269
269
 
270
+ /** Paint native selection once per page; overlapping PDF spans must not
271
+ * composite their translucent backgrounds on top of each other. */
272
+ export declare const createSelectionBackground: () => {
273
+ update: (selection: Selection | null, layers: Iterable<HTMLElement>) => void;
274
+ clear: (layer: HTMLElement) => void;
275
+ };
276
+
270
277
  export declare const CurrentPage: ({ ...props }: HTMLProps<HTMLInputElement>) => JSX.Element;
271
278
 
272
279
  export declare const CurrentZoom: ({ ...props }: HTMLProps<HTMLInputElement>) => JSX.Element;
@@ -305,6 +312,13 @@ export declare const getFitWidthZoom: (containerWidth: number, viewports: PageVi
305
312
 
306
313
  export declare const getMidHeightOfHighlightLine: (selection: ColoredHighlight) => number;
307
314
 
315
+ /**
316
+ * Per-text-node client rects for a selection. Avoids the block-level rects
317
+ * that `range.getClientRects()` returns for `.textLayer` / page wrappers,
318
+ * which would render as full-page highlights on multi-page selections.
319
+ */
320
+ export declare const getTextNodeClientRects: (range: Range) => TextNodeRect[];
321
+
308
322
  export declare const HighlightLayer: ForwardRefExoticComponent<HighlightLayerProps & RefAttributes<HTMLDivElement>>;
309
323
 
310
324
  declare interface HighlightLayerProps extends ComponentPropsWithoutRef<"div"> {
@@ -384,6 +398,8 @@ export declare const MAX_CANVAS_DIMENSION = 32767;
384
398
 
385
399
  export declare const MAX_CANVAS_PIXELS = 16777216;
386
400
 
401
+ export declare const mergeTextRuns: <T extends Rect>(rects: T[]) => T[];
402
+
387
403
  export declare const NextPage: () => void;
388
404
 
389
405
  export declare const Outline: ({ children, ...props }: HTMLProps<HTMLUListElement> & {
@@ -502,6 +518,13 @@ export declare interface RecolorContextOptions {
502
518
  pageArea?: number;
503
519
  }
504
520
 
521
+ declare type Rect = {
522
+ left: number;
523
+ top: number;
524
+ width: number;
525
+ height: number;
526
+ };
527
+
505
528
  /**
506
529
  * Removes any recolor wrapper installed on the context. For long-lived
507
530
  * contexts (the visible canvas in the no-buffer fallback, thumbnails) that
@@ -654,6 +677,11 @@ declare type TextContent = {
654
677
 
655
678
  export declare const TextLayer: MemoExoticComponent<({ className, style, ...props }: HTMLProps<HTMLDivElement>) => JSX.Element>;
656
679
 
680
+ declare type TextNodeRect = {
681
+ rect: DOMRect;
682
+ element: Element | null;
683
+ };
684
+
657
685
  declare interface TextPosition {
658
686
  pageNumber: number;
659
687
  text: string;
package/dist/index.js CHANGED
@@ -2894,9 +2894,7 @@ var CanvasLayer = memo(
2894
2894
  );
2895
2895
  CanvasLayer.displayName = "CanvasLayer";
2896
2896
 
2897
- // src/hooks/useSelectionDimensions.tsx
2898
- var MERGE_THRESHOLD = 2;
2899
- var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
2897
+ // src/lib/selection-rects.ts
2900
2898
  var getTextNodeClientRects = (range) => {
2901
2899
  const root = range.commonAncestorContainer;
2902
2900
  const ownerDoc = root.ownerDocument ?? document;
@@ -2948,6 +2946,39 @@ var getTextNodeClientRects = (range) => {
2948
2946
  }
2949
2947
  return results;
2950
2948
  };
2949
+ var connected = (a, b) => {
2950
+ const overlap = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
2951
+ const gap = Math.max(a.left, b.left) - Math.min(a.left + a.width, b.left + b.width);
2952
+ return overlap >= Math.min(a.height, b.height) / 2 && gap <= Math.min(a.height, b.height) * 0.5;
2953
+ };
2954
+ var mergeTextRuns = (rects) => {
2955
+ const groups = [];
2956
+ for (const rect of rects) {
2957
+ const group = [rect];
2958
+ for (let i = groups.length - 1; i >= 0; i--) {
2959
+ if (groups[i].some((member) => connected(member, rect))) {
2960
+ group.push(...groups[i]);
2961
+ groups.splice(i, 1);
2962
+ }
2963
+ }
2964
+ groups.push(group);
2965
+ }
2966
+ return groups.map((group) => {
2967
+ const left = Math.min(...group.map((rect) => rect.left));
2968
+ const top = Math.min(...group.map((rect) => rect.top));
2969
+ return {
2970
+ ...group[0],
2971
+ left,
2972
+ top,
2973
+ width: Math.max(...group.map((rect) => rect.left + rect.width)) - left,
2974
+ height: Math.max(...group.map((rect) => rect.top + rect.height)) - top
2975
+ };
2976
+ });
2977
+ };
2978
+
2979
+ // src/hooks/useSelectionDimensions.tsx
2980
+ var MERGE_THRESHOLD = 2;
2981
+ var LAYER_ATTRIBUTION_TOLERANCE_PX = 4;
2951
2982
  var mapSelectionRectsToLayers = (range) => {
2952
2983
  const clientRects = getTextNodeClientRects(range).filter(
2953
2984
  ({ rect }) => rect.width > 2 && rect.height > 2
@@ -2990,122 +3021,6 @@ var mapSelectionRectsToLayers = (range) => {
2990
3021
  }
2991
3022
  return result;
2992
3023
  };
2993
- var shouldMergeRects = (rect1, rect2) => {
2994
- const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
2995
- 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;
2996
- return verticalOverlap && horizontallyConnected;
2997
- };
2998
- var consolidateHighlightRects = (rects) => {
2999
- if (rects.length <= 1) return rects;
3000
- const consolidated = [];
3001
- const sorted = [...rects].sort((a, b) => {
3002
- const pageCompare = a.pageNumber - b.pageNumber;
3003
- if (pageCompare !== 0) return pageCompare;
3004
- const topDiff = a.top - b.top;
3005
- return Math.abs(topDiff) < 2 ? a.left - b.left : topDiff;
3006
- });
3007
- let current = sorted[0];
3008
- if (!current) return rects;
3009
- for (let i = 1; i < sorted.length; i++) {
3010
- const next = sorted[i];
3011
- if (!next) continue;
3012
- const samePageAndLine = current.pageNumber === next.pageNumber && Math.abs(current.top - next.top) < Math.max(current.height, next.height) * 0.5;
3013
- const horizontallyConnected = samePageAndLine && // Adjacent (touching or very close)
3014
- (Math.abs(current.left + current.width - next.left) <= MERGE_THRESHOLD || Math.abs(next.left + next.width - current.left) <= MERGE_THRESHOLD || // Overlapping
3015
- current.left < next.left + next.width && next.left < current.left + current.width || // Very close (small gap)
3016
- Math.abs(current.left + current.width - next.left) <= current.height * 0.2);
3017
- if (horizontallyConnected) {
3018
- const newLeft = Math.min(current.left, next.left);
3019
- const newRight = Math.max(
3020
- current.left + current.width,
3021
- next.left + next.width
3022
- );
3023
- const newTop = Math.min(current.top, next.top);
3024
- const newBottom = Math.max(
3025
- current.top + current.height,
3026
- next.top + next.height
3027
- );
3028
- current = {
3029
- left: newLeft,
3030
- top: newTop,
3031
- width: newRight - newLeft,
3032
- height: newBottom - newTop,
3033
- pageNumber: current.pageNumber
3034
- };
3035
- } else {
3036
- consolidated.push(current);
3037
- current = next;
3038
- }
3039
- }
3040
- if (current) {
3041
- consolidated.push(current);
3042
- }
3043
- return consolidated;
3044
- };
3045
- var consolidateRects = (rects) => {
3046
- if (rects.length <= 1) return rects;
3047
- const result = [];
3048
- const visited = /* @__PURE__ */ new Set();
3049
- for (let i = 0; i < rects.length; i++) {
3050
- if (visited.has(i)) continue;
3051
- const currentRect = rects[i];
3052
- if (!currentRect) continue;
3053
- const currentGroup = [currentRect];
3054
- visited.add(i);
3055
- let foundNew = true;
3056
- while (foundNew) {
3057
- foundNew = false;
3058
- for (let j = 0; j < rects.length; j++) {
3059
- if (visited.has(j)) continue;
3060
- const candidateRect = rects[j];
3061
- if (!candidateRect) continue;
3062
- const shouldMergeWithGroup = currentGroup.some(
3063
- (groupRect) => doRectsOverlap(groupRect, candidateRect)
3064
- );
3065
- if (shouldMergeWithGroup) {
3066
- currentGroup.push(candidateRect);
3067
- visited.add(j);
3068
- foundNew = true;
3069
- }
3070
- }
3071
- }
3072
- result.push(mergeRectGroup(currentGroup));
3073
- }
3074
- return result;
3075
- };
3076
- var doRectsOverlap = (rect1, rect2) => {
3077
- const horizontalOverlap = rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
3078
- const verticalOverlap = rect1.top < rect2.top + rect2.height && rect2.top < rect1.top + rect1.height;
3079
- const closeEnough = shouldMergeRects(rect1, rect2);
3080
- return horizontalOverlap && verticalOverlap || closeEnough;
3081
- };
3082
- var mergeRectGroup = (rects) => {
3083
- if (rects.length === 1) {
3084
- const rect = rects[0];
3085
- if (!rect) throw new Error("Invalid rect in group");
3086
- return rect;
3087
- }
3088
- const firstRect = rects[0];
3089
- if (!firstRect) throw new Error("Invalid first rect in group");
3090
- let minLeft = firstRect.left;
3091
- let minTop = firstRect.top;
3092
- let maxRight = firstRect.left + firstRect.width;
3093
- let maxBottom = firstRect.top + firstRect.height;
3094
- rects.forEach((rect) => {
3095
- if (!rect) return;
3096
- minLeft = Math.min(minLeft, rect.left);
3097
- minTop = Math.min(minTop, rect.top);
3098
- maxRight = Math.max(maxRight, rect.left + rect.width);
3099
- maxBottom = Math.max(maxBottom, rect.top + rect.height);
3100
- });
3101
- return {
3102
- left: minLeft,
3103
- top: minTop,
3104
- width: maxRight - minLeft,
3105
- height: maxBottom - minTop,
3106
- pageNumber: firstRect.pageNumber
3107
- };
3108
- };
3109
3024
  var useSelectionDimensions = () => {
3110
3025
  const store = PDFStore.useContext();
3111
3026
  const getAnnotationDimension = () => {
@@ -3178,14 +3093,7 @@ var useSelectionDimensions = () => {
3178
3093
  }
3179
3094
  });
3180
3095
  textLayerMapHighlight.forEach((rects) => {
3181
- if (rects.length > 0) {
3182
- if (rects.length === 1) {
3183
- highlightRects.push(...rects);
3184
- } else {
3185
- const consolidated = consolidateHighlightRects(rects);
3186
- highlightRects.push(...consolidated);
3187
- }
3188
- }
3096
+ highlightRects.push(...mergeTextRuns(rects));
3189
3097
  });
3190
3098
  textLayerMapUnderline.forEach((rects) => {
3191
3099
  if (rects.length > 0) {
@@ -3332,7 +3240,7 @@ var useSelectionDimensions = () => {
3332
3240
  });
3333
3241
  textLayerMap.forEach((rects) => {
3334
3242
  if (rects.length > 0) {
3335
- const consolidated = consolidateRects(rects);
3243
+ const consolidated = mergeTextRuns(rects);
3336
3244
  highlights.push(...consolidated);
3337
3245
  }
3338
3246
  });
@@ -3777,13 +3685,110 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
3777
3685
  }) });
3778
3686
  });
3779
3687
  HighlightLayer.displayName = "HighlightLayer";
3688
+
3689
+ // src/lib/selection-background.ts
3690
+ var SVG_NS = "http://www.w3.org/2000/svg";
3691
+ var createSelectionBackground = () => {
3692
+ const overlays = /* @__PURE__ */ new Map();
3693
+ let style;
3694
+ const clear = (layer) => {
3695
+ overlays.get(layer)?.remove();
3696
+ overlays.delete(layer);
3697
+ layer.removeAttribute("data-lector-selection-active");
3698
+ if (overlays.size === 0) {
3699
+ style?.remove();
3700
+ style = void 0;
3701
+ }
3702
+ };
3703
+ const update = (selection, layers) => {
3704
+ const registered = new Set(layers);
3705
+ const byLayer = /* @__PURE__ */ new Map();
3706
+ if (selection && !selection.isCollapsed) {
3707
+ for (let i = 0; i < selection.rangeCount; i++) {
3708
+ for (const { rect, element } of getTextNodeClientRects(
3709
+ selection.getRangeAt(i)
3710
+ )) {
3711
+ const layer = element?.closest(".textLayer");
3712
+ if (!layer || !element || !registered.has(layer) || rect.width <= 0 || rect.height <= 0)
3713
+ continue;
3714
+ let entry = byLayer.get(layer);
3715
+ if (!entry) {
3716
+ entry = { rects: [], element };
3717
+ byLayer.set(layer, entry);
3718
+ }
3719
+ entry.rects.push(rect);
3720
+ }
3721
+ }
3722
+ }
3723
+ for (const layer of registered) {
3724
+ const entry = byLayer.get(layer);
3725
+ if (!entry) {
3726
+ clear(layer);
3727
+ continue;
3728
+ }
3729
+ layer.removeAttribute("data-lector-selection-active");
3730
+ const color = getComputedStyle(
3731
+ entry.element,
3732
+ "::selection"
3733
+ ).backgroundColor;
3734
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
3735
+ clear(layer);
3736
+ continue;
3737
+ }
3738
+ const bounds = layer.getBoundingClientRect();
3739
+ if (!bounds.width || !bounds.height || !layer.clientWidth || !layer.clientHeight) {
3740
+ clear(layer);
3741
+ continue;
3742
+ }
3743
+ const scaleX = bounds.width / layer.clientWidth;
3744
+ const scaleY = bounds.height / layer.clientHeight;
3745
+ const rects = mergeTextRuns(
3746
+ entry.rects.map((rect) => ({
3747
+ left: (rect.left - bounds.left) / scaleX,
3748
+ top: (rect.top - bounds.top) / scaleY,
3749
+ width: rect.width / scaleX,
3750
+ height: rect.height / scaleY
3751
+ }))
3752
+ );
3753
+ let overlay = overlays.get(layer);
3754
+ if (!overlay) {
3755
+ overlay = document.createElementNS(SVG_NS, "svg");
3756
+ overlay.dataset.lectorSelection = "";
3757
+ overlay.setAttribute("aria-hidden", "true");
3758
+ overlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;user-select:none;transform:none;z-index:0;overflow:visible";
3759
+ overlay.append(document.createElementNS(SVG_NS, "path"));
3760
+ layer.append(overlay);
3761
+ overlays.set(layer, overlay);
3762
+ }
3763
+ const path = overlay.firstElementChild;
3764
+ path.setAttribute(
3765
+ "d",
3766
+ rects.map(
3767
+ (rect) => `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h${-rect.width}Z`
3768
+ ).join(" ")
3769
+ );
3770
+ path.setAttribute("fill", color);
3771
+ if (!style) {
3772
+ style = document.createElement("style");
3773
+ style.textContent = ".textLayer[data-lector-selection-active] ::selection { background-color: transparent !important; }";
3774
+ document.head.append(style);
3775
+ }
3776
+ layer.setAttribute("data-lector-selection-active", "");
3777
+ }
3778
+ };
3779
+ return { update, clear };
3780
+ };
3781
+
3782
+ // src/hooks/layers/useTextLayer.tsx
3780
3783
  var createTextSelectionManager = () => {
3781
3784
  const textLayers = /* @__PURE__ */ new Map();
3785
+ const selectionBackground = createSelectionBackground();
3782
3786
  let selectionChangeAbortController = null;
3783
3787
  let isPointerDown = false;
3784
3788
  let prevRange = null;
3785
3789
  let isFirefox;
3786
3790
  const removeGlobalSelectionListener = (textLayerDiv) => {
3791
+ selectionBackground.clear(textLayerDiv);
3787
3792
  textLayers.delete(textLayerDiv);
3788
3793
  if (textLayers.size === 0) {
3789
3794
  selectionChangeAbortController?.abort();
@@ -3840,6 +3845,7 @@ var createTextSelectionManager = () => {
3840
3845
  "selectionchange",
3841
3846
  () => {
3842
3847
  const selection = document.getSelection();
3848
+ selectionBackground.update(selection, textLayers.keys());
3843
3849
  if (!selection || selection.rangeCount === 0) {
3844
3850
  textLayers.forEach(reset);
3845
3851
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.14.11",
3
+ "version": "3.14.12",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",