@anaralabs/lector 3.15.1 → 3.15.3

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>>;
@@ -830,7 +841,6 @@ export declare interface SearchResults {
830
841
  hasMoreResults: boolean;
831
842
  }
832
843
 
833
- /** A mounted text layer. Only this object owns DOM nodes; snapshots never do. */
834
844
  export declare class SelectionPage {
835
845
  readonly element: HTMLElement;
836
846
  readonly pageNumber: number;
@@ -1098,7 +1108,10 @@ export declare const useSelectionDimensions: () => {
1098
1108
 
1099
1109
  declare type ViewportCallback = () => void;
1100
1110
 
1101
- /** Materialize one missing selected page as text only, never as a page canvas. */
1111
+ declare type ViewportRef = {
1112
+ current: HTMLDivElement | null;
1113
+ };
1114
+
1102
1115
  export declare function withSelectionPage<T>(proxy: PDFPageProxy, owner: Document, signal: AbortSignal, read: (page: SelectionPage) => T): Promise<T>;
1103
1116
 
1104
1117
  export declare const ZoomIn: ({ ...props }: HTMLProps<HTMLButtonElement>) => JSX.Element;
package/dist/index.js CHANGED
@@ -267,7 +267,10 @@ var SelectionPage = class {
267
267
  point(offset3, end = false) {
268
268
  const index = Math.max(0, Math.min(offset3, this.content.text.length));
269
269
  const item = this.nodes.find((item2) => end ? item2.end >= index : item2.end > index) ?? this.nodes.at(-1);
270
- return item ? { node: item.node, offset: Math.max(0, index - item.start) } : null;
270
+ return item && this.element.contains(item.node) ? {
271
+ node: item.node,
272
+ offset: Math.max(0, Math.min(index - item.start, item.node.length))
273
+ } : null;
271
274
  }
272
275
  rects(from, to) {
273
276
  const bounds = this.element.getBoundingClientRect();
@@ -275,13 +278,17 @@ var SelectionPage = class {
275
278
  const scaleY = bounds.height / this.height || 1;
276
279
  const rects = [];
277
280
  for (const item of this.nodes) {
278
- const start = Math.max(from, item.start);
279
- const end = Math.min(to, item.end);
280
- if (end <= start || !item.node.textContent?.slice(start - item.start, end - item.start).trim())
281
- continue;
281
+ if (!this.element.contains(item.node)) continue;
282
+ const start = Math.max(0, from - item.start);
283
+ const end = Math.min(
284
+ to - item.start,
285
+ item.end - item.start,
286
+ item.node.length
287
+ );
288
+ if (end <= start || !item.node.data.slice(start, end).trim()) continue;
282
289
  const range = this.element.ownerDocument.createRange();
283
- range.setStart(item.node, start - item.start);
284
- range.setEnd(item.node, end - item.start);
290
+ range.setStart(item.node, start);
291
+ range.setEnd(item.node, end);
285
292
  for (const rect of range.getClientRects()) {
286
293
  if (rect.width <= 0 || rect.height <= 0) continue;
287
294
  rects.push({
@@ -474,6 +481,467 @@ function originalTextRange(normalized, start, end) {
474
481
  return { start, end };
475
482
  }
476
483
 
484
+ // src/lib/selection-rects.ts
485
+ var getTextNodeClientRects = (range) => {
486
+ const root = range.commonAncestorContainer;
487
+ const ownerDoc = root.ownerDocument ?? document;
488
+ if (root.nodeType === Node.TEXT_NODE) {
489
+ const parentElement = root.parentElement;
490
+ return Array.from(range.getClientRects()).map((rect) => ({
491
+ rect,
492
+ element: parentElement
493
+ }));
494
+ }
495
+ const walker = ownerDoc.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
496
+ acceptNode(node) {
497
+ if (!node.nodeValue || node.nodeValue.length === 0) {
498
+ return NodeFilter.FILTER_REJECT;
499
+ }
500
+ try {
501
+ return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
502
+ } catch {
503
+ return NodeFilter.FILTER_REJECT;
504
+ }
505
+ }
506
+ });
507
+ const results = [];
508
+ let current = walker.nextNode();
509
+ while (current) {
510
+ const textNode = current;
511
+ const parentElement = textNode.parentElement;
512
+ const length = textNode.nodeValue?.length ?? 0;
513
+ const isStartNode = textNode === range.startContainer;
514
+ const isEndNode = textNode === range.endContainer;
515
+ const start = isStartNode ? range.startOffset : 0;
516
+ const end = isEndNode ? range.endOffset : length;
517
+ if (end > start) {
518
+ const sub = ownerDoc.createRange();
519
+ try {
520
+ sub.setStart(textNode, start);
521
+ sub.setEnd(textNode, end);
522
+ const subRects = sub.getClientRects();
523
+ for (let i = 0; i < subRects.length; i++) {
524
+ const r2 = subRects[i];
525
+ if (r2) results.push({ rect: r2, element: parentElement });
526
+ }
527
+ } catch {
528
+ } finally {
529
+ sub.detach?.();
530
+ }
531
+ }
532
+ current = walker.nextNode();
533
+ }
534
+ return results;
535
+ };
536
+ var connected = (a, b) => {
537
+ const overlap = Math.min(a.top + a.height, b.top + b.height) - Math.max(a.top, b.top);
538
+ const gap = Math.max(a.left, b.left) - Math.min(a.left + a.width, b.left + b.width);
539
+ return overlap >= Math.min(a.height, b.height) / 2 && gap <= Math.min(a.height, b.height) * 0.5;
540
+ };
541
+ var mergeTextRuns = (rects) => {
542
+ const groups = [];
543
+ for (const rect of rects) {
544
+ const group = [rect];
545
+ for (let i = groups.length - 1; i >= 0; i--) {
546
+ if (groups[i].some((member) => connected(member, rect))) {
547
+ group.push(...groups[i]);
548
+ groups.splice(i, 1);
549
+ }
550
+ }
551
+ groups.push(group);
552
+ }
553
+ return groups.map((group) => {
554
+ const left = Math.min(...group.map((rect) => rect.left));
555
+ const top = Math.min(...group.map((rect) => rect.top));
556
+ return {
557
+ ...group[0],
558
+ left,
559
+ top,
560
+ width: Math.max(...group.map((rect) => rect.left + rect.width)) - left,
561
+ height: Math.max(...group.map((rect) => rect.top + rect.height)) - top
562
+ };
563
+ });
564
+ };
565
+
566
+ // src/lib/selection-background.ts
567
+ var SVG_NS = "http://www.w3.org/2000/svg";
568
+ var createSelectionBackground = () => {
569
+ const overlays = /* @__PURE__ */ new Map();
570
+ let style;
571
+ const clear = (layer) => {
572
+ overlays.get(layer)?.remove();
573
+ overlays.delete(layer);
574
+ layer.removeAttribute("data-lector-selection-active");
575
+ if (overlays.size === 0) {
576
+ style?.remove();
577
+ style = void 0;
578
+ }
579
+ };
580
+ const update = (selection, layers) => {
581
+ const registered = new Set(layers);
582
+ const byLayer = /* @__PURE__ */ new Map();
583
+ if (selection && !selection.isCollapsed) {
584
+ for (let i = 0; i < selection.rangeCount; i++) {
585
+ for (const { rect, element } of getTextNodeClientRects(
586
+ selection.getRangeAt(i)
587
+ )) {
588
+ const layer = element?.closest(".textLayer");
589
+ if (!layer || !element || !registered.has(layer) || rect.width <= 0 || rect.height <= 0)
590
+ continue;
591
+ let entry = byLayer.get(layer);
592
+ if (!entry) {
593
+ entry = { rects: [], element };
594
+ byLayer.set(layer, entry);
595
+ }
596
+ entry.rects.push(rect);
597
+ }
598
+ }
599
+ }
600
+ for (const layer of registered) {
601
+ const entry = byLayer.get(layer);
602
+ if (!entry) {
603
+ clear(layer);
604
+ continue;
605
+ }
606
+ layer.removeAttribute("data-lector-selection-active");
607
+ const color = getComputedStyle(
608
+ entry.element,
609
+ "::selection"
610
+ ).backgroundColor;
611
+ if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
612
+ clear(layer);
613
+ continue;
614
+ }
615
+ const bounds = layer.getBoundingClientRect();
616
+ if (!bounds.width || !bounds.height || !layer.clientWidth || !layer.clientHeight) {
617
+ clear(layer);
618
+ continue;
619
+ }
620
+ const scaleX = bounds.width / layer.clientWidth;
621
+ const scaleY = bounds.height / layer.clientHeight;
622
+ const rects = mergeTextRuns(
623
+ entry.rects.map((rect) => ({
624
+ left: (rect.left - bounds.left) / scaleX,
625
+ top: (rect.top - bounds.top) / scaleY,
626
+ width: rect.width / scaleX,
627
+ height: rect.height / scaleY
628
+ }))
629
+ );
630
+ let overlay = overlays.get(layer);
631
+ if (!overlay) {
632
+ overlay = document.createElementNS(SVG_NS, "svg");
633
+ overlay.dataset.lectorSelection = "";
634
+ overlay.setAttribute("aria-hidden", "true");
635
+ overlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;user-select:none;transform:none;z-index:0;overflow:visible";
636
+ overlay.append(document.createElementNS(SVG_NS, "path"));
637
+ layer.append(overlay);
638
+ overlays.set(layer, overlay);
639
+ }
640
+ const path = overlay.firstElementChild;
641
+ path.setAttribute(
642
+ "d",
643
+ rects.map(
644
+ (rect) => `M${rect.left} ${rect.top}h${rect.width}v${rect.height}h${-rect.width}Z`
645
+ ).join(" ")
646
+ );
647
+ path.setAttribute("fill", color);
648
+ if (!style) {
649
+ style = document.createElement("style");
650
+ style.textContent = ".textLayer[data-lector-selection-active] ::selection { background-color: transparent !important; }";
651
+ document.head.append(style);
652
+ }
653
+ layer.setAttribute("data-lector-selection-active", "");
654
+ }
655
+ };
656
+ return { update, clear };
657
+ };
658
+
659
+ // src/lib/text-selection.ts
660
+ var interactive = 'a, button, input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="button"], [role="link"], [role="textbox"], [data-lector-selection-ignore]';
661
+ var distance = (value, start, end) => Math.max(start - value, 0, value - end);
662
+ function nearestBox(boxes, x, y) {
663
+ let nearest;
664
+ let best = Number.POSITIVE_INFINITY;
665
+ for (const box of boxes) {
666
+ const { rect } = box;
667
+ const dx = distance(x, rect.left, rect.right);
668
+ const dy = distance(y, rect.top, rect.bottom);
669
+ const score = dy * dy + Math.min(dx, 30) ** 2 / 100 + dx / 1e4;
670
+ if (score < best) {
671
+ best = score;
672
+ nearest = box;
673
+ }
674
+ }
675
+ return nearest;
676
+ }
677
+ function caretInBox({ node, rect }, x, y) {
678
+ const doc = node.ownerDocument;
679
+ const px = Math.min(Math.max(x, rect.left + 0.1), rect.right - 0.1);
680
+ const py = Math.min(Math.max(y, rect.top + 0.1), rect.bottom - 0.1);
681
+ const caretDoc = doc;
682
+ const position = caretDoc.caretPositionFromPoint?.(px, py);
683
+ if (position?.offsetNode === node) return { node, offset: position.offset };
684
+ const range = caretDoc.caretRangeFromPoint?.(px, py);
685
+ if (range?.startContainer === node)
686
+ return { node, offset: range.startOffset };
687
+ const probe = doc.createRange();
688
+ let offset3 = 0;
689
+ let best = Number.POSITIVE_INFINITY;
690
+ let previous;
691
+ for (let i = 0; i <= node.length; ) {
692
+ probe.setStart(node, i);
693
+ probe.collapse(true);
694
+ const rects = Array.from(probe.getClientRects());
695
+ if (!rects.length && i === node.length && previous) {
696
+ probe.setStart(node, previous.offset);
697
+ probe.setEnd(node, i);
698
+ const glyph = probe.getBoundingClientRect();
699
+ const prior = previous.rect;
700
+ rects.push(
701
+ new DOMRect(
702
+ glyph.left + glyph.right - prior.right,
703
+ glyph.top + glyph.bottom - prior.bottom,
704
+ prior.width,
705
+ prior.height
706
+ )
707
+ );
708
+ }
709
+ for (const r2 of rects) {
710
+ const score = distance(x, r2.left, r2.right) ** 2 + distance(y, r2.top, r2.bottom) ** 2;
711
+ if (score < best) {
712
+ best = score;
713
+ offset3 = i;
714
+ }
715
+ }
716
+ if (rects[0]) previous = { offset: i, rect: rects[0] };
717
+ i += (node.data.codePointAt(i) ?? 0) > 65535 ? 2 : 1;
718
+ }
719
+ return { node, offset: offset3 };
720
+ }
721
+ function createSelectionManager(viewport, doc, isPersistent) {
722
+ const layers = /* @__PURE__ */ new Set();
723
+ const background = createSelectionBackground();
724
+ const container = viewport.current;
725
+ const win = doc.defaultView;
726
+ const controller = new AbortController();
727
+ const { signal } = controller;
728
+ let drag = null;
729
+ let boxes = null;
730
+ let scrollLeft = container?.scrollLeft;
731
+ let scrollTop = container?.scrollTop;
732
+ let frame = 0;
733
+ let mousePointer = true;
734
+ const invalidate = () => {
735
+ boxes = null;
736
+ if (drag && !frame) frame = win.requestAnimationFrame(tick);
737
+ };
738
+ const contains = (node) => !!node && [...layers].some((layer) => layer.contains(node));
739
+ const reset = () => {
740
+ drag = null;
741
+ boxes = null;
742
+ win.cancelAnimationFrame(frame);
743
+ frame = 0;
744
+ for (const layer of layers) layer.classList.remove("selecting");
745
+ };
746
+ const readBoxes = () => {
747
+ if (boxes) return boxes;
748
+ boxes = [];
749
+ for (const layer of layers) {
750
+ const walker = doc.createTreeWalker(layer, NodeFilter.SHOW_TEXT);
751
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
752
+ const parent = node.parentElement;
753
+ if (!node.textContent?.trim() || !parent || parent.closest('[role="img"], .endOfContent') || win.getComputedStyle(parent).userSelect === "none")
754
+ continue;
755
+ const range = doc.createRange();
756
+ range.selectNodeContents(node);
757
+ for (const rect of range.getClientRects()) {
758
+ if (rect.width > 0 && rect.height > 0)
759
+ boxes.push({ node, rect });
760
+ }
761
+ }
762
+ }
763
+ return boxes;
764
+ };
765
+ const resolve = (x, y) => {
766
+ if (scrollLeft !== container?.scrollLeft || scrollTop !== container?.scrollTop) {
767
+ boxes = null;
768
+ scrollLeft = container?.scrollLeft;
769
+ scrollTop = container?.scrollTop;
770
+ }
771
+ const box = nearestBox(readBoxes(), x, y);
772
+ return box ? caretInBox(box, x, y) : null;
773
+ };
774
+ const update = () => {
775
+ if (!drag) return;
776
+ if (!drag.anchor.node.isConnected) {
777
+ reset();
778
+ return;
779
+ }
780
+ const focus = resolve(drag.x, drag.y);
781
+ const selection = doc.getSelection();
782
+ if (!focus || !selection) return;
783
+ if (selection.anchorNode === drag.anchor.node && selection.anchorOffset === drag.anchor.offset && selection.focusNode === focus.node && selection.focusOffset === focus.offset)
784
+ return;
785
+ selection.setBaseAndExtent(
786
+ drag.anchor.node,
787
+ drag.anchor.offset,
788
+ focus.node,
789
+ focus.offset
790
+ );
791
+ };
792
+ const tick = () => {
793
+ frame = 0;
794
+ if (!drag) return;
795
+ let scrolled = false;
796
+ const container2 = viewport.current;
797
+ if (container2 && !isPersistent()) {
798
+ const r2 = container2.getBoundingClientRect();
799
+ const edge = 24;
800
+ 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;
801
+ const dx = speed(drag.x, r2.left, r2.right);
802
+ const dy = speed(drag.y, r2.top, r2.bottom);
803
+ if (dx || dy) {
804
+ const left = container2.scrollLeft;
805
+ const top = container2.scrollTop;
806
+ container2.scrollBy(dx, dy);
807
+ if (container2.scrollLeft !== left || container2.scrollTop !== top) {
808
+ boxes = null;
809
+ scrolled = true;
810
+ }
811
+ }
812
+ }
813
+ update();
814
+ if (drag && scrolled && !frame) frame = win.requestAnimationFrame(tick);
815
+ };
816
+ doc.addEventListener(
817
+ "pointerdown",
818
+ (event) => {
819
+ mousePointer = event.pointerType === "mouse";
820
+ if (!mousePointer) reset();
821
+ },
822
+ { signal }
823
+ );
824
+ doc.addEventListener(
825
+ "mousedown",
826
+ (event) => {
827
+ reset();
828
+ if (!mousePointer || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey || event.detail > 1)
829
+ return;
830
+ const target = event.target;
831
+ if (!(target instanceof Element) || target.closest(interactive)) return;
832
+ const onPage = [...layers].some(
833
+ (layer) => layer.parentElement?.contains(target)
834
+ );
835
+ if (!onPage && !(viewport.current === target || viewport.current?.contains(target) && target.contains([...layers][0] ?? null)))
836
+ return;
837
+ if (target instanceof HTMLElement && /auto|scroll/.test(win.getComputedStyle(target).overflow)) {
838
+ const r2 = target.getBoundingClientRect();
839
+ const scrollbarX = (target.offsetWidth - target.clientWidth - target.clientLeft * 2) * r2.width / target.offsetWidth;
840
+ const scrollbarY = (target.offsetHeight - target.clientHeight - target.clientTop * 2) * r2.height / target.offsetHeight;
841
+ if (event.clientX >= r2.right - scrollbarX || event.clientY >= r2.bottom - scrollbarY)
842
+ return;
843
+ }
844
+ const focus = resolve(event.clientX, event.clientY);
845
+ const selection = doc.getSelection();
846
+ if (!focus || !selection) return;
847
+ const anchor = event.shiftKey && contains(selection.anchorNode) ? { node: selection.anchorNode, offset: selection.anchorOffset } : focus;
848
+ event.preventDefault();
849
+ if (viewport.current && viewport.current.tabIndex >= 0)
850
+ viewport.current.focus({ preventScroll: true });
851
+ drag = { anchor, x: event.clientX, y: event.clientY };
852
+ for (const layer of layers) layer.classList.add("selecting");
853
+ update();
854
+ },
855
+ { signal }
856
+ );
857
+ doc.addEventListener(
858
+ "mousemove",
859
+ (event) => {
860
+ if (!drag) return;
861
+ if (!(event.buttons & 1)) {
862
+ reset();
863
+ return;
864
+ }
865
+ event.preventDefault();
866
+ drag.x = event.clientX;
867
+ drag.y = event.clientY;
868
+ update();
869
+ if (!frame) frame = win.requestAnimationFrame(tick);
870
+ },
871
+ { signal }
872
+ );
873
+ doc.addEventListener(
874
+ "mouseup",
875
+ (event) => {
876
+ if (drag && event.button === 0) {
877
+ drag.x = event.clientX;
878
+ drag.y = event.clientY;
879
+ update();
880
+ reset();
881
+ }
882
+ },
883
+ { signal }
884
+ );
885
+ doc.addEventListener(
886
+ "selectionchange",
887
+ () => background.update(doc.getSelection(), layers),
888
+ { signal }
889
+ );
890
+ doc.addEventListener("scroll", invalidate, {
891
+ signal,
892
+ capture: true,
893
+ passive: true
894
+ });
895
+ win.addEventListener("resize", invalidate, { signal });
896
+ win.addEventListener("blur", reset, { signal });
897
+ doc.addEventListener("pointercancel", reset, { signal });
898
+ doc.addEventListener(
899
+ "keydown",
900
+ (event) => {
901
+ if (event.key === "Escape") reset();
902
+ },
903
+ { signal }
904
+ );
905
+ return {
906
+ resolve,
907
+ add(layer) {
908
+ layers.add(layer);
909
+ if (container) viewportManagers.set(container, { resolve });
910
+ invalidate();
911
+ return () => {
912
+ background.clear(layer);
913
+ layers.delete(layer);
914
+ layer.classList.remove("selecting");
915
+ invalidate();
916
+ if (drag && layer.contains(drag.anchor.node)) reset();
917
+ if (!layers.size) {
918
+ reset();
919
+ controller.abort();
920
+ managers.delete(viewport);
921
+ if (container) viewportManagers.delete(container);
922
+ }
923
+ };
924
+ }
925
+ };
926
+ }
927
+ var viewportManagers = /* @__PURE__ */ new WeakMap();
928
+ function resolveTextSelectionCaret(viewport, x, y) {
929
+ return viewportManagers.get(viewport)?.resolve(x, y);
930
+ }
931
+ var managers = /* @__PURE__ */ new WeakMap();
932
+ function bindTextSelection(layer, viewport, isPersistent = () => false) {
933
+ let manager = managers.get(viewport);
934
+ if (!manager) {
935
+ manager = createSelectionManager(
936
+ viewport,
937
+ layer.ownerDocument,
938
+ isPersistent
939
+ );
940
+ managers.set(viewport, manager);
941
+ }
942
+ return manager.add(layer);
943
+ }
944
+
477
945
  // src/lib/pdf-selection.ts
478
946
  var sameAnchor = (a, b) => a.pageNumber === b.pageNumber && a.offset === b.offset;
479
947
  var compare = (a, b) => a.pageNumber - b.pageNumber || a.offset - b.offset;
@@ -504,6 +972,9 @@ var PDFSelectionController = class {
504
972
  this.listeners.delete(listener);
505
973
  };
506
974
  };
975
+ get isConnected() {
976
+ return this.container !== null;
977
+ }
507
978
  get isDragging() {
508
979
  return this.drag !== null;
509
980
  }
@@ -848,6 +1319,8 @@ var PDFSelectionController = class {
848
1319
  });
849
1320
  }
850
1321
  caret(x, y) {
1322
+ const geometry = resolveTextSelectionCaret(this.container, x, y);
1323
+ if (geometry) return this.anchorFor(geometry.node, geometry.offset);
851
1324
  const owner = this.container.ownerDocument;
852
1325
  const doc = owner;
853
1326
  const position = doc.caretPositionFromPoint?.(x, y);
@@ -936,13 +1409,15 @@ var PDFSelectionController = class {
936
1409
  const down = (event) => {
937
1410
  if (event.defaultPrevented) return;
938
1411
  const target = event.target instanceof Element ? event.target : null;
939
- if (target?.closest("button, a, [role=button], [data-selection-tooltip]"))
1412
+ if (target?.closest(
1413
+ 'button, a, input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role=button], [role=link], [role=textbox], [data-selection-tooltip], [data-lector-selection-ignore]'
1414
+ ))
940
1415
  return;
941
1416
  if (!container.contains(event.target)) {
942
1417
  this.clear();
943
1418
  return;
944
1419
  }
945
- if (event.defaultPrevented || event.button !== 0 || event.pointerType === "touch" || !container.contains(event.target))
1420
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType === "touch" || event.ctrlKey || event.metaKey || event.altKey || !container.contains(event.target))
946
1421
  return;
947
1422
  const point = this.caret(event.clientX, event.clientY);
948
1423
  if (!point) return;
@@ -3846,88 +4321,6 @@ var CanvasLayer = memo(
3846
4321
  );
3847
4322
  CanvasLayer.displayName = "CanvasLayer";
3848
4323
 
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
4324
  // src/lib/selection-text.ts
3932
4325
  function textLayer(node) {
3933
4326
  const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
@@ -4760,244 +5153,6 @@ var HighlightLayer = forwardRef(({ asChild, className, style, ...props }, ref) =
4760
5153
  }) });
4761
5154
  });
