@anaralabs/lector 3.15.1 → 3.15.2

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.
@@ -177,6 +177,9 @@ declare interface AsyncSearchOptions extends SearchOptions {
177
177
  export { AsyncSearchOptions }
178
178
  export { AsyncSearchOptions as AsyncSearchOptions_alias_1 }
179
179
 
180
+ /** One mouse-selection controller per viewer, shared by its mounted pages. */
181
+ export declare function bindTextSelection(layer: HTMLDivElement, viewport: ViewportRef, isPersistent?: () => boolean): () => void;
182
+
180
183
  export declare function calculateHighlightRects(pageProxy: PDFPageProxy, textMatch: TextPosition): Promise<HighlightRect[]>;
181
184
 
182
185
  export declare const cancellable: <T extends Promise<unknown>>(promise: T) => {
@@ -195,6 +198,11 @@ export declare const CanvasLayer: MemoExoticComponent<({ style, background, ...p
195
198
  background?: string;
196
199
  }) => JSX.Element>;
197
200
 
201
+ declare type Caret = {
202
+ node: Node;
203
+ offset: number;
204
+ };
205
+
198
206
  export declare const clamp: (value: number, minimum: number, maximum: number) => number;
199
207
 
200
208
  export declare function clampScaleForPage(targetScale: number, pageWidth: number, pageHeight: number, maxPixels?: number): number;
@@ -569,6 +577,7 @@ export declare class PDFSelectionController {
569
577
  });
570
578
  getSnapshot: () => PDFTextSelection | null;
571
579
  subscribe: (listener: () => void) => () => void;
580
+ get isConnected(): boolean;
572
581
  get isDragging(): boolean;
573
582
  includesPage(page: number): boolean;
574
583
  private publish;
@@ -729,6 +738,8 @@ export declare interface RenderColorMapRef {
729
738
  current: RenderColorMap | null;
730
739
  }
731
740
 
741
+ export declare function resolveTextSelectionCaret(viewport: HTMLElement, x: number, y: number): Caret | null | undefined;
742
+
732
743
  export declare const Root: ForwardRefExoticComponent<Omit<Omit<HTMLProps<HTMLDivElement>, "onError"> & usePDFDocumentParams & {
733
744
  loader?: ReactNode;
734
745
  }, "ref"> & RefAttributes<unknown>>;
@@ -1098,6 +1109,10 @@ export declare const useSelectionDimensions: () => {
1098
1109
 
1099
1110
  declare type ViewportCallback = () => void;
1100
1111
 
1112
+ declare type ViewportRef = {
1113
+ current: HTMLDivElement | null;
1114
+ };
1115
+
1101
1116
  /** Materialize one missing selected page as text only, never as a page canvas. */
1102
1117
  export declare function withSelectionPage<T>(proxy: PDFPageProxy, owner: Document, signal: AbortSignal, read: (page: SelectionPage) => T): Promise<T>;
1103
1118
 
package/dist/index.js CHANGED
@@ -474,6 +474,467 @@ function originalTextRange(normalized, start, end) {
474
474
  return { start, end };
475
475
  }
476
476
 
477
+ // src/lib/selection-rects.ts
478
+ var getTextNodeClientRects = (range) => {
479
+ const root = range.commonAncestorContainer;
480
+ const ownerDoc = root.ownerDocument ?? document;
481
+ if (root.nodeType === Node.TEXT_NODE) {
482
+ const parentElement = root.parentElement;
483
+ return Array.from(range.getClientRects()).map((rect) => ({
484
+ rect,
485
+ element: parentElement
486
+ }));
487
+ }
488
+ const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
489
+ acceptNode(node) {
490
+ if (!node.nodeValue || node.nodeValue.length === 0) {
491
+ return NodeFilter.FILTER_REJECT;
492
+ }
493
+ try {
494
+ return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
495
+ } catch {
496
+ return NodeFilter.FILTER_REJECT;
497
+ }
498
+ }
499
+ });
500
+ const results = [];
501
+ let current = walker.nextNode();
502
+ while (current) {
503
+ const textNode = current;
504
+ const parentElement = textNode.parentElement;
505
+ const length = textNode.nodeValue?.length ?? 0;
506
+ const isStartNode = textNode === range.startContainer;
507
+ const isEndNode = textNode === range.endContainer;
508
+ const start = isStartNode ? range.startOffset : 0;
509
+ const end = isEndNode ? range.endOffset : length;
510
+ if (end > start) {
511
+ const sub = ownerDoc.createRange();
512
+ try {
513
+ sub.setStart(textNode, start);
514
+ sub.setEnd(textNode, end);
515
+ const subRects = sub.getClientRects();
516
+ for (let i = 0; i < subRects.length; i++) {
517
+ const r2 = subRects[i];
518
+ if (r2) results.push({ rect: r2, element: parentElement });
519
+ }
520
+ } catch {
521
+ } finally {
522
+ sub.detach?.();
523
+ }
524
+ }
525
+ current = walker.nextNode();
526
+ }
527
+ return results;
528
+ };
529
+ var connected = (a, b) => {
530
+ const overlap = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
531
+ const gap = Math.max(a.left, b.left) - Math.min(a.left + a.width, b.left + b.width);
532
+ return overlap >= Math.min(a.height, b.height) / 2 && gap <= Math.min(a.height, b.height) * 0.5;
533
+ };
534
+ var mergeTextRuns = (rects) => {
535
+ const groups = [];
536
+ for (const rect of rects) {
537
+ const group = [rect];
538
+ for (let i = groups.length - 1; i >= 0; i--) {
539
+ if (groups[i].some((member) => connected(member, rect))) {
540
+ group.push(...groups[i]);
541
+ groups.splice(i, 1);
542
+ }
543
+ }
544
+ groups.push(group);
545
+ }
546
+ return groups.map((group) => {
547
+ const left = Math.min(...group.map((rect) => rect.left));
548
+ const top = Math.min(...group.map((rect) => rect.top));
549
+ return {
550
+ ...group[0],
551
+ left,
552
+ top,
553
+ width: Math.max(...group.map((rect) => rect.left + rect.width)) - left,
554
+ height: Math.max(...group.map((rect) => rect.top + rect.height)) - top
555
+ };
556
+ });
557
+ };
558
+
559
+ // src/lib/selection-background.ts
560
+ var SVG_NS = "http://www.w3.org/2000/svg";
561
+ var createSelectionBackground = () => {
562
+ const overlays = /* @__PURE__ */ new Map();
563
+ let style;
564
+ const clear = (layer) => {
565
+ overlays.get(layer)?.remove();
566
+ overlays.delete(layer);
567
+ layer.removeAttribute("data-lector-selection-active");
568
+ if (overlays.size === 0) {
569
+ style?.remove();
570
+ style = void 0;
571
+ }
572
+ };
573
+ const update = (selection, layers) => {
574
+ const registered = new Set(layers);
575
+ const byLayer = /* @__PURE__ */ new Map();
576
+ if (selection && !selection.isCollapsed) {
577
+ for (let i = 0; i < selection.rangeCount; i++) {
578
+ for (const { rect, element } of getTextNodeClientRects(
579
+ selection.getRangeAt(i)
580
+ )) {
581
+ const layer = element?.closest(".textLayer");
582
+ if (!layer || !element || !registered.has(layer) || rect.width <= 0 || rect.height <= 0)
583
+ continue;
584
+ let entry = byLayer.get(layer);
585
+ if (!entry) {
586
+ entry = { rects: [], element };
587
+ byLayer.set(layer, entry);
588
+ }
589
+ entry.rects.push(rect);
590
+ }
591
+ }
592
+ }
593
+ for (const layer of registered) {
594
+ const entry = byLayer.get(layer);
595
+ if (!entry) {
596
+ clear(layer);
597
+ continue;
598
+ }
599
+ layer.removeAttribute("data-lector-selection-active");
600
+ const color = getComputedStyle(
601
+ entry.element,
602
+ "::selection"
603
+ ).backgroundColor;
604
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
605
+ clear(layer);
606
+ continue;
607
+ }
608
+ const bounds = layer.getBoundingClientRect();
609
+ if (!bounds.width || !bounds.height || !layer.clientWidth || !layer.clientHeight) {
610
+ clear(layer);
611
+ continue;
612
+ }
613
+ const scaleX = bounds.width / layer.clientWidth;
614
+ const scaleY = bounds.height / layer.clientHeight;
615
+ const rects = mergeTextRuns(
616
+ entry.rects.map((rect) => ({
617
+ left: (rect.left - bounds.left) / scaleX,
618
+ top: (rect.top - bounds.top) / scaleY,
619
+ width: rect.width / scaleX,
620
+ height: rect.height / scaleY
621
+ }))
622
+ );
623
+ let overlay = overlays.get(layer);
624
+ if (!overlay) {
625
+ overlay = document.createElementNS(SVG_NS, "svg");
626
+ overlay.dataset.lectorSelection = "";
627
+ overlay.setAttribute("aria-hidden", "true");
628
+ overlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;user-select:none;transform:none;z-index:0;overflow:visible";
629
+ overlay.append(document.createElementNS(SVG_NS, "path"));
630
+ layer.append(overlay);
631
+ overlays.set(layer, overlay);
632
+ }
633
+ const path = overlay.firstElementChild;
634
+ path.setAttribute(
635
+ "d",
636
+ rects.map(
637
+ (rect) => `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h${-rect.width}Z`
638
+ ).join(" ")
639
+ );
640
+ path.setAttribute("fill", color);
641
+ if (!style) {
642
+ style = document.createElement("style");
643
+ style.textContent = ".textLayer[data-lector-selection-active] ::selection { background-color: transparent !important; }";
644
+ document.head.append(style);
645
+ }
646
+ layer.setAttribute("data-lector-selection-active", "");
647
+ }
648
+ };
649
+ return { update, clear };
650
+ };
651
+
652
+ // src/lib/text-selection.ts
653
+ var interactive = 'a, button, input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="button"], [role="link"], [role="textbox"], [data-lector-selection-ignore]';
654
+ var distance = (value, start, end) => Math.max(start - value, 0, value - end);
655
+ function nearestBox(boxes, x, y) {
656
+ let nearest;
657
+ let best = Number.POSITIVE_INFINITY;
658
+ for (const box of boxes) {
659
+ const { rect } = box;
660
+ const dx = distance(x, rect.left, rect.right);
661
+ const dy = distance(y, rect.top, rect.bottom);
662
+ const score = dy * dy + Math.min(dx, 30) ** 2 / 100 + dx / 1e4;
663
+ if (score < best) {
664
+ best = score;
665
+ nearest = box;
666
+ }
667
+ }
668
+ return nearest;
669
+ }
670
+ function caretInBox({ node, rect }, x, y) {
671
+ const doc = node.ownerDocument;
672
+ const px = Math.min(Math.max(x, rect.left + 0.1), rect.right - 0.1);
673
+ const py = Math.min(Math.max(y, rect.top + 0.1), rect.bottom - 0.1);
674
+ const caretDoc = doc;
675
+ const position = caretDoc.caretPositionFromPoint?.(px, py);
676
+ if (position?.offsetNode === node) return { node, offset: position.offset };
677
+ const range = caretDoc.caretRangeFromPoint?.(px, py);
678
+ if (range?.startContainer === node)
679
+ return { node, offset: range.startOffset };
680
+ const probe = doc.createRange();
681
+ let offset3 = 0;
682
+ let best = Number.POSITIVE_INFINITY;
683
+ let previous;
684
+ for (let i = 0; i <= node.length; ) {
685
+ probe.setStart(node, i);
686
+ probe.collapse(true);
687
+ const rects = Array.from(probe.getClientRects());
688
+ if (!rects.length && i === node.length && previous) {
689
+ probe.setStart(node, previous.offset);
690
+ probe.setEnd(node, i);
691
+ const glyph = probe.getBoundingClientRect();
692
+ const prior = previous.rect;
693
+ rects.push(
694
+ new DOMRect(
695
+ glyph.left + glyph.right - prior.right,
696
+ glyph.top + glyph.bottom - prior.bottom,
697
+ prior.width,
698
+ prior.height
699
+ )
700
+ );
701
+ }
702
+ for (const r2 of rects) {
703
+ const score = distance(x, r2.left, r2.right) ** 2 + distance(y, r2.top, r2.bottom) ** 2;
704
+ if (score < best) {
705
+ best = score;
706
+ offset3 = i;
707
+ }
708
+ }
709
+ if (rects[0]) previous = { offset: i, rect: rects[0] };
710
+ i += (node.data.codePointAt(i) ?? 0) > 65535 ? 2 : 1;
711
+ }
712
+ return { node, offset: offset3 };
713
+ }
714
+ function createSelectionManager(viewport, doc, isPersistent) {
715
+ const layers = /* @__PURE__ */ new Set();
716
+ const background = createSelectionBackground();
717
+ const container = viewport.current;
718
+ const win = doc.defaultView;
719
+ const controller = new AbortController();
720
+ const { signal } = controller;
721
+ let drag = null;
722
+ let boxes = null;
723
+ let scrollLeft = container?.scrollLeft;
724
+ let scrollTop = container?.scrollTop;
725
+ let frame = 0;
726
+ let mousePointer = true;
727
+ const invalidate = () => {
728
+ boxes = null;
729
+ if (drag && !frame) frame = win.requestAnimationFrame(tick);
730
+ };
731
+ const contains = (node) => !!node && [...layers].some((layer) => layer.contains(node));
732
+ const reset = () => {
733
+ drag = null;
734
+ boxes = null;
735
+ win.cancelAnimationFrame(frame);
736
+ frame = 0;
737
+ for (const layer of layers) layer.classList.remove("selecting");
738
+ };
739
+ const readBoxes = () => {
740
+ if (boxes) return boxes;
741
+ boxes = [];
742
+ for (const layer of layers) {
743
+ const walker = doc.createTreeWalker(layer, NodeFilter.SHOW_TEXT);
744
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
745
+ const parent = node.parentElement;
746
+ if (!node.textContent?.trim() || !parent || parent.closest('[role="img"], .endOfContent') || win.getComputedStyle(parent).userSelect === "none")
747
+ continue;
748
+ const range = doc.createRange();
749
+ range.selectNodeContents(node);
750
+ for (const rect of range.getClientRects()) {
751
+ if (rect.width > 0 && rect.height > 0)
752
+ boxes.push({ node, rect });
753
+ }
754
+ }
755
+ }
756
+ return boxes;
757
+ };
758
+ const resolve = (x, y) => {
759
+ if (scrollLeft !== container?.scrollLeft || scrollTop !== container?.scrollTop) {
760
+ boxes = null;
761
+ scrollLeft = container?.scrollLeft;
762
+ scrollTop = container?.scrollTop;
763
+ }
764
+ const box = nearestBox(readBoxes(), x, y);
765
+ return box ? caretInBox(box, x, y) : null;
766
+ };
767
+ const update = () => {
768
+ if (!drag) return;
769
+ if (!drag.anchor.node.isConnected) {
770
+ reset();
771
+ return;
772
+ }
773
+ const focus = resolve(drag.x, drag.y);
774
+ const selection = doc.getSelection();
775
+ if (!focus || !selection) return;
776
+ if (selection.anchorNode === drag.anchor.node && selection.anchorOffset === drag.anchor.offset && selection.focusNode === focus.node && selection.focusOffset === focus.offset)
777
+ return;
778
+ selection.setBaseAndExtent(
779
+ drag.anchor.node,
780
+ drag.anchor.offset,
781
+ focus.node,
782
+ focus.offset
783
+ );
784
+ };
785
+ const tick = () => {
786
+ frame = 0;
787
+ if (!drag) return;
788
+ let scrolled = false;
789
+ const container2 = viewport.current;
790
+ if (container2 && !isPersistent()) {
791
+ const r2 = container2.getBoundingClientRect();
792
+ const edge = 24;
793
+ const speed = (point, start, end) => point < start + edge ? -Math.min(18, (start + edge - point) / 2) : point > end - edge ? Math.min(18, (point - end + edge) / 2) : 0;
794
+ const dx = speed(drag.x, r2.left, r2.right);
795
+ const dy = speed(drag.y, r2.top, r2.bottom);
796
+ if (dx || dy) {
797
+ const left = container2.scrollLeft;
798
+ const top = container2.scrollTop;
799
+ container2.scrollBy(dx, dy);
800
+ if (container2.scrollLeft !== left || container2.scrollTop !== top) {
801
+ boxes = null;
802
+ scrolled = true;
803
+ }
804
+ }
805
+ }
806
+ update();
807
+ if (drag && scrolled && !frame) frame = win.requestAnimationFrame(tick);
808
+ };
809
+ doc.addEventListener(
810
+ "pointerdown",
811
+ (event) => {
812
+ mousePointer = event.pointerType === "mouse";
813
+ if (!mousePointer) reset();
814
+ },
815
+ { signal }
816
+ );
817
+ doc.addEventListener(
818
+ "mousedown",
819
+ (event) => {
820
+ reset();
821
+ if (!mousePointer || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.detail > 1)
822
+ return;
823
+ const target = event.target;
824
+ if (!(target instanceof Element) || target.closest(interactive)) return;
825
+ const onPage = [...layers].some(
826
+ (layer) => layer.parentElement?.contains(target)
827
+ );
828
+ if (!onPage && !(viewport.current === target || viewport.current?.contains(target) && target.contains([...layers][0] ?? null)))
829
+ return;
830
+ if (target instanceof HTMLElement && /auto|scroll/.test(win.getComputedStyle(target).overflow)) {
831
+ const r2 = target.getBoundingClientRect();
832
+ const scrollbarX = (target.offsetWidth - target.clientWidth - target.clientLeft * 2) * r2.width / target.offsetWidth;
833
+ const scrollbarY = (target.offsetHeight - target.clientHeight - target.clientTop * 2) * r2.height / target.offsetHeight;
834
+ if (event.clientX >= r2.right - scrollbarX || event.clientY >= r2.bottom - scrollbarY)
835
+ return;
836
+ }
837
+ const focus = resolve(event.clientX, event.clientY);
838
+ const selection = doc.getSelection();
839
+ if (!focus || !selection) return;
840
+ const anchor = event.shiftKey && contains(selection.anchorNode) ? { node: selection.anchorNode, offset: selection.anchorOffset } : focus;
841
+ event.preventDefault();
842
+ if (viewport.current && viewport.current.tabIndex >= 0)
843
+ viewport.current.focus({ preventScroll: true });
844
+ drag = { anchor, x: event.clientX, y: event.clientY };
845
+ for (const layer of layers) layer.classList.add("selecting");
846
+ update();
847
+ },
848
+ { signal }
849
+ );
850
+ doc.addEventListener(
851
+ "mousemove",
852
+ (event) => {
853
+ if (!drag) return;
854
+ if (!(event.buttons & 1)) {
855
+ reset();
856
+ return;
857
+ }
858
+ event.preventDefault();
859
+ drag.x = event.clientX;
860
+ drag.y = event.clientY;
861
+ update();
862
+ if (!frame) frame = win.requestAnimationFrame(tick);
863
+ },
864
+ { signal }
865
+ );
866
+ doc.addEventListener(
867
+ "mouseup",
868
+ (event) => {
869
+ if (drag && event.button === 0) {
870
+ drag.x = event.clientX;
871
+ drag.y = event.clientY;
872
+ update();
873
+ reset();
874
+ }
875
+ },
876
+ { signal }
877
+ );
878
+ doc.addEventListener(
879
+ "selectionchange",
880
+ () => background.update(doc.getSelection(), layers),
881
+ { signal }
882
+ );
883
+ doc.addEventListener("scroll", invalidate, {
884
+ signal,
885
+ capture: true,
886
+ passive: true
887
+ });
888
+ win.addEventListener("resize", invalidate, { signal });
889
+ win.addEventListener("blur", reset, { signal });
890
+ doc.addEventListener("pointercancel", reset, { signal });
891
+ doc.addEventListener(
892
+ "keydown",
893
+ (event) => {
894
+ if (event.key === "Escape") reset();
895
+ },
896
+ { signal }
897
+ );
898
+ return {
899
+ resolve,
900
+ add(layer) {
901
+ layers.add(layer);
902
+ if (container) viewportManagers.set(container, { resolve });
903
+ invalidate();
904
+ return () => {
905
+ background.clear(layer);
906
+ layers.delete(layer);
907
+ layer.classList.remove("selecting");
908
+ invalidate();
909
+ if (drag && layer.contains(drag.anchor.node)) reset();
910
+ if (!layers.size) {
911
+ reset();
912
+ controller.abort();
913
+ managers.delete(viewport);
914
+ if (container) viewportManagers.delete(container);
915
+ }
916
+ };
917
+ }
918
+ };
919
+ }
920
+ var viewportManagers = /* @__PURE__ */ new WeakMap();
921
+ function resolveTextSelectionCaret(viewport, x, y) {
922
+ return viewportManagers.get(viewport)?.resolve(x, y);
923
+ }
924
+ var managers = /* @__PURE__ */ new WeakMap();
925
+ function bindTextSelection(layer, viewport, isPersistent = () => false) {
926
+ let manager = managers.get(viewport);
927
+ if (!manager) {
928
+ manager = createSelectionManager(
929
+ viewport,
930
+ layer.ownerDocument,
931
+ isPersistent
932
+ );
933
+ managers.set(viewport, manager);
934
+ }
935
+ return manager.add(layer);
936
+ }
937
+
477
938
  // src/lib/pdf-selection.ts
