@anaralabs/lector 3.3.0 → 3.4.0

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.
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { AnnotationLayer, TextLayer, getDocument } from 'pdfjs-dist';
4
- import React, { createContext, forwardRef, useContext, useCallback, cloneElement, useState, useRef, useEffect, createRef, useLayoutEffect } from 'react';
4
+ import React, { createContext, forwardRef, useContext, useState, useRef, useCallback, useEffect, cloneElement, createRef, useLayoutEffect } from 'react';
5
5
  import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual';
6
6
  import { useStore, createStore } from 'zustand';
7
7
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
8
8
  import { useDebounce } from 'use-debounce';
9
- import { Slot } from '@radix-ui/react-slot';
9
+ import { v4 } from 'uuid';
10
10
  import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions } from '@floating-ui/react';
11
+ import { Slot } from '@radix-ui/react-slot';
11
12
 
12
13
  // ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
13
14
  function r(e) {
@@ -130,7 +131,16 @@ var PDFStore = createZustandContext(
130
131
  set({
131
132
  customSelectionRects: val
132
133
  });
133
- }
134
+ },
135
+ coloredHighlights: [],
136
+ addColoredHighlight: (value) => set((prevState) => ({
137
+ coloredHighlights: [...prevState.coloredHighlights, value]
138
+ })),
139
+ deleteColoredHighlight: (uuid) => set((prevState) => ({
140
+ coloredHighlights: prevState.coloredHighlights.filter(
141
+ (rect) => rect.uuid !== uuid
142
+ )
143
+ }))
134
144
  }));
135
145
  }
136
146
  );
@@ -160,10 +170,74 @@ var cancellable = (promise) => {
160
170
  }
161
171
  };
162
172
  };