4762
5155
  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
5156
  var TEXT_BUILD_IDLE_MS = 300;
5002
5157
  var TEXT_SCROLL_SETTLE_MS = 160;
5003
5158
  var VIEWPORT_ATTACH_MAX_FRAMES = 300;
@@ -5016,6 +5171,7 @@ var useTextLayer = () => {
5016
5171
  }
5017
5172
  let isCancelled = false;
5018
5173
  let unregisterSelection;
5174
+ let cleanupMouseSelection;
5019
5175
  let buildTimer = null;
5020
5176
  let unsubscribe = null;
5021
5177
  let built = false;
@@ -5043,10 +5199,11 @@ var useTextLayer = () => {
5043
5199
  if (isCancelled || gen !== buildGen || textContainerRef.current !== textContainer) {
5044
5200
  return;
5045
5201
  }
5046
- const endOfContent = document.createElement("div");
5047
- endOfContent.className = "endOfContent";
5048
- textContainer.appendChild(endOfContent);
5049
- bindMouseEvents(textContainer, endOfContent);
5202
+ cleanupMouseSelection = bindTextSelection(
5203
+ textContainer,
5204
+ viewportRef,
5205
+ () => selection?.isConnected ?? false
5206
+ );
5050
5207
  const viewport = pdfPageProxy.getViewport({ scale: 1 });
5051
5208
  const unrotated = pdfPageProxy.getViewport({ scale: 1, rotation: 0 });
5052
5209
  textContainer.style.width = `${unrotated.width}px`;
@@ -5121,10 +5278,7 @@ var useTextLayer = () => {
5121
5278
  textLayerRef.current.cancel();
5122
5279
  textLayerRef.current = null;
5123
5280
  }
5124
- if (textContainer?._cleanupTextSelection) {
5125
- textContainer._cleanupTextSelection();
5126
- delete textContainer._cleanupTextSelection;
5127
- }
5281
+ cleanupMouseSelection?.();
5128
5282
  };