478
939
  var sameAnchor = (a, b) => a.pageNumber === b.pageNumber && a.offset === b.offset;
479
940
  var compare = (a, b) => a.pageNumber - b.pageNumber || a.offset - b.offset;
@@ -504,6 +965,9 @@ var PDFSelectionController = class {
504
965
  this.listeners.delete(listener);
505
966
  };
506
967
  };
968
+ get isConnected() {
969
+ return this.container !== null;
970
+ }
507
971
  get isDragging() {
508
972
  return this.drag !== null;
509
973
  }
@@ -848,6 +1312,8 @@ var PDFSelectionController = class {
848
1312
  });
849
1313
  }
850
1314
  caret(x, y) {
1315
+ const geometry = resolveTextSelectionCaret(this.container, x, y);
1316
+ if (geometry) return this.anchorFor(geometry.node, geometry.offset);
851
1317
  const owner = this.container.ownerDocument;
852
1318
  const doc = owner;
853
1319
  const position = doc.caretPositionFromPoint?.(x, y);
@@ -936,13 +1402,15 @@ var PDFSelectionController = class {
936
1402
  const down = (event) => {
937
1403
  if (event.defaultPrevented) return;
938
1404
  const target = event.target instanceof Element ? event.target : null;
939
- if (target?.closest("button, a, [role=button], [data-selection-tooltip]"))
1405
+ if (target?.closest(
1406
+ 'button, a, input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role=button], [role=link], [role=textbox], [data-selection-tooltip], [data-lector-selection-ignore]'
1407
+ ))
940
1408
  return;
941
1409
  if (!container.contains(event.target)) {
942
1410
  this.clear();
943
1411
  return;
944
1412
  }
945
- if (event.defaultPrevented || event.button !== 0 || event.pointerType === "touch" || !container.contains(event.target))
1413
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType === "touch" || event.ctrlKey || event.metaKey || event.altKey || !container.contains(event.target))
946
1414
  return;
947
1415
  const point = this.caret(event.clientX, event.clientY);
948
1416
  if (!point) return;
@@ -3846,88 +4314,6 @@ var CanvasLayer = memo(
3846
4314
  );
3847
4315
  CanvasLayer.displayName = "CanvasLayer";
3848
4316
 
3849
- // src/lib/selection-rects.ts
3850
- var getTextNodeClientRects = (range) => {
3851
- const root = range.commonAncestorContainer;
3852
- const ownerDoc = root.ownerDocument ?? document;
3853
- if (root.nodeType === Node.TEXT_NODE) {
3854
- const parentElement = root.parentElement;
3855
- return Array.from(range.getClientRects()).map((rect) => ({
3856
- rect,
3857
- element: parentElement
3858
- }));
3859
- }
3860
- const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
3861
- acceptNode(node) {
3862
- if (!node.nodeValue || node.nodeValue.length === 0) {
3863
- return NodeFilter.FILTER_REJECT;
3864
- }
3865
- try {
3866
- return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
3867
- } catch {
3868
- return NodeFilter.FILTER_REJECT;
3869
- }
3870
- }
3871
- });
3872
- const results = [];
3873
- let current = walker.nextNode();
3874
- while (current) {
3875
- const textNode = current;
3876
- const parentElement = textNode.parentElement;
3877
- const length = textNode.nodeValue?.length ?? 0;
3878
- const isStartNode = textNode === range.startContainer;
3879
- const isEndNode = textNode === range.endContainer;
3880
- const start = isStartNode ? range.startOffset : 0;
3881
- const end = isEndNode ? range.endOffset : length;
3882
- if (end > start) {
3883
- const sub = ownerDoc.createRange();
3884
- try {
3885
- sub.setStart(textNode, start);
3886
- sub.setEnd(textNode, end);
3887
- const subRects = sub.getClientRects();
3888
- for (let i = 0; i < subRects.length; i++) {
3889
- const r2 = subRects[i];
3890
- if (r2) results.push({ rect: r2, element: parentElement });
3891
- }
3892
- } catch {
3893
- } finally {
3894
- sub.detach?.();
3895
- }
3896
- }
3897
- current = walker.nextNode();
3898
- }
3899
- return results;
3900
- };
3901
- var connected = (a, b) => {
3902
- const overlap = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
3903
- const gap = Math.max(a.left, b.left) - Math.min(a.left + a.width, b.left + b.width);
3904
- return overlap >= Math.min(a.height, b.height) / 2 && gap <= Math.min(a.height, b.height) * 0.5;
3905
- };
3906
- var mergeTextRuns = (rects) => {
3907
- const groups = [];
3908
- for (const rect of rects) {
3909
- const group = [rect];
3910
- for (let i = groups.length - 1; i >= 0; i--) {
3911
- if (groups[i].some((member) => connected(member, rect))) {
3912
- group.push(...groups[i]);
3913
- groups.splice(i, 1);
3914
- }
3915
- }
3916
- groups.push(group);
3917
- }
3918
- return groups.map((group) => {
3919
- const left = Math.min(...group.map((rect) => rect.left));
3920
- const top = Math.min(...group.map((rect) => rect.top));
3921
- return {
3922
- ...group[0],
3923
- left,
3924
- top,
3925
- width: Math.max(...group.map((rect) => rect.left + rect.width)) - left,
3926
- height: Math.max(...group.map((rect) => rect.top + rect.height)) - top
3927
- };
3928
- });
3929
- };
3930
-
3931
4317
  // src/lib/selection-text.ts
