@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/dist_bundle/bundle.js +254 -26
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-media/dees-pdf-viewer/component.d.ts +4 -11
- package/dist_ts_web/elements/00group-media/dees-pdf-viewer/component.js +250 -36
- package/dist_ts_web/elements/00group-media/dees-pdf-viewer/styles.js +48 -1
- package/dist_watch/bundle.js +252 -24
- package/dist_watch/bundle.js.map +3 -3
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-media/dees-pdf-viewer/component.ts +264 -25
- package/ts_web/elements/00group-media/dees-pdf-viewer/styles.ts +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-catalog",
|
|
3
|
-
"version": "3.41.
|
|
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.
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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(
|
|
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
|
}
|