5129
5283
  }, [
5130
5284
  pdfPageProxy.streamTextContent,
@@ -5727,9 +5881,9 @@ var useVisiblePage = ({
5727
5881
  let smallestDistance = Infinity;
5728
5882
  for (const item of virtualItems) {
5729
5883
  const itemCenter = item.start + item.size / 2;
5730
- const distance = Math.abs(itemCenter - viewportCenter);
5731
- if (distance < smallestDistance * 0.8) {
5732
- smallestDistance = distance;
5884
+ const distance2 = Math.abs(itemCenter - viewportCenter);
5885
+ if (distance2 < smallestDistance * 0.8) {
5886
+ smallestDistance = distance2;
5733
5887
  closestIndex = item.index;
5734
5888
  }
5735
5889
  }
@@ -5789,9 +5943,9 @@ var V = {
5789
5943
  v1[1] -= v2[1];
5790
5944
  }
5791
5945
  };
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);
5946
+ function rubberband(distance2, dimension, constant) {
5947
+ if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance2, constant * 5);
5948
+ return distance2 * dimension * constant / (dimension + constant * distance2);
5795
5949
  }
5796
5950
  function rubberbandIfOutOfBounds(position, min, max, constant = 0.15) {
5797
5951
  if (constant === 0) return clamp2(position, min, max);
@@ -5934,12 +6088,12 @@ function distanceAngle(P1, P2) {
5934
6088
  const dy = P2.clientY - P1.clientY;
5935
6089
  const cx = (P2.clientX + P1.clientX) / 2;
5936
6090
  const cy = (P2.clientY + P1.clientY) / 2;
5937
- const distance = Math.hypot(dx, dy);
6091
+ const distance2 = Math.hypot(dx, dy);
5938
6092
  const angle = -(Math.atan2(dx, dy) * 180) / Math.PI;
5939
6093
  const origin = [cx, cy];
5940
6094
  return {
5941
6095
  angle,
5942
- distance,
6096
+ distance: distance2,
5943
6097
  origin
5944
6098
  };
5945
6099
  } catch (_unused) {
@@ -6733,12 +6887,12 @@ var dragConfigResolver = _objectSpread2(_objectSpread2({}, coordinatesConfigReso
6733
6887
  },
6734
6888
  swipe({
6735
6889
  velocity = DEFAULT_SWIPE_VELOCITY,
6736
- distance = DEFAULT_SWIPE_DISTANCE,
6890
+ distance: distance2 = DEFAULT_SWIPE_DISTANCE,
6737
6891
  duration = DEFAULT_SWIPE_DURATION
6738
6892
  } = {}) {
6739
6893
  return {
6740
6894
  velocity: this.transform(V.toVector(velocity)),
6741
- distance: this.transform(V.toVector(distance)),
6895
+ distance: this.transform(V.toVector(distance2)),
6742
6896
  duration
6743
6897
  };
6744
6898
  },
@@ -9305,7 +9459,7 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
9305
9459
  const query = normalizeSearchText(searchText, options).text;
9306
9460
  if (!query.trim()) return empty;
9307
9461
  const maxDistance = Math.floor(query.length * (1 - threshold));
9308
- const distance = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
9462
+ const distance2 = maxDistance > 0 ? createBoundedDistance(query, maxDistance) : null;
9309
9463
  const exactMatches = [];
9310
9464
  const fuzzyMatches = [];
9311
9465
  let exactCount = 0;
@@ -9374,7 +9528,7 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
9374
9528
  index = exactStart + query.length;
9375
9529
  continue;
9376
9530
  }
9377
- const edits = distance(lower, index);
9531
+ const edits = distance2(lower, index);
9378
9532
  if (edits > 0 && edits <= maxDistance) {
9379
9533
  fuzzyCount++;
9380
9534
  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.3",
4
4
  "description": "A headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",