3932
4318
  function textLayer(node) {
3933
4319
  const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
@@ -4760,244 +5146,6 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
4760
5146
  }) });
4761
5147
  });
4762
5148
  HighlightLayer.displayName = "HighlightLayer";
4763
-
4764
- // src/lib/selection-background.ts
4765
- var SVG_NS = "http://www.w3.org/2000/svg";
4766
- var createSelectionBackground = () => {
4767
- const overlays = /* @__PURE__ */ new Map();
4768
- let style;
4769
- const clear = (layer) => {
4770
- overlays.get(layer)?.remove();
4771
- overlays.delete(layer);
4772
- layer.removeAttribute("data-lector-selection-active");
4773
- if (overlays.size === 0) {
4774
- style?.remove();
4775
- style = void 0;
4776
- }
4777
- };
4778
- const update = (selection, layers) => {
4779
- const registered = new Set(layers);
4780
- const byLayer = /* @__PURE__ */ new Map();
4781
- if (selection && !selection.isCollapsed) {
4782
- for (let i = 0; i < selection.rangeCount; i++) {
4783
- for (const { rect, element } of getTextNodeClientRects(
4784
- selection.getRangeAt(i)
4785
- )) {
4786
- const layer = element?.closest(".textLayer");
4787
- if (!layer || !element || !registered.has(layer) || rect.width <= 0 || rect.height <= 0)
4788
- continue;
4789
- let entry = byLayer.get(layer);
4790
- if (!entry) {
4791
- entry = { rects: [], element };
4792
- byLayer.set(layer, entry);
4793
- }
4794
- entry.rects.push(rect);
4795
- }
4796
- }
4797
- }
4798
- for (const layer of registered) {
4799
- const entry = byLayer.get(layer);
4800
- if (!entry) {
4801
- clear(layer);
4802
- continue;
4803
- }
4804
- layer.removeAttribute("data-lector-selection-active");
4805
- const color = getComputedStyle(
4806
- entry.element,
4807
- "::selection"
4808
- ).backgroundColor;
4809
- if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
4810
- clear(layer);
4811
- continue;
4812
- }
4813
- const bounds = layer.getBoundingClientRect();
4814
- if (!bounds.width || !bounds.height || !layer.clientWidth || !layer.clientHeight) {
4815
- clear(layer);
4816
- continue;
4817
- }
4818
- const scaleX = bounds.width / layer.clientWidth;
4819
- const scaleY = bounds.height / layer.clientHeight;
4820
- const rects = mergeTextRuns(
4821
- entry.rects.map((rect) => ({
4822
- left: (rect.left - bounds.left) / scaleX,
4823
- top: (rect.top - bounds.top) / scaleY,
4824
- width: rect.width / scaleX,
4825
- height: rect.height / scaleY
4826
- }))
4827
- );
4828
- let overlay = overlays.get(layer);
4829
- if (!overlay) {
4830
- overlay = document.createElementNS(SVG_NS, "svg");
4831
- overlay.dataset.lectorSelection = "";
4832
- overlay.setAttribute("aria-hidden", "true");
4833
- overlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;user-select:none;transform:none;z-index:0;overflow:visible";
4834
- overlay.append(document.createElementNS(SVG_NS, "path"));
4835
- layer.append(overlay);
4836
- overlays.set(layer, overlay);
4837
- }
4838
- const path = overlay.firstElementChild;
4839
- path.setAttribute(
4840
- "d",
4841
- rects.map(
4842
- (rect) => `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h${-rect.width}Z`
4843
- ).join(" ")
4844
- );
4845
- path.setAttribute("fill", color);
4846
- if (!style) {
4847
- style = document.createElement("style");
4848
- style.textContent = ".textLayer[data-lector-selection-active] ::selection { background-color: transparent !important; }";
4849
- document.head.append(style);
4850
- }
4851
- layer.setAttribute("data-lector-selection-active", "");
4852
- }
4853
- };
4854
- return { update, clear };
4855
- };
4856
-
4857
- // src/hooks/layers/useTextLayer.tsx
4858
- var createTextSelectionManager = () => {
4859
- const textLayers = /* @__PURE__ */ new Map();
4860
- const selectionBackground = createSelectionBackground();
4861
- let selectionChangeAbortController = null;
4862
- let isPointerDown = false;
4863
- let prevRange = null;
4864
- let isFirefox;
4865
- const removeGlobalSelectionListener = (textLayerDiv) => {
4866
- selectionBackground.clear(textLayerDiv);
4867
- textLayers.delete(textLayerDiv);
4868
- if (textLayers.size === 0) {
4869
- selectionChangeAbortController?.abort();
4870
- selectionChangeAbortController = null;
4871
- }
4872
- };
4873
- const enableGlobalSelectionListener = () => {
4874
- if (selectionChangeAbortController) {
4875
- return;
4876
- }
4877
- selectionChangeAbortController = new AbortController();
4878
- const { signal } = selectionChangeAbortController;
4879
- const reset = (endDiv, textLayer2) => {
4880
- if (endDiv.parentNode !== textLayer2) {
4881
- textLayer2.appendChild(endDiv);
4882
- }
4883
- endDiv.style.width = "";
4884
- endDiv.style.height = "";
4885
- textLayer2.classList.remove("selecting");
4886
- };
4887
- document.addEventListener(
4888
- "pointerdown",
4889
- () => {
4890
- isPointerDown = true;
4891
- },
4892
- { signal }
4893
- );
4894
- document.addEventListener(
4895
- "pointerup",
4896
- () => {
4897
- isPointerDown = false;
4898
- textLayers.forEach(reset);
4899
- },
4900
- { signal }
4901
- );
4902
- window.addEventListener(
4903
- "blur",
4904
- () => {
4905
- isPointerDown = false;
4906
- textLayers.forEach(reset);
4907
- },
4908
- { signal }
4909
- );
4910
- document.addEventListener(
4911
- "keyup",
4912
- () => {
4913
- if (!isPointerDown) {
4914
- textLayers.forEach(reset);
4915
- }
4916
- },
4917
- { signal }
4918
- );
4919
- document.addEventListener(
4920
- "selectionchange",
4921
- () => {
4922
- const selection = document.getSelection();
4923
- selectionBackground.update(selection, textLayers.keys());
4924
- if (!selection || selection.rangeCount === 0) {
4925
- textLayers.forEach(reset);
4926
- return;
4927
- }
4928
- const activeTextLayers = /* @__PURE__ */ new Set();
4929
- for (let i = 0; i < selection.rangeCount; i++) {
4930
- const range = selection.getRangeAt(i);
4931
- for (const textLayerDiv of textLayers.keys()) {
4932
- if (!activeTextLayers.has(textLayerDiv) && range.intersectsNode(textLayerDiv)) {
4933
- activeTextLayers.add(textLayerDiv);
4934
- }
4935
- }
4936
- }
4937
- for (const [textLayerDiv, endDiv] of textLayers) {
4938
- if (activeTextLayers.has(textLayerDiv)) {
4939
- textLayerDiv.classList.add("selecting");
4940
- } else {
4941
- reset(endDiv, textLayerDiv);
4942
- }
4943
- }
4944
- if (isFirefox === void 0) {
4945
- const firstTextLayer = textLayers.keys().next().value;
4946
- if (firstTextLayer) {
4947
- isFirefox = getComputedStyle(firstTextLayer).getPropertyValue(
4948
- "-moz-user-select"
4949
- ) === "none";
4950
- }
4951
- }
4952
- if (isFirefox) {
4953
- return;
4954
- }
4955
- try {
4956
- const range = selection.getRangeAt(0);
4957
- const modifyStart = prevRange && (range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 || range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0);
4958
- let anchor = modifyStart ? range.startContainer : range.endContainer;
4959
- if (anchor.nodeType === Node.TEXT_NODE) {
4960
- anchor = anchor.parentNode;
4961
- }
4962
- const parentTextLayer = anchor.parentElement?.closest(
4963
- ".textLayer"
4964
- );
4965
- const endDiv = textLayers.get(parentTextLayer);
4966
- if (endDiv && parentTextLayer) {
4967
- endDiv.style.width = parentTextLayer.style.width;
4968
- endDiv.style.height = parentTextLayer.style.height;
4969
- const insertTarget = modifyStart ? anchor : anchor.nextSibling;
4970
- if (anchor.parentElement && insertTarget) {
4971
- anchor.parentElement.insertBefore(endDiv, insertTarget);
4972
- }
4973
- }
4974
- prevRange = range.cloneRange();
4975
- } catch {
4976
- }
4977
- },
4978
- { signal }
4979
- );
4980
- };
4981
- const bindMouseEvents2 = (textLayerDiv, endOfContent) => {
4982
- if (textLayerDiv._textSelectionBound) {
4983
- return;
4984
- }
4985
- textLayerDiv._textSelectionBound = true;
4986
- textLayers.set(textLayerDiv, endOfContent);
4987
- enableGlobalSelectionListener();
4988
- const handleMouseDown = () => {
4989
- textLayerDiv.classList.add("selecting");
4990
- };
4991
- textLayerDiv.addEventListener("mousedown", handleMouseDown);
4992
- textLayerDiv._cleanupTextSelection = () => {
4993
- textLayerDiv.removeEventListener("mousedown", handleMouseDown);
4994
- removeGlobalSelectionListener(textLayerDiv);
4995
- delete textLayerDiv._textSelectionBound;
4996
- };
4997
- };
4998
- return bindMouseEvents2;
4999
- };
5000
- var bindMouseEvents = createTextSelectionManager();
5001
5149
  var TEXT_BUILD_IDLE_MS = 300;