173
+
174
+ // src/hooks/pages/usePdfJump.tsx
175
+ var usePdfJump = () => {
176
+ const virtualizer = usePdf((state) => state.virtualizer);
177
+ const setHighlight = usePdf((state) => state.setHighlight);
178
+ const jumpToPage = (pageIndex, options) => {
179
+ if (!virtualizer) return;
180
+ const defaultOptions = {
181
+ align: "start",
182
+ behavior: "smooth"
183
+ };
184
+ const finalOptions = { ...defaultOptions, ...options };
185
+ virtualizer.scrollToIndex(pageIndex - 1, finalOptions);
186
+ };
187
+ const jumpToOffset = (offset2) => {
188
+ if (!virtualizer) return;
189
+ virtualizer.scrollToOffset(offset2, {
190
+ align: "start",
191
+ behavior: "smooth"
192
+ });
193
+ };
194
+ const jumpToHighlightRects = (rects, type, align = "start", additionalOffset = 0) => {
195
+ if (!virtualizer) return;
196
+ setHighlight(rects);
197
+ const firstPage = Math.min(...rects.map((rect) => rect.pageNumber));
198
+ const pageOffset = virtualizer.getOffsetForIndex(firstPage - 1, "start");
199
+ if (pageOffset === null) return;
200
+ const targetRect = rects.find((rect) => rect.pageNumber === firstPage);
201
+ if (!targetRect) return;
202
+ const isNumber = pageOffset?.[0] != null;
203
+ if (!isNumber) return;
204
+ const pageStart = pageOffset[0] ?? 0;
205
+ let rectTop;
206
+ let rectHeight;
207
+ if (type === "percent") {
208
+ const estimatePageHeight = virtualizer.options.estimateSize(
209
+ firstPage - 1
210
+ );
211
+ rectTop = targetRect.top / 100 * estimatePageHeight;
212
+ rectHeight = targetRect.height / 100 * estimatePageHeight;
213
+ } else {
214
+ rectTop = targetRect.top;
215
+ rectHeight = targetRect.height;
216
+ }
217
+ let scrollOffset;
218
+ if (align === "center") {
219
+ const viewportHeight = virtualizer.scrollElement?.clientHeight || 0;
220
+ const rectCenter = pageStart + rectTop + rectHeight / 2;
221
+ scrollOffset = rectCenter - viewportHeight / 2;
222
+ } else {
223
+ scrollOffset = pageStart + rectTop;
224
+ }
225
+ scrollOffset += additionalOffset;
226
+ const adjustedOffset = Math.max(0, scrollOffset);
227
+ virtualizer.scrollToOffset(adjustedOffset, {
228
+ align: "start",
229
+ // Always use start when we've calculated our own centering
230
+ behavior: "smooth"
231
+ });
232
+ };
233
+ return { jumpToPage, jumpToOffset, jumpToHighlightRects };
234
+ };
163
235
  var LinkService = class {
164
236
  _pdfDocumentProxy;
165
237
  externalLinkEnabled = true;
166
238
  isInPresentationMode = false;
239
+ _currentPageNumber = 0;
240
+ _pageNavigationCallback;
167
241
  get pdfDocumentProxy() {
168
242
  if (!this._pdfDocumentProxy) {
169
243
  throw new Error("pdfDocumentProxy is not set");
@@ -173,17 +247,66 @@ var LinkService = class {
173
247
  constructor() {
174
248
  }
175
249
  get pagesCount() {
176
- return this.pdfDocumentProxy.numPages;
250
+ return this._pdfDocumentProxy?.numPages || 0;
251
+ }
252
+ get page() {
253
+ return this._currentPageNumber;
254
+ }
255
+ set page(value) {
256
+ this._currentPageNumber = value;
257
+ if (this._pageNavigationCallback) {
258
+ this._pageNavigationCallback(value);
259
+ }
260
+ }
261
+ // Required for link annotations to work
262
+ setDocument(pdfDocument) {
263
+ this._pdfDocumentProxy = pdfDocument;
264
+ }
265
+ setViewer() {
177
266
  }
178
267
  getDestinationHash(dest) {
179
- return `#dest-${dest[0].num}`;
268
+ if (!dest) return "";
269
+ const destRef = dest[0];
270
+ if (dest.length > 1 && typeof dest[1] === "object" && dest[1] !== null && "url" in dest[1]) {
271
+ const urlDest = dest[1];
272
+ return urlDest.url;
273
+ }
274
+ if (destRef && typeof destRef === "object") {
275
+ if ("num" in destRef) {
276
+ const numRef = destRef;
277
+ return `#page=${numRef.num + 1}`;
278
+ }
279
+ if ("gen" in destRef) {
280
+ const genRef = destRef;
281
+ const refNum = genRef.num ?? 0;
282
+ return `#page=${refNum + 1}`;
283
+ }
284
+ }
285
+ if (typeof destRef === "number") {
286
+ return `#page=${destRef + 1}`;
287
+ }
288
+ return `#dest-${String(dest)}`;
180
289
  }
181
- getAnchorUrl() {
182
- return "#";
290
+ getAnchorUrl(hash) {
291
+ if (hash.startsWith("http://") || hash.startsWith("https://")) {
292
+ return hash;
293
+ }
294
+ return `#${hash}`;
183
295
  }
184
296
  addLinkAttributes(link, url, newWindow) {
185
- link.href = url;
186
- link.target = newWindow === false ? "" : "_blank";
297
+ if (!link) return;
298
+ const isExternalLink = url.startsWith("http://") || url.startsWith("https://");
299
+ if (isExternalLink && this.externalLinkEnabled) {
300
+ link.href = url;
301
+ link.target = newWindow === false ? "" : "_blank";
302
+ link.rel = "noopener noreferrer";
303
+ } else if (!isExternalLink) {
304
+ link.href = url;
305
+ link.target = "";
306
+ } else {
307
+ link.href = "#";
308
+ link.target = "";
309
+ }
187
310
  }
188
311
  async goToDestination(dest) {
189
312
  let explicitDest;
@@ -197,11 +320,70 @@ var LinkService = class {
197
320
  if (!explicitDest) {
198
321
  return;
199
322
  }
200
- const explicitRef = explicitDest[0];
201
- await this.pdfDocumentProxy.getPageIndex(explicitRef);
323
+ if (explicitDest.length > 1 && typeof explicitDest[1] === "object" && explicitDest[1] !== null && "url" in explicitDest[1]) {
324
+ return;
325
+ }
326
+ const destRef = explicitDest[0];
327
+ let pageIndex;
328
+ if (destRef && typeof destRef === "object") {
329
+ if ("num" in destRef) {
330
+ try {
331
+ const refProxy = destRef;
332
+ pageIndex = await this.pdfDocumentProxy.getPageIndex(refProxy);
333
+ } catch (_error) {
334
+ return;
335
+ }
336
+ } else {
337
+ return;
338
+ }
339
+ } else if (typeof destRef === "number") {
340
+ pageIndex = destRef;
341
+ } else {
342
+ return;
343
+ }
344
+ const pageNumber = pageIndex + 1;
345
+ if (this._pageNavigationCallback) {
346
+ this._pageNavigationCallback(pageNumber);
347
+ }
348
+ }
349
+ executeNamedAction() {
350
+ }
351
+ navigateTo(dest) {
352
+ this.goToDestination(dest);
353
+ }
354
+ get rotation() {
355
+ return 0;
356
+ }
357
+ set rotation(value) {
358
+ }
359
+ goToPage(pageNumber) {
360
+ if (pageNumber >= 1 && pageNumber <= this.pagesCount) {
361
+ if (this._pageNavigationCallback) {
362
+ this._pageNavigationCallback(pageNumber);
363
+ }
364
+ }
365
+ }
366
+ setHash(hash) {
367
+ if (hash.startsWith("#page=")) {
368
+ const pageNumber = parseInt(hash.substring(6), 10);
369
+ if (!isNaN(pageNumber)) {
370
+ this.goToPage(pageNumber);
371
+ }
372
+ }
373
+ }
374
+ executeSetOCGState() {
375
+ }
376
+ // Method to register navigation callback
377
+ registerPageNavigationCallback(callback) {
378
+ this._pageNavigationCallback = callback;
379
+ }
380
+ // Method to unregister navigation callback
381
+ unregisterPageNavigationCallback() {
382
+ this._pageNavigationCallback = void 0;
202
383
  }
203
384
  };
204
- var PDFLinkServiceContext = createContext(null);
385
+ var defaultLinkService = new LinkService();
386
+ var PDFLinkServiceContext = createContext(defaultLinkService);
205
387
  var usePDFLinkService = () => {
206
388
  return useContext(PDFLinkServiceContext);
207
389
  };
@@ -230,9 +412,12 @@ var useVisibility = ({
230
412
 
231
413
  // src/hooks/layers/useAnnotationLayer.tsx
232
414
  var defaultAnnotationLayerParams = {
233
- renderForms: true
415
+ renderForms: true,
416
+ externalLinksEnabled: true,
417
+ jumpOptions: { behavior: "smooth", align: "start" }
234
418
  };
235
419
  var useAnnotationLayer = (params) => {
420
+ const mergedParams = { ...defaultAnnotationLayerParams, ...params };
236
421
  const annotationLayerRef = useRef(null);
237
422
  const annotationLayerObjectRef = useRef(null);
238
423
  const linkService = usePDFLinkService();
@@ -241,6 +426,83 @@ var useAnnotationLayer = (params) => {
241
426
  });
242
427
  const pageNumber = usePDFPageNumber();
243
428
  const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
429
+ const pdfDocumentProxy = usePdf((state) => state.pdfDocumentProxy);
430
+ useEffect(() => {
431
+ linkService.externalLinkEnabled = mergedParams.externalLinksEnabled;
432
+ }, [linkService, mergedParams.externalLinksEnabled]);
433
+ const { jumpToPage } = usePdfJump();
434
+ useEffect(() => {
435
+ if (!jumpToPage) return;
436
+ const handlePageNavigation = (pageNumber2) => {
437
+ jumpToPage(pageNumber2, mergedParams.jumpOptions);
438
+ };
439
+ linkService.registerPageNavigationCallback(handlePageNavigation);
440
+ return () => {
441
+ linkService.unregisterPageNavigationCallback();
442
+ };
443
+ }, [jumpToPage, linkService, mergedParams.jumpOptions]);
444
+ useEffect(() => {
445
+ const style = document.createElement("style");
446
+ style.textContent = `
447
+ .annotationLayer {
448
+ position: absolute;
449
+ left: 0;
450
+ top: 0;
451
+ right: 0;
452
+ bottom: 0;
453
+ overflow: hidden;
454
+ opacity: 1;
455
+ z-index: 3;
456
+ }
457
+
458
+ .annotationLayer section {
459
+ position: absolute;
460
+ }
461
+
462
+ .annotationLayer .linkAnnotation > a,
463
+ .annotationLayer .buttonWidgetAnnotation.pushButton > a {
464
+ position: absolute;
465
+ font-size: 1em;
466
+ top: 0;
467
+ left: 0;
468
+ width: 100%;
469
+ height: 100%;
470
+ background: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7") 0 0 repeat;
471
+ cursor: pointer;
472
+ }
473
+
474
+ .annotationLayer .linkAnnotation > a:hover,
475
+ .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover {
476
+ opacity: 0.2;
477
+ background: rgba(255, 255, 0, 1);
478
+ box-shadow: 0 2px 10px rgba(255, 255, 0, 1);
479
+ }
480
+ `;
481
+ document.head.appendChild(style);
482
+ return () => {
483
+ document.head.removeChild(style);
484
+ };
485
+ }, []);
486
+ useEffect(() => {
487
+ if (!annotationLayerRef.current) return;
488
+ const element = annotationLayerRef.current;
489
+ const handleLinkClick = (e) => {
490
+ if (!e.target || !(e.target instanceof HTMLAnchorElement)) return;
491
+ const target = e.target;
492
+ const href = target.getAttribute("href") || "";
493
+ if (href.startsWith("#page=")) {
494
+ e.preventDefault();
495
+ const pageNumber2 = parseInt(href.substring(6), 10);
496
+ if (!isNaN(pageNumber2)) {
497
+ linkService.goToPage(pageNumber2);
498
+ }
499
+ }
500
+ };
501
+ element.addEventListener("click", handleLinkClick);
502
+ return () => {
503
+ element.removeEventListener("click", handleLinkClick);
504
+ };
505
+ }, [linkService]);
244
506
  useEffect(() => {
245
507
  if (!annotationLayerRef.current) {
246
508
  return;
@@ -252,14 +514,20 @@ var useAnnotationLayer = (params) => {
252
514
  }
253
515
  }, [visible]);
254
516
  useEffect(() => {
255
- if (!annotationLayerRef.current) {
517
+ if (!annotationLayerRef.current || !pdfPageProxy || !pdfDocumentProxy) {
256
518
  return;
257
519
  }
520
+ if (linkService._pdfDocumentProxy !== pdfDocumentProxy) {
521
+ linkService.setDocument(pdfDocumentProxy);
522
+ }
258
523
  annotationLayerRef.current.innerHTML = "";
524
+ annotationLayerRef.current.className = "annotationLayer";
525
+ const viewport = pdfPageProxy.getViewport({ scale: 1 });
259
526
  const annotationLayerConfig = {
260
527
  div: annotationLayerRef.current,
261
- viewport: pdfPageProxy.getViewport({ scale: 1 }),
528
+ viewport,
262
529
  page: pdfPageProxy,
530
+ linkService,
263
531
  accessibilityManager: void 0,
264
532
  annotationCanvasMap: void 0,
265
533
  annotationEditorUIManager: void 0,
@@ -269,33 +537,38 @@ var useAnnotationLayer = (params) => {
269
537
  annotationLayerObjectRef.current = annotationLayer;
270
538
  const { cancel } = cancellable(
271
539
  (async () => {
272
- await annotationLayer.render({
273
- ...annotationLayerConfig,
274
- ...defaultAnnotationLayerParams,
275
- ...params,
276
- annotations: await pdfPageProxy.getAnnotations(),
277
- // @ts-expect-error TODO: Fix this
278
- linkService
279
- });
540
+ try {
541
+ const annotations = await pdfPageProxy.getAnnotations();
542
+ await annotationLayer.render({
543
+ ...annotationLayerConfig,
544
+ ...mergedParams,
545
+ annotations,
546
+ linkService
547
+ });
548
+ } catch (_error) {
549
+ }
280
550
  })()
281
551
  );
282
- cancel();
283
552
  return () => {
284
553
  cancel();
285
554
  };
286
- }, [pdfPageProxy, params, linkService]);
555
+ }, [pdfPageProxy, pdfDocumentProxy, mergedParams, linkService]);
287
556
  return {
288
557
  annotationLayerRef
289
558
  };
290
559
  };
291
560
  var AnnotationLayer2 = ({
292
561
  renderForms = true,
562
+ externalLinksEnabled = true,
563
+ jumpOptions = { behavior: "smooth", align: "start" },
293
564
  className,
294
565
  style,
295
566
  ...props
296
567
  }) => {
297
568
  const { annotationLayerRef } = useAnnotationLayer({
298
- renderForms
569
+ renderForms,
570
+ externalLinksEnabled,
571
+ jumpOptions
299
572
  });
300
573
  return /* @__PURE__ */ jsx(
301
574
  "div",
@@ -374,27 +647,442 @@ var useCanvasLayer = ({ background }) => {
374
647
  canvasRef
375
648
  };
376
649
  };
377
- var CanvasLayer = ({
378
- style,
379
- background,
380
- ...props
650
+ var CanvasLayer = ({
651
+ style,
652
+ background,
653
+ ...props
654
+ }) => {
655
+ const { canvasRef } = useCanvasLayer({ background });
656
+ return /* @__PURE__ */ jsx(
657
+ "canvas",
658
+ {
659
+ style: {
660
+ ...style,
661
+ position: "absolute",
662
+ top: 0,
663
+ left: 0,
664
+ width: "100%",
665
+ height: "100%"
666
+ },
667
+ ...props,
668
+ ref: canvasRef
669
+ }
670
+ );
671
+ };
672
+
673
+ // src/hooks/useSelectionDimensions.tsx
674
+ var MERGE_THRESHOLD = 20;
675
+ var shouldMergeRectangles = (rect1, rect2) => {
676
+ const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
677
+ const horizontallyClose = Math.abs(rect1.left + rect1.width - rect2.left) < MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) < MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
678
+ return verticalOverlap && horizontallyClose;
679
+ };
680
+ var mergeRectangles = (rect1, rect2) => {
681
+ return {
682
+ left: Math.min(rect1.left, rect2.left),
683
+ top: Math.min(rect1.top, rect2.top),
684
+ width: Math.max(rect1.left + rect1.width, rect2.left + rect2.width) - Math.min(rect1.left, rect2.left),
685
+ height: Math.max(rect1.top + rect1.height, rect2.top + rect2.height) - Math.min(rect1.top, rect2.top),
686
+ pageNumber: rect1.pageNumber
687
+ };
688
+ };
689
+ var consolidateRects = (rects) => {
690
+ if (rects.length <= 1) return rects;
691
+ const sortedRectangles = rects.toSorted((a, b) => a.top - b.top);
692
+ let withinThresholdMergedRectangles = sortedRectangles[0];
693
+ const allMergedRectangles = [];
694
+ const blowUpMergedRectangles = (rect) => {
695
+ return {
696
+ ...rect,
697
+ left: rect.left - 2,
698
+ top: rect.top - 2,
699
+ width: rect.width + 4,
700
+ height: rect.height + 2
701
+ };
702
+ };
703
+ for (const [index, currentRectangle] of sortedRectangles.entries()) {
704
+ withinThresholdMergedRectangles = mergeRectangles(
705
+ withinThresholdMergedRectangles,
706
+ currentRectangle
707
+ );
708
+ const nextRectangle = sortedRectangles[index + 1];
709
+ if (!nextRectangle) {
710
+ allMergedRectangles.push(
711
+ blowUpMergedRectangles(withinThresholdMergedRectangles)
712
+ );
713
+ break;
714
+ }
715
+ if (!shouldMergeRectangles(withinThresholdMergedRectangles, nextRectangle)) {
716
+ allMergedRectangles.push(
717
+ blowUpMergedRectangles(withinThresholdMergedRectangles)
718
+ );
719
+ withinThresholdMergedRectangles = nextRectangle;
720
+ }
721
+ }
722
+ return allMergedRectangles;
723
+ };
724
+ var useSelectionDimensions = () => {
725
+ const store = PDFStore.useContext();
726
+ const getDimension = () => {
727
+ const selection = window.getSelection();
728
+ if (!selection || selection.isCollapsed) return;
729
+ const range = selection.getRangeAt(0);
730
+ const highlights = [];
731
+ const textLayerMap = /* @__PURE__ */ new Map();
732
+ const clientRects = Array.from(range.getClientRects()).filter(
733
+ (rect) => rect.width > 2 && rect.height > 2
734
+ );
735
+ clientRects.forEach((clientRect) => {
736
+ const element = document.elementFromPoint(
737
+ clientRect.left + 1,
738
+ clientRect.top + clientRect.height / 2
739
+ );
740
+ const textLayer = element?.closest(".textLayer");
741
+ if (!textLayer) return;
742
+ const pageNumber = parseInt(
743
+ textLayer.getAttribute("data-page-number") || "1",
744
+ 10
745
+ );
746
+ const textLayerRect = textLayer.getBoundingClientRect();
747
+ const zoom = store.getState().zoom;
748
+ const rect = {
749
+ width: clientRect.width / zoom,
750
+ height: clientRect.height / zoom,
751
+ top: (clientRect.top - textLayerRect.top) / zoom,
752
+ left: (clientRect.left - textLayerRect.left) / zoom,
753
+ pageNumber
754
+ };
755
+ if (!textLayerMap.has(pageNumber)) {
756
+ textLayerMap.set(pageNumber, []);
757
+ }
758
+ textLayerMap.get(pageNumber)?.push(rect);
759
+ });
760
+ textLayerMap.forEach((rects) => {
761
+ if (rects.length > 0) {
762
+ highlights.push(...consolidateRects(rects));
763
+ }
764
+ });
765
+ return {
766
+ highlights: highlights.sort((a, b) => a.pageNumber - b.pageNumber),
767
+ text: range.toString().trim(),
768
+ isCollapsed: false
769
+ };
770
+ };
771
+ const getSelection = () => getDimension();
772
+ return { getDimension, getSelection };
773
+ };
774
+ var SelectionTooltip = ({ children }) => {
775
+ const [isOpen, setIsOpen] = useState(false);
776
+ const lastSelectionRef = useRef(null);
777
+ const viewportRef = usePdf((state) => state.viewportRef);
778
+ const { refs, floatingStyles, context } = useFloating({
779
+ placement: "bottom",
780
+ open: isOpen,
781
+ onOpenChange: setIsOpen,
782
+ whileElementsMounted: autoUpdate,
783
+ middleware: [offset(10), shift({ padding: 8 })]
784
+ });
785
+ const dismiss = useDismiss(context);
786
+ const { getFloatingProps } = useInteractions([dismiss]);
787
+ const updateTooltipPosition = useCallback(() => {
788
+ const selection = document.getSelection();
789
+ if (!selection || selection.isCollapsed) {
790
+ setIsOpen(false);
791
+ lastSelectionRef.current = null;
792
+ return;
793
+ }
794
+ const range = selection.getRangeAt(0);
795
+ if (!range) return;
796
+ const rects = range.getClientRects();
797
+ const lastRect = rects[rects.length - 1];
798
+ lastSelectionRef.current = range;
799
+ if (lastRect) {
800
+ refs.setReference({
801
+ getBoundingClientRect: () => ({
802
+ width: lastRect.width,
803
+ height: lastRect.height,
804
+ x: lastRect.left,
805
+ y: lastRect.bottom,
806
+ // Position below the last line of selection
807
+ top: lastRect.bottom,
808
+ right: lastRect.right,
809
+ bottom: lastRect.bottom + lastRect.height,
810
+ left: lastRect.left
811
+ }),
812
+ getClientRects: () => [lastRect]
813
+ });
814
+ setIsOpen(true);
815
+ } else {
816
+ setIsOpen(false);
817
+ }
818
+ }, [refs]);
819
+ useEffect(() => {
820
+ const handleSelectionChange = () => {
821
+ const selection = document.getSelection();
822
+ if (selection && viewportRef.current?.contains(selection.anchorNode)) {
823
+ requestAnimationFrame(updateTooltipPosition);
824
+ } else {
825
+ setIsOpen(false);
826
+ }
827
+ };
828
+ const handleScroll = () => {
829
+ if (!isOpen || !lastSelectionRef.current) return;
830
+ requestAnimationFrame(updateTooltipPosition);
831
+ };
832
+ document.addEventListener("selectionchange", handleSelectionChange);
833
+ if (viewportRef.current) {
834
+ viewportRef.current.addEventListener("scroll", handleScroll, {
835
+ passive: true
836
+ });
837
+ }
838
+ return () => {
839
+ document.removeEventListener("selectionchange", handleSelectionChange);
840
+ if (viewportRef.current) {
841
+ viewportRef.current.removeEventListener("scroll", handleScroll);
842
+ }
843
+ };
844
+ }, [refs, isOpen, viewportRef, updateTooltipPosition]);
845
+ useEffect(() => {
846
+ const handleFloatingClick = (e) => {
847
+ if (refs.floating.current?.contains(e.target)) {
848
+ e.stopPropagation();
849
+ }
850
+ };
851
+ document.addEventListener("click", handleFloatingClick);
852
+ return () => document.removeEventListener("click", handleFloatingClick);
853
+ }, [refs.floating]);
854
+ return /* @__PURE__ */ jsx(Fragment, { children: isOpen && /* @__PURE__ */ jsx(
855
+ "div",
856
+ {
857
+ ref: refs.setFloating,
858
+ style: {
859
+ ...floatingStyles
860
+ },
861
+ ...getFloatingProps(),
862
+ children
863
+ }
864
+ ) });
865
+ };
866
+ var defaultColors = [
867
+ {
868
+ color: "#e3b127",
869
+ localization: {
870
+ id: "yellow",
871
+ defaultMessage: "Yellow"
872
+ }
873
+ },
874
+ {
875
+ color: "#419931",
876
+ localization: {
877
+ id: "green",
878
+ defaultMessage: "Green"
879
+ }
880
+ },
881
+ {
882
+ color: "#4286c9",
883
+ localization: {
884
+ id: "blue",
885
+ defaultMessage: "Blue"
886
+ }
887
+ },
888
+ {
889
+ color: "#f246b6",
890
+ localization: {
891
+ id: "pink",
892
+ defaultMessage: "Pink"
893
+ }
894
+ },
895
+ {
896
+ color: "#a53dd1",
897
+ localization: {
898
+ id: "purple",
899
+ defaultMessage: "Purple"
900
+ }
901
+ },
902
+ {
903
+ color: "#f09037",
904
+ localization: {
905
+ id: "orange",
906
+ defaultMessage: "Orange"
907
+ }
908
+ },
909
+ {
910
+ color: "#37f0d4",
911
+ localization: {
912
+ id: "teal",
913
+ defaultMessage: "Teal"
914
+ }
915
+ },
916
+ {
917
+ color: "#3d0ff5",
918
+ localization: {
919
+ id: "purple",
920
+ defaultMessage: "Purple"
921
+ }
922
+ },
923
+ {
924
+ color: "#f50f26",
925
+ localization: {
926
+ id: "red",
927
+ defaultMessage: "Red"
928
+ }
929
+ }
930
+ ];
931
+ var ColorSelectionTool = ({
932
+ highlighterColors = defaultColors,
933
+ onColorSelection
934
+ }) => {
935
+ return /* @__PURE__ */ jsx(SelectionTooltip, { children: /* @__PURE__ */ jsx(
936
+ "div",
937
+ {
938
+ style: {
939
+ display: "flex",
940
+ gap: "0.5rem",
941
+ padding: "0.5rem",
942
+ backgroundColor: "#363636",
943
+ borderRadius: "0.5rem"
944
+ },
945
+ children: highlighterColors.map((colorItem, index) => /* @__PURE__ */ jsx(
946
+ "button",
947
+ {
948
+ onClick: () => onColorSelection(colorItem),
949
+ title: colorItem.localization.defaultMessage,
950
+ "aria-label": colorItem.localization.defaultMessage,
951
+ style: {
952
+ width: "1.25rem",
953
+ height: "1.25rem",
954
+ borderRadius: "0.25rem",
955
+ cursor: "pointer",
956
+ backgroundColor: colorItem.color
957
+ }
958
+ },
959
+ index
960
+ ))
961
+ }
962
+ ) });
963
+ };
964
+
965
+ // src/utils/selectionUtils.ts
966
+ var getEndOfHighlight = (selection) => {
967
+ const lastRectangle = selection.rectangles[selection.rectangles.length - 1];
968
+ return lastRectangle.left + lastRectangle.width + 10;
969
+ };
970
+ var getMidHeightOfHighlightLine = (selection) => {
971
+ const lastRectangle = selection.rectangles[selection.rectangles.length - 1];
972
+ return lastRectangle.top + lastRectangle.height / 2;
973
+ };
974
+ var ColoredHighlightComponent = ({
975
+ selection
976
+ }) => {
977
+ const deleteColoredHighlight = usePdf(
978
+ (state) => state.deleteColoredHighlight
979
+ );
980
+ const [showButton, setShowButton] = useState(false);
981
+ return /* @__PURE__ */ jsxs("div", { className: "colored-highlight", children: [
982
+ selection.rectangles.map((rect, index) => /* @__PURE__ */ jsx(
983
+ "span",
984
+ {
985
+ onClick: () => setShowButton(!showButton),
986
+ style: {
987
+ position: "absolute",
988
+ top: rect.top,
989
+ left: rect.left,
990
+ height: rect.height,
991
+ width: rect.width,
992
+ cursor: "pointer",
993
+ zIndex: 30,
994
+ backgroundColor: selection.color,
995
+ // mixBlendMode: "lighten", // changes the color of the text
996
+ mixBlendMode: "darken",
997
+ // best results
998
+ // mixBlendMode: "multiply", // works but coloring has some inconsistencies
999
+ borderRadius: "0.2rem"
1000
+ }
1001
+ },
1002
+ `${selection.uuid}-${index}`
1003
+ )),
1004
+ showButton && /* @__PURE__ */ jsx(
1005
+ "button",
1006
+ {
1007
+ style: {
1008
+ backgroundColor: "white",
1009
+ color: "white",
1010
+ borderRadius: "5px",
1011
+ padding: "5px",
1012
+ cursor: "pointer",
1013
+ boxShadow: "2px 2px 5px black",
1014
+ position: "absolute",
1015
+ top: getMidHeightOfHighlightLine(selection),
1016
+ left: getEndOfHighlight(selection),
1017
+ zIndex: 30,
1018
+ transform: "translateY(-50%)"
1019
+ },
1020
+ onClick: () => deleteColoredHighlight(selection.uuid),
1021
+ children: /* @__PURE__ */ jsx(
1022
+ "svg",
1023
+ {
1024
+ fill: "#000000",
1025
+ version: "1.1",
1026
+ id: "Capa_1",
1027
+ xmlns: "http://www.w3.org/2000/svg",
1028
+ width: "15px",
1029
+ height: "15px",
1030
+ viewBox: "0 0 485 485",
1031
+ children: /* @__PURE__ */ jsx("g", { children: /* @__PURE__ */ jsxs("g", { children: [
1032
+ /* @__PURE__ */ jsx("rect", { x: "67.224", width: "350.535", height: "71.81" }),
1033
+ /* @__PURE__ */ jsx(
1034
+ "path",
1035
+ {
1036
+ d: "M417.776,92.829H67.237V485h350.537V92.829H417.776z M165.402,431.447h-28.362V146.383h28.362V431.447z M256.689,431.447\n h-28.363V146.383h28.363V431.447z M347.97,431.447h-28.361V146.383h28.361V431.447z"
1037
+ }
1038
+ )
1039
+ ] }) })
1040
+ }
1041
+ )
1042
+ },
1043
+ `${selection.uuid}-delete-button`
1044
+ )
1045
+ ] });
1046
+ };
1047
+ var ColoredHighlightLayer = ({
1048
+ onHighlight
381
1049
  }) => {
382
- const { canvasRef } = useCanvasLayer({ background });
383
- return /* @__PURE__ */ jsx(
384
- "canvas",
385
- {
386
- style: {
387
- ...style,
388
- position: "absolute",
389
- top: 0,
390
- left: 0,
391
- width: "100%",
392
- height: "100%"
393
- },
394
- ...props,
395
- ref: canvasRef
396
- }
1050
+ const pageNumber = usePDFPageNumber();
1051
+ const { getSelection } = useSelectionDimensions();
1052
+ const highlights = usePdf(
1053
+ (state) => state.coloredHighlights
397
1054
  );
1055
+ const addColoredHighlight = usePdf((state) => state.addColoredHighlight);
1056
+ const handleHighlighting = useCallback((color) => {
1057
+ const { highlights: highlights2, text } = getSelection();
1058
+ if (highlights2[0]) {
1059
+ const highlight = {
1060
+ uuid: v4(),
1061
+ pageNumber: highlights2[0].pageNumber,
1062
+ // usePDFPageNumber() doesn't return the correct page number, so i'm getting the number directly from the first highlight
1063
+ color,
1064
+ rectangles: highlights2,
1065
+ text
1066
+ };
1067
+ addColoredHighlight(highlight);
1068
+ if (onHighlight) onHighlight(highlight);
1069
+ }
1070
+ }, []);
1071
+ return /* @__PURE__ */ jsxs("div", { className: "colored-highlights-layer", children: [
1072
+ highlights.filter((selection) => selection.pageNumber === pageNumber).map((selection) => /* @__PURE__ */ jsx(
1073
+ ColoredHighlightComponent,
1074
+ {
1075
+ selection
1076
+ },
1077
+ selection.uuid
1078
+ )),
1079
+ /* @__PURE__ */ jsx(
1080
+ ColorSelectionTool,
1081
+ {
1082
+ onColorSelection: (colorItem) => handleHighlighting(colorItem.color)
1083
+ }
1084
+ )
1085
+ ] });
398
1086
  };
399
1087
 
400
1088
  // src/components/layers/custom-layer.tsx
@@ -491,68 +1179,6 @@ var TextLayer2 = ({
491
1179
  }
492
1180
  );
493
1181
  };
494
-
495
- // src/hooks/pages/usePdfJump.tsx
496
- var usePdfJump = () => {
497
- const virtualizer = usePdf((state) => state.virtualizer);
498
- const setHighlight = usePdf((state) => state.setHighlight);
499
- const jumpToPage = (pageIndex, options) => {
500
- if (!virtualizer) return;
501
- const defaultOptions = {
502
- align: "start",
503
- behavior: "smooth"
504
- };
505
- const finalOptions = { ...defaultOptions, ...options };
506
- virtualizer.scrollToIndex(pageIndex - 1, finalOptions);
507
- };
508
- const jumpToOffset = (offset2) => {
509
- if (!virtualizer) return;
510
- virtualizer.scrollToOffset(offset2, {
511
- align: "start",
512
- behavior: "smooth"
513
- });
514
- };
515
- const jumpToHighlightRects = (rects, type, align = "start", additionalOffset = 0) => {
516
- if (!virtualizer) return;
517
- setHighlight(rects);
518
- const firstPage = Math.min(...rects.map((rect) => rect.pageNumber));
519
- const pageOffset = virtualizer.getOffsetForIndex(firstPage - 1, "start");
520
- if (pageOffset === null) return;
521
- const targetRect = rects.find((rect) => rect.pageNumber === firstPage);
522
- if (!targetRect) return;
523
- const isNumber = pageOffset?.[0] != null;
524
- if (!isNumber) return;
525
- const pageStart = pageOffset[0] ?? 0;
526
- let rectTop;
527
- let rectHeight;
528
- if (type === "percent") {
529
- const estimatePageHeight = virtualizer.options.estimateSize(
530
- firstPage - 1
531
- );
532
- rectTop = targetRect.top / 100 * estimatePageHeight;
533
- rectHeight = targetRect.height / 100 * estimatePageHeight;
534
- } else {
535
- rectTop = targetRect.top;
536
- rectHeight = targetRect.height;
537
- }
538
- let scrollOffset;
539
- if (align === "center") {
540
- const viewportHeight = virtualizer.scrollElement?.clientHeight || 0;
541
- const rectCenter = pageStart + rectTop + rectHeight / 2;
542
- scrollOffset = rectCenter - viewportHeight / 2;
543
- } else {
544
- scrollOffset = pageStart + rectTop;
545
- }
546
- scrollOffset += additionalOffset;
547
- const adjustedOffset = Math.max(0, scrollOffset);
548
- virtualizer.scrollToOffset(adjustedOffset, {
549
- align: "start",
550
- // Always use start when we've calculated our own centering
551
- behavior: "smooth"
552
- });
553
- };
554
- return { jumpToPage, jumpToOffset, jumpToHighlightRects };
555
- };
556
1182
  var usePDFOutline = () => {
557
1183
  const pdfDocumentProxy = usePdf((state) => state.pdfDocumentProxy);
558
1184
  const [outline, setOutline] = useState();
@@ -2955,12 +3581,10 @@ var Pages = ({
2955
3581
  toOffset -= size;
2956
3582
  }
2957
3583
  const scrollSizeProp = virtualizer.options.horizontal ? "scrollWidth" : "scrollHeight";
2958
- const scrollSize = virtualizer.scrollElement ? "document" in virtualizer.scrollElement ? (
3584
+ virtualizer.scrollElement ? "document" in virtualizer.scrollElement ? (
2959
3585
  //@ts-expect-error this is a private stuff
2960
3586
  virtualizer.scrollElement.document.documentElement[scrollSizeProp]
2961
3587
  ) : virtualizer.scrollElement[scrollSizeProp] : 0;
2962
- const maxOffset = scrollSize - size;
2963
- console.log(scrollSize, maxOffset, toOffset);
2964
3588
  return Math.max(toOffset, 0);
2965
3589
  };
2966
3590
  }, [zoom, virtualizer]);
@@ -3153,98 +3777,6 @@ var Search = ({ children, loading = "Loading..." }) => {
3153
3777
  }, [proxies, getTextContent]);
3154
3778
  return isLoading ? loading : children;
3155
3779
  };
3156
- var SelectionTooltip = ({ children }) => {
3157
- const [isOpen, setIsOpen] = useState(false);
3158
- const lastSelectionRef = useRef(null);
3159
- const viewportRef = usePdf((state) => state.viewportRef);
3160
- const { refs, floatingStyles, context } = useFloating({
3161
- placement: "bottom",
3162
- open: isOpen,
3163
- onOpenChange: setIsOpen,
3164
- whileElementsMounted: autoUpdate,
3165
- middleware: [offset(10), shift({ padding: 8 })]
3166
- });
3167
- const dismiss = useDismiss(context);
3168
- const { getFloatingProps } = useInteractions([dismiss]);
3169
- const updateTooltipPosition = useCallback(() => {
3170
- const selection = document.getSelection();
3171
- if (!selection || selection.isCollapsed) {
3172
- setIsOpen(false);
3173
- lastSelectionRef.current = null;
3174
- return;
3175
- }
3176
- const range = selection.getRangeAt(0);
3177
- if (!range) return;
3178
- const rects = range.getClientRects();
3179
- const lastRect = rects[rects.length - 1];
3180
- lastSelectionRef.current = range;
3181
- if (lastRect) {
3182
- refs.setReference({
3183
- getBoundingClientRect: () => ({
3184
- width: lastRect.width,
3185
- height: lastRect.height,
3186
- x: lastRect.left,
3187
- y: lastRect.bottom,
3188
- // Position below the last line of selection
3189
- top: lastRect.bottom,
3190
- right: lastRect.right,
3191
- bottom: lastRect.bottom + lastRect.height,
3192
- left: lastRect.left
3193
- }),
3194
- getClientRects: () => [lastRect]
3195
- });
3196
- setIsOpen(true);
3197
- } else {
3198
- setIsOpen(false);
3199
- }
3200
- }, [refs]);
3201
- useEffect(() => {
3202
- const handleSelectionChange = () => {
3203
- const selection = document.getSelection();
3204
- if (selection && viewportRef.current?.contains(selection.anchorNode)) {
3205
- requestAnimationFrame(updateTooltipPosition);
3206
- } else {
3207
- setIsOpen(false);
3208
- }
3209
- };
3210
- const handleScroll = () => {
3211
- if (!isOpen || !lastSelectionRef.current) return;
3212
- requestAnimationFrame(updateTooltipPosition);
3213
- };
3214
- document.addEventListener("selectionchange", handleSelectionChange);
3215
- if (viewportRef.current) {
3216
- viewportRef.current.addEventListener("scroll", handleScroll, {
3217
- passive: true
3218
- });
3219
- }
3220
- return () => {
3221
- document.removeEventListener("selectionchange", handleSelectionChange);
3222
- if (viewportRef.current) {
3223
- viewportRef.current.removeEventListener("scroll", handleScroll);
3224
- }
3225
- };
3226
- }, [refs, isOpen, viewportRef, updateTooltipPosition]);
3227
- useEffect(() => {
3228
- const handleFloatingClick = (e) => {
3229
- if (refs.floating.current?.contains(e.target)) {
3230
- e.stopPropagation();
3231
- }
3232
- };
3233
- document.addEventListener("click", handleFloatingClick);
3234
- return () => document.removeEventListener("click", handleFloatingClick);
3235
- }, [refs.floating]);
3236
- return /* @__PURE__ */ jsx(Fragment, { children: isOpen && /* @__PURE__ */ jsx(
3237
- "div",
3238
- {
3239
- ref: refs.setFloating,
3240
- style: {
3241
- ...floatingStyles
3242
- },
3243
- ...getFloatingProps(),
3244
- children
3245
- }
3246
- ) });
3247
- };
3248
3780
  var DEFAULT_CONFIG = {
3249
3781
  maxHeight: 800,
3250
3782
  maxWidth: 400
@@ -3444,7 +3976,8 @@ var useSearch = () => {
3444
3976
  text: text.substr(matchIndex, searchText.length + textSize),
3445
3977
  score: 1,
3446
3978
  matchIndex,
3447
- isExactMatch: true
3979
+ isExactMatch: true,
3980
+ searchText
3448
3981
  });
3449
3982
  index = matchIndex + searchText.length;
3450
3983
  }
@@ -3473,7 +4006,8 @@ var useSearch = () => {
3473
4006
  text: text.substr(index, searchLower.length + textSize),
3474
4007
  score,
3475
4008
  matchIndex: index,
3476
- isExactMatch: false
4009
+ isExactMatch: false,
4010
+ searchText
3477
4011
  });
3478
4012
  index += searchLower.length;
3479
4013
  } else {
@@ -3548,7 +4082,7 @@ var useSearch = () => {
3548
4082
  async function calculateHighlightRects(pageProxy, textMatch) {
3549
4083
  const textContent = await pageProxy.getTextContent();
3550
4084
  const items = textContent.items;
3551
- const matchLength = textMatch.text.length;
4085
+ const matchLength = textMatch.searchText ? textMatch.searchText.length : textMatch.text.length;
3552
4086
  const matchRects = [];
3553
4087
  let currentIndex = 0;
3554
4088
  let remainingMatchLength = matchLength;
@@ -3619,95 +4153,4 @@ function mergeAdjacentRects(rects) {
3619
4153
  return merged;
3620
4154
  }
3621
4155
 
3622
- // src/hooks/useSelectionDimensions.tsx
3623
- var MERGE_THRESHOLD = 5;
3624
- var shouldMergeRects = (rect1, rect2) => {
3625
- const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
3626
- const horizontallyClose = Math.abs(rect1.left + rect1.width - rect2.left) < MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) < MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
3627
- return verticalOverlap && horizontallyClose;
3628
- };
3629
- var mergeRects = (rect1, rect2) => {
3630
- return {
3631
- left: Math.min(rect1.left, rect2.left),
3632
- top: Math.min(rect1.top, rect2.top),
3633
- width: Math.max(rect1.left + rect1.width, rect2.left + rect2.width) - Math.min(rect1.left, rect2.left),
3634
- height: Math.max(rect1.top + rect1.height, rect2.top + rect2.height) - Math.min(rect1.top, rect2.top),
3635
- pageNumber: rect1.pageNumber
3636
- };
3637
- };
3638
- var consolidateRects = (rects) => {
3639
- if (rects.length <= 1) return rects;
3640
- const sortedRects = [...rects].sort((a, b) => a.top - b.top);
3641
- let hasChanges;
3642
- do {
3643
- hasChanges = false;
3644
- let currentRect = sortedRects[0];
3645
- const tempResult = [];
3646
- for (let i = 1; i < sortedRects.length; i++) {
3647
- const sorted = sortedRects[i];
3648
- if (!currentRect || !sorted) continue;
3649
- if (shouldMergeRects(currentRect, sorted)) {
3650
- currentRect = mergeRects(currentRect, sorted);
3651
- hasChanges = true;
3652
- } else {
3653
- tempResult.push(currentRect);
3654
- currentRect = sorted;
3655
- }
3656
- }
3657
- if (currentRect) tempResult.push(currentRect);
3658
- sortedRects.length = 0;
3659
- sortedRects.push(...tempResult);
3660
- } while (hasChanges);
3661
- return sortedRects;
3662
- };
3663
- var useSelectionDimensions = () => {
3664
- const store = PDFStore.useContext();
3665
- const getDimension = () => {
3666
- const selection = window.getSelection();
3667
- if (!selection || selection.isCollapsed) return;
3668
- const range = selection.getRangeAt(0);
3669
- const highlights = [];
3670
- const textLayerMap = /* @__PURE__ */ new Map();
3671
- const clientRects = Array.from(range.getClientRects()).filter(
3672
- (rect) => rect.width > 2 && rect.height > 2
3673
- );
3674
- clientRects.forEach((clientRect) => {
3675
- const element = document.elementFromPoint(
3676
- clientRect.left + 1,
3677
- clientRect.top + clientRect.height / 2
3678
- );
3679
- const textLayer = element?.closest(".textLayer");
3680
- if (!textLayer) return;
3681
- const pageNumber = parseInt(
3682
- textLayer.getAttribute("data-page-number") || "1",
3683
- 10
3684
- );
3685
- const textLayerRect = textLayer.getBoundingClientRect();
3686
- const zoom = store.getState().zoom;
3687
- const rect = {
3688
- width: clientRect.width / zoom,
3689
- height: clientRect.height / zoom,
3690
- top: (clientRect.top - textLayerRect.top) / zoom,
3691
- left: (clientRect.left - textLayerRect.left) / zoom,
3692
- pageNumber
3693
- };
3694
- if (!textLayerMap.has(pageNumber)) {
3695
- textLayerMap.set(pageNumber, []);
3696
- }
3697
- textLayerMap.get(pageNumber)?.push(rect);
3698
- });
3699
- textLayerMap.forEach((rects) => {
3700
- if (rects.length > 0) {
3701
- highlights.push(...consolidateRects(rects));
3702
- }
3703
- });
3704
- return {
3705
- highlights: highlights.sort((a, b) => a.pageNumber - b.pageNumber),
3706
- text: range.toString().trim(),
3707
- isCollapsed: false
3708
- };
3709
- };
3710
- return { getDimension };
3711
- };
3712
-
3713
- export { AnnotationLayer2 as AnnotationLayer, CanvasLayer, CurrentPage, CurrentZoom, CustomLayer, HighlightLayer, LinkService, NextPage, Outline, OutlineChildItems, OutlineItem, Page, Pages, PreviousPage, Root, Search, SelectionTooltip, TextLayer2 as TextLayer, Thumbnail, Thumbnails, TotalPages, ZoomIn, ZoomOut, calculateHighlightRects, usePdf, usePdfJump, useSearch, useSelectionDimensions };
4156
+ export { AnnotationLayer2 as AnnotationLayer, CanvasLayer, ColoredHighlightLayer, CurrentPage, CurrentZoom, CustomLayer, HighlightLayer, LinkService, NextPage, Outline, OutlineChildItems, OutlineItem, Page, Pages, PreviousPage, Root, Search, SelectionTooltip, TextLayer2 as TextLayer, Thumbnail, Thumbnails, TotalPages, ZoomIn, ZoomOut, calculateHighlightRects, usePdf, usePdfJump, useSearch, useSelectionDimensions };