@design.estate/dees-catalog 3.41.2 → 3.41.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "3.41.2",
3
+ "version": "3.41.3",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '3.41.2',
6
+ version: '3.41.3',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -52,7 +52,7 @@ export class DeesPdfViewer extends DeesElement {
52
52
  accessor thumbnailData: Array<{page: number, rendered: boolean}> = [];
53
53
 
54
54
  @property({ type: Array })
55
- accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean}> = [];
55
+ accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean, textLayerRendered: boolean}> = [];
56
56
 
57
57
  private pdfDocument: any;
58
58
  private renderState: RenderState = 'idle';
@@ -63,6 +63,7 @@ export class DeesPdfViewer extends DeesElement {
63
63
  private currentRenderPromise: Promise<void> | null = null;
64
64
  private thumbnailRenderTasks: any[] = [];
65
65
  private pageRenderTasks: Map<number, any> = new Map();
66
+ private textLayerRenderTasks: Map<number, any> = new Map();
66
67
  private canvas: HTMLCanvasElement | undefined;
67
68
  private ctx: CanvasRenderingContext2D | undefined;
68
69
  private viewerMain: HTMLElement | null = null;
@@ -230,6 +231,7 @@ export class DeesPdfViewer extends DeesElement {
230
231
  <div class="page-wrapper" data-page="${item.page}">
231
232
  <div class="canvas-container">
232
233
  <canvas class="page-canvas" data-page="${item.page}"></canvas>
234
+ <div class="text-layer" data-page="${item.page}"></div>
233
235
  </div>
234
236
  </div>
235
237
  `
@@ -330,7 +332,8 @@ export class DeesPdfViewer extends DeesElement {
330
332
  this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
331
333
  page: i + 1,
332
334
  rendered: false,
333
- rendering: false
335
+ rendering: false,
336
+ textLayerRendered: false,
334
337
  }));
335
338
 
336
339
  // Set loading to false to render the pages
@@ -476,6 +479,9 @@ export class DeesPdfViewer extends DeesElement {
476
479
  pageInfo.rendering = false;
477
480
  this.pageRenderTasks.delete(pageNum);
478
481
 
482
+ // Render text layer for selection
483
+ await this.renderTextLayer(pageNum);
484
+
479
485
  // Update page data to reflect rendered state
480
486
  this.requestUpdate('pageData');
481
487
  } catch (error: any) {
@@ -487,6 +493,132 @@ export class DeesPdfViewer extends DeesElement {
487
493
  }
488
494
  }
489
495
 
496
+ private async renderTextLayer(pageNum: number): Promise<void> {
497
+ const pageInfo = this.pageData.find(p => p.page === pageNum);
498
+ if (!pageInfo || pageInfo.textLayerRendered) return;
499
+
500
+ try {
501
+ const textLayerDiv = this.shadowRoot?.querySelector(
502
+ `.text-layer[data-page="${pageNum}"]`
503
+ ) as HTMLElement;
504
+ if (!textLayerDiv) return;
505
+
506
+ textLayerDiv.innerHTML = '';
507
+
508
+ const page = await this.pdfDocument.getPage(pageNum);
509
+ const textContent = await page.getTextContent();
510
+ const viewport = this.computeViewport(page);
511
+
512
+ // @ts-ignore - Dynamic import of pdfjs
513
+ const pdfjs = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm');
514
+
515
+ textLayerDiv.style.width = `${viewport.width}px`;
516
+ textLayerDiv.style.height = `${viewport.height}px`;
517
+
518
+ // Set the scale factor CSS variable - required by PDF.js text layer
519
+ textLayerDiv.style.setProperty('--scale-factor', String(viewport.scale));
520
+
521
+ const textLayerRenderTask = pdfjs.renderTextLayer({
522
+ textContentSource: textContent,
523
+ container: textLayerDiv,
524
+ viewport: viewport,
525
+ });
526
+
527
+ this.textLayerRenderTasks.set(pageNum, textLayerRenderTask);
528
+ await textLayerRenderTask.promise;
529
+
530
+ // Add endOfContent for selection boundary
531
+ const endOfContent = document.createElement('div');
532
+ endOfContent.className = 'endOfContent';
533
+ textLayerDiv.appendChild(endOfContent);
534
+
535
+ // Custom drag selection for Shadow DOM compatibility
536
+ // caretRangeFromPoint doesn't pierce shadow DOM, so we find spans manually
537
+ let isDragging = false;
538
+ let anchorNode: Node | null = null;
539
+ let anchorOffset = 0;
540
+
541
+ const getTextPositionFromPoint = (x: number, y: number): { node: Node; offset: number } | null => {
542
+ // Find span at coordinates by checking bounding rects
543
+ const spans = Array.from(textLayerDiv.querySelectorAll('span'));
544
+ for (const span of spans) {
545
+ const rect = span.getBoundingClientRect();
546
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
547
+ const textNode = span.firstChild;
548
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
549
+ // Calculate character offset based on x position
550
+ const text = textNode.textContent || '';
551
+ const charWidth = rect.width / text.length;
552
+ const relativeX = x - rect.left;
553
+ const offset = Math.min(Math.round(relativeX / charWidth), text.length);
554
+ return { node: textNode, offset };
555
+ }
556
+ }
557
+ }
558
+ return null;
559
+ };
560
+
561
+ const handleMouseUp = () => {
562
+ if (isDragging) {
563
+ isDragging = false;
564
+ anchorNode = null;
565
+ textLayerDiv.classList.remove('selecting');
566
+ }
567
+ document.removeEventListener('mouseup', handleMouseUp);
568
+ document.removeEventListener('mousemove', handleMouseMove);
569
+ };
570
+
571
+ const handleMouseMove = (e: MouseEvent) => {
572
+ if (!isDragging || !anchorNode) return;
573
+
574
+ e.preventDefault();
575
+ const pos = getTextPositionFromPoint(e.clientX, e.clientY);
576
+ if (pos) {
577
+ const selection = window.getSelection();
578
+ if (selection) {
579
+ try {
580
+ selection.setBaseAndExtent(anchorNode, anchorOffset, pos.node, pos.offset);
581
+ } catch (err) {
582
+ // Ignore errors from invalid selections
583
+ }
584
+ }
585
+ }
586
+ };
587
+
588
+ textLayerDiv.addEventListener('mousedown', (e: MouseEvent) => {
589
+ if (e.button !== 0) return;
590
+
591
+ const pos = getTextPositionFromPoint(e.clientX, e.clientY);
592
+ if (pos) {
593
+ // Prevent native selection behavior
594
+ e.preventDefault();
595
+
596
+ isDragging = true;
597
+ anchorNode = pos.node;
598
+ anchorOffset = pos.offset;
599
+ textLayerDiv.classList.add('selecting');
600
+
601
+ // Clear existing selection
602
+ const selection = window.getSelection();
603
+ selection?.removeAllRanges();
604
+
605
+ // Add document-level listeners for drag
606
+ document.addEventListener('mousemove', handleMouseMove);
607
+ document.addEventListener('mouseup', handleMouseUp);
608
+ }
609
+ });
610
+
611
+ pageInfo.textLayerRendered = true;
612
+ page.cleanup?.();
613
+ this.textLayerRenderTasks.delete(pageNum);
614
+ } catch (error: any) {
615
+ if (error?.name !== 'RenderingCancelledException') {
616
+ console.error(`Error rendering text layer for page ${pageNum}:`, error);
617
+ }
618
+ this.textLayerRenderTasks.delete(pageNum);
619
+ }
620
+ }
621
+
490
622
  private handleScroll = () => {
491
623
  // Throttle scroll events
492
624
  if (this.scrollThrottleTimeout) {
@@ -771,6 +903,7 @@ export class DeesPdfViewer extends DeesElement {
771
903
  this.pageData.forEach(page => {
772
904
  page.rendered = false;
773
905
  page.rendering = false;
906
+ page.textLayerRendered = false;
774
907
  });
775
908
 
776
909
  // Cancel any ongoing render tasks
@@ -783,6 +916,16 @@ export class DeesPdfViewer extends DeesElement {
783
916
  });
784
917
  this.pageRenderTasks.clear();
785
918
 
919
+ // Cancel text layer render tasks
920
+ this.textLayerRenderTasks.forEach(task => {
921
+ try {
922
+ task.cancel?.();
923
+ } catch (error) {
924
+ // Ignore cancellation errors
925
+ }
926
+ });
927
+ this.textLayerRenderTasks.clear();
928
+
786
929
  // Request update to re-render pages
787
930
  this.requestUpdate();
788
931
 
@@ -792,52 +935,138 @@ export class DeesPdfViewer extends DeesElement {
792
935
  });
793
936
  }
794
937
 
795
- private downloadPdf() {
796
- const link = document.createElement('a');
797
- link.href = this.pdfUrl;
798
- link.download = this.pdfUrl.split('/').pop() || 'document.pdf';
799
- link.click();
938
+ private async downloadPdf() {
939
+ if (!this.pdfDocument) return;
940
+
941
+ try {
942
+ // Get raw PDF data from the loaded document
943
+ const data = await this.pdfDocument.getData();
944
+ const blob = new Blob([data.buffer], { type: 'application/pdf' });
945
+ const blobUrl = URL.createObjectURL(blob);
946
+
947
+ const link = document.createElement('a');
948
+ link.href = blobUrl;
949
+ link.download = this.pdfUrl ? this.pdfUrl.split('/').pop() || 'document.pdf' : 'document.pdf';
950
+ link.click();
951
+
952
+ // Clean up blob URL after short delay
953
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
954
+ } catch (error) {
955
+ console.error('Error downloading PDF:', error);
956
+ }
800
957
  }
801
958
 
802
- private printPdf() {
803
- window.open(this.pdfUrl, '_blank')?.print();
959
+ private async printPdf() {
960
+ if (!this.pdfDocument) return;
961
+
962
+ try {
963
+ // Get raw PDF data from the loaded document
964
+ const data = await this.pdfDocument.getData();
965
+ const blob = new Blob([data.buffer], { type: 'application/pdf' });
966
+ const pdfUrl = URL.createObjectURL(blob);
967
+
968
+ // Create an HTML wrapper page that embeds the PDF and handles print/close
969
+ // This gives us control over the afterprint event (direct PDF URLs don't support it)
970
+ const htmlContent = `
971
+ <!DOCTYPE html>
972
+ <html>
973
+ <head>
974
+ <title>Print PDF</title>
975
+ <style>
976
+ * { margin: 0; padding: 0; }
977
+ html, body { width: 100%; height: 100%; overflow: hidden; }
978
+ iframe { width: 100%; height: 100%; border: none; }
979
+ @media print {
980
+ html, body, iframe { width: 100%; height: 100%; }
981
+ }
982
+ </style>
983
+ </head>
984
+ <body>
985
+ <iframe src="${pdfUrl}" type="application/pdf"></iframe>
986
+ <script>
987
+ window.onload = function() {
988
+ setTimeout(function() {
989
+ window.focus();
990
+ window.print();
991
+ }, 500);
992
+ };
993
+ window.onafterprint = function() {
994
+ window.close();
995
+ };
996
+ // Safety close after 2 minutes
997
+ setTimeout(function() { window.close(); }, 120000);
998
+ </script>
999
+ </body>
1000
+ </html>
1001
+ `;
1002
+ const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
1003
+ const htmlUrl = URL.createObjectURL(htmlBlob);
1004
+
1005
+ const printWindow = window.open(htmlUrl, '_blank', 'width=800,height=600');
1006
+ if (printWindow) {
1007
+ // Cleanup blob URLs when window closes
1008
+ const checkClosed = setInterval(() => {
1009
+ if (printWindow.closed) {
1010
+ clearInterval(checkClosed);
1011
+ URL.revokeObjectURL(pdfUrl);
1012
+ URL.revokeObjectURL(htmlUrl);
1013
+ }
1014
+ }, 500);
1015
+ // Safety cleanup after 2 minutes
1016
+ setTimeout(() => {
1017
+ clearInterval(checkClosed);
1018
+ URL.revokeObjectURL(pdfUrl);
1019
+ URL.revokeObjectURL(htmlUrl);
1020
+ }, 120000);
1021
+ } else {
1022
+ // Popup blocked - fall back to direct navigation
1023
+ window.open(pdfUrl, '_blank');
1024
+ setTimeout(() => URL.revokeObjectURL(pdfUrl), 60000);
1025
+ URL.revokeObjectURL(htmlUrl);
1026
+ }
1027
+ } catch (error) {
1028
+ console.error('Error printing PDF:', error);
1029
+ }
804
1030
  }
805
1031
 
806
1032
  /**
807
1033
  * Provide context menu items for right-click functionality
808
1034
  */
809
1035
  public getContextMenuItems() {
810
- return [
811
- {
812
- name: 'Open PDF in New Tab',
813
- iconName: 'lucide:ExternalLink',
814
- action: async () => {
815
- window.open(this.pdfUrl, '_blank');
816
- }
817
- },
818
- { divider: true },
819
- {
820
- name: 'Copy PDF URL',
1036
+ const items: any[] = [];
1037
+
1038
+ // Add copy option if text is selected
1039
+ const selection = window.getSelection();
1040
+ const selectedText = selection?.toString() || '';
1041
+ if (selectedText) {
1042
+ items.push({
1043
+ name: 'Copy',
821
1044
  iconName: 'lucide:Copy',
822
1045
  action: async () => {
823
- await navigator.clipboard.writeText(this.pdfUrl);
1046
+ await navigator.clipboard.writeText(selectedText);
824
1047
  }
825
- },
1048
+ });
1049
+ items.push({ divider: true });
1050
+ }
1051
+
1052
+ items.push(
826
1053
  {
827
1054
  name: 'Download PDF',
828
1055
  iconName: 'lucide:Download',
829
1056
  action: async () => {
830
- this.downloadPdf();
1057
+ await this.downloadPdf();
831
1058
  }
832
1059
  },
833
1060
  {
834
1061
  name: 'Print PDF',
835
1062
  iconName: 'lucide:Printer',
836
1063
  action: async () => {
837
- this.printPdf();
1064
+ await this.printPdf();
838
1065
  }
839
1066
  }
840
- ];
1067
+ );
1068
+
1069
+ return items;
841
1070
  }
842
1071
 
843
1072
  private get canZoomIn(): boolean {
@@ -996,6 +1225,16 @@ export class DeesPdfViewer extends DeesElement {
996
1225
  });
997
1226
  this.pageRenderTasks.clear();
998
1227
 
1228
+ // Cancel text layer render tasks
1229
+ this.textLayerRenderTasks.forEach(task => {
1230
+ try {
1231
+ task.cancel?.();
1232
+ } catch (error) {
1233
+ // Ignore cancellation errors
1234
+ }
1235
+ });
1236
+ this.textLayerRenderTasks.clear();
1237
+
999
1238
  // Cancel any thumbnail render tasks
1000
1239
  for (const task of (this.thumbnailRenderTasks || [])) {
1001
1240
  try {
@@ -276,6 +276,7 @@ export const viewerStyles = [
276
276
  border-radius: 4px;
277
277
  overflow: hidden;
278
278
  display: inline-block;
279
+ position: relative;
279
280
  }
280
281
 
281
282
  .page-canvas {
@@ -284,6 +285,52 @@ export const viewerStyles = [
284
285
  image-rendering: crisp-edges;
285
286
  }
286
287
 
288
+ /* Text layer for selection */
289
+ .text-layer {
290
+ position: absolute;
291
+ inset: 0;
292
+ overflow: visible;
293
+ line-height: 1;
294
+ text-size-adjust: none;
295
+ forced-color-adjust: none;
296
+ transform-origin: 0 0;
297
+ z-index: 1;
298
+ user-select: text;
299
+ -webkit-user-select: text;
300
+ }
301
+
302
+ .text-layer span,
303
+ .text-layer br {
304
+ color: transparent;
305
+ position: absolute;
306
+ white-space: pre;
307
+ cursor: text;
308
+ transform-origin: 0% 0%;
309
+ user-select: text;
310
+ -webkit-user-select: text;
311
+ }
312
+
313
+ .text-layer ::selection {
314
+ background: rgba(0, 100, 200, 0.3);
315
+ }
316
+
317
+ .text-layer br::selection {
318
+ background: transparent;
319
+ }
320
+
321
+ .text-layer .endOfContent {
322
+ display: block;
323
+ position: absolute;
324
+ inset: 100% 0 0;
325
+ z-index: 0;
326
+ cursor: default;
327
+ user-select: none;
328
+ }
329
+
330
+ .text-layer.selecting .endOfContent {
331
+ top: 0;
332
+ }
333
+
287
334
  .pdf-viewer.with-sidebar .viewer-main {
288
335
  margin-left: 0;
289
336
  }