5002
5150
  var TEXT_SCROLL_SETTLE_MS = 160;
5003
5151
  var VIEWPORT_ATTACH_MAX_FRAMES = 300;
@@ -5016,6 +5164,7 @@ var useTextLayer = () => {
5016
5164
  }
5017
5165
  let isCancelled = false;
5018
5166
  let unregisterSelection;
5167
+ let cleanupMouseSelection;
5019
5168
  let buildTimer = null;
5020
5169
  let unsubscribe = null;
5021
5170
  let built = false;
@@ -5043,10 +5192,11 @@ var useTextLayer = () => {
5043
5192
  if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer) {
5044
5193
  return;
5045
5194
  }
5046
- const endOfContent = document.createElement("div");
5047
- endOfContent.className = "endOfContent";
5048
- textContainer.appendChild(endOfContent);
5049
- bindMouseEvents(textContainer, endOfContent);
5195
+ cleanupMouseSelection = bindTextSelection(
5196
+ textContainer,
5197
+ viewportRef,
5198
+ () => selection?.isConnected ?? false
5199
+ );
5050
5200
  const viewport = pdfPageProxy.getViewport({ scale: 1 });
5051
5201
  const unrotated = pdfPageProxy.getViewport({ scale: 1, rotation: 0 });
5052
5202
  textContainer.style.width = `${unrotated.width}px`;
@@ -5121,10 +5271,7 @@ var useTextLayer = () => {
5121
5271
  textLayerRef.current.cancel();
5122
5272
  textLayerRef.current = null;
5123
5273
  }
5124
- if (textContainer?._cleanupTextSelection) {
5125
- textContainer._cleanupTextSelection();
5126
- delete textContainer._cleanupTextSelection;
5127
- }
5274
+ cleanupMouseSelection?.();
5128
5275
  };
5129
5276
  }, [
5130
5277
  pdfPageProxy.streamTextContent,
@@ -5727,9 +5874,9 @@ var useVisiblePage = ({
5727
5874
  let smallestDistance = Infinity;
5728
5875
  for (const item of virtualItems) {
5729
5876
  const itemCenter = item.start + item.size / 2;
5730
- const distance = Math.abs(itemCenter - viewportCenter);
5731
- if (distance < smallestDistance * 0.8) {
5732
- smallestDistance = distance;
5877
+ const distance2 = Math.abs(itemCenter - viewportCenter);
5878
+ if (distance2 < smallestDistance * 0.8) {
5879
+ smallestDistance = distance2;
5733
5880
  closestIndex = item.index;
5734
5881
  }
5735
5882
  }
@@ -5789,9 +5936,9 @@ var V = {
5789
5936
  v1[1] -= v2[1];
5790
5937
  }
5791
5938
  };
5792
- function rubberband(distance, dimension, constant) {
5793
- if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance, constant * 5);
5794
- return distance * dimension * constant / (dimension + constant * distance);
5939
+ function rubberband(distance2, dimension, constant) {
5940
+ if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance2, constant * 5);
5941
+ return distance2 * dimension * constant / (dimension + constant * distance2);
5795
5942
  }
5796
5943
  function rubberbandIfOutOfBounds(position, min, max, constant = 0.15) {
5797
5944
  if (constant === 0) return clamp2(position, min, max);
@@ -5934,12 +6081,12 @@ function distanceAngle(P1, P2) {
5934
6081
  const dy = P2.clientY - P1.clientY;
5935
6082
  const cx = (P2.clientX + P1.clientX) / 2;
5936
6083
  const cy = (P2.clientY + P1.clientY) / 2;
5937
- const distance = Math.hypot(dx, dy);
6084
+ const distance2 = Math.hypot(dx, dy);
5938
6085
  const angle = -(Math.atan2(dx, dy) * 180) / Math.PI;
5939
6086
  const origin = [cx, cy];
5940
6087
  return {
5941
6088
  angle,
5942
- distance,
6089
+ distance: distance2,
5943
6090
  origin
5944
6091
  };
5945
6092
  } catch (_unused) {
@@ -6733,12 +6880,12 @@ var dragConfigResolver = _objectSpread2(_objectSpread2({}, coordinatesConfigReso
6733
6880
  },
6734
6881
  swipe({
6735
6882
  velocity = DEFAULT_SWIPE_VELOCITY,
6736
- distance = DEFAULT_SWIPE_DISTANCE,
6883
+ distance: distance2 = DEFAULT_SWIPE_DISTANCE,
6737
6884
  duration = DEFAULT_SWIPE_DURATION
6738
6885
  } = {}) {
6739
6886
  return {
6740
6887
  velocity: this.transform(V.toVector(velocity)),
6741
- distance: this.transform(V.toVector(distance)),
6888
+ distance: this.transform(V.toVector(distance2)),
6742
6889
  duration
6743
6890
  };
6744
6891
  },
@@ -9305,7 +9452,7 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
9305
9452
  const query = normalizeSearchText(searchText, options).text;
9306
9453
  if (!query.trim()) return empty;
9307
9454
  const maxDistance = Math.floor(query.length * (1 - threshold));
9308
- const distance = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
9455
+ const distance2 = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
9309
9456
  const exactMatches = [];
9310
9457
  const fuzzyMatches = [];
9311
9458
  let exactCount = 0;
@@ -9374,7 +9521,7 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
9374
9521
  index = exactStart + query.length;
9375
9522
  continue;
9376
9523
  }
9377
- const edits = distance(lower, index);
9524
+ const edits = distance2(lower, index);
9378
9525
  if (edits > 0 && edits <= maxDistance) {
9379
9526
  fuzzyCount++;
9380
9527
  const score = 1 - edits / query.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.15.1",
3
+ "version": "3.15.2",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",