nodebb-plugin-pdf-secure 1.2.12 → 1.2.14
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 +1 -1
- package/static/lib/main.js +4 -2
- package/static/viewer.html +283 -14
package/package.json
CHANGED
package/static/lib/main.js
CHANGED
|
@@ -73,11 +73,13 @@
|
|
|
73
73
|
const cached = pdfBufferCache.get(filename);
|
|
74
74
|
if (cached && event.source) {
|
|
75
75
|
// Send cached buffer to viewer (transferable for 0-copy)
|
|
76
|
+
// Clone once: keep original in cache, transfer the copy
|
|
77
|
+
const copy = cached.slice(0);
|
|
76
78
|
event.source.postMessage({
|
|
77
79
|
type: 'pdf-secure-cache-response',
|
|
78
80
|
filename: filename,
|
|
79
|
-
buffer:
|
|
80
|
-
}, event.origin, [
|
|
81
|
+
buffer: copy
|
|
82
|
+
}, event.origin, [copy]);
|
|
81
83
|
console.log('[PDF-Secure] Cache: Hit -', filename);
|
|
82
84
|
} else if (event.source) {
|
|
83
85
|
// No cache, viewer will fetch normally
|
package/static/viewer.html
CHANGED
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
-moz-user-select: none;
|
|
53
53
|
-ms-user-select: none;
|
|
54
54
|
user-select: none;
|
|
55
|
+
/* Prevent scroll chaining and browser overscroll gestures (e.g. pull-to-exit-fullscreen) */
|
|
56
|
+
overscroll-behavior: none;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/* Print Protection - hide everything when printing */
|
|
@@ -624,6 +626,9 @@
|
|
|
624
626
|
overflow: auto;
|
|
625
627
|
background: #525659;
|
|
626
628
|
z-index: 1;
|
|
629
|
+
/* Prevent scroll chaining and overscroll gestures on touch devices */
|
|
630
|
+
overscroll-behavior: none;
|
|
631
|
+
-webkit-overflow-scrolling: touch;
|
|
627
632
|
}
|
|
628
633
|
|
|
629
634
|
#viewerContainer.withSidebar {
|
|
@@ -1524,6 +1529,11 @@
|
|
|
1524
1529
|
width: 56px;
|
|
1525
1530
|
height: 56px;
|
|
1526
1531
|
}
|
|
1532
|
+
|
|
1533
|
+
/* Let our JS handle pinch-to-zoom, but allow browser pan-y for scroll */
|
|
1534
|
+
#viewerContainer {
|
|
1535
|
+
touch-action: manipulation;
|
|
1536
|
+
}
|
|
1527
1537
|
}
|
|
1528
1538
|
</style>
|
|
1529
1539
|
</head>
|
|
@@ -2243,8 +2253,10 @@
|
|
|
2243
2253
|
}
|
|
2244
2254
|
|
|
2245
2255
|
// Events
|
|
2256
|
+
let initialScale = 1;
|
|
2246
2257
|
eventBus.on('pagesinit', () => {
|
|
2247
2258
|
pdfViewer.currentScaleValue = 'page-width';
|
|
2259
|
+
initialScale = pdfViewer.currentScale;
|
|
2248
2260
|
document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
|
|
2249
2261
|
});
|
|
2250
2262
|
|
|
@@ -2688,17 +2700,89 @@
|
|
|
2688
2700
|
svg.addEventListener('mouseleave', () => stopDraw(pageNum), { signal });
|
|
2689
2701
|
|
|
2690
2702
|
// Touch support for tablets
|
|
2703
|
+
// Uses direction detection: predominantly vertical = scroll, otherwise = draw
|
|
2704
|
+
let touchDrawDecided = false;
|
|
2705
|
+
let touchDrawing = false;
|
|
2706
|
+
let touchStartX = 0;
|
|
2707
|
+
let touchStartYDraw = 0;
|
|
2708
|
+
|
|
2691
2709
|
svg.addEventListener('touchstart', (e) => {
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2710
|
+
if (!currentTool || e.touches.length !== 1) return;
|
|
2711
|
+
|
|
2712
|
+
// Eraser and select need immediate response
|
|
2713
|
+
if (currentTool === 'eraser' || currentTool === 'select') {
|
|
2714
|
+
e.preventDefault();
|
|
2715
|
+
startDraw(e, pageNum);
|
|
2716
|
+
touchDrawing = true;
|
|
2717
|
+
touchDrawDecided = true;
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// For pen/highlight/shape: wait to decide scroll vs draw
|
|
2722
|
+
touchDrawDecided = false;
|
|
2723
|
+
touchDrawing = false;
|
|
2724
|
+
touchStartX = e.touches[0].clientX;
|
|
2725
|
+
touchStartYDraw = e.touches[0].clientY;
|
|
2695
2726
|
}, { passive: false, signal });
|
|
2727
|
+
|
|
2696
2728
|
svg.addEventListener('touchmove', (e) => {
|
|
2697
|
-
if (currentTool
|
|
2698
|
-
|
|
2729
|
+
if (!currentTool || e.touches.length !== 1) return;
|
|
2730
|
+
|
|
2731
|
+
if (touchDrawing) {
|
|
2732
|
+
// Already decided: drawing
|
|
2733
|
+
e.preventDefault();
|
|
2734
|
+
draw(e);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (touchDrawDecided) return; // decided scroll, let it through
|
|
2739
|
+
|
|
2740
|
+
const dx = e.touches[0].clientX - touchStartX;
|
|
2741
|
+
const dy = e.touches[0].clientY - touchStartYDraw;
|
|
2742
|
+
|
|
2743
|
+
if (Math.abs(dx) + Math.abs(dy) > 10) {
|
|
2744
|
+
touchDrawDecided = true;
|
|
2745
|
+
|
|
2746
|
+
if (Math.abs(dy) > Math.abs(dx) * 2) {
|
|
2747
|
+
// Predominantly vertical → scroll (don't prevent default)
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
// Horizontal or diagonal → drawing
|
|
2751
|
+
e.preventDefault();
|
|
2752
|
+
// Start draw from ORIGINAL touch position (not current 10px-offset position)
|
|
2753
|
+
const syntheticStart = {
|
|
2754
|
+
currentTarget: svg,
|
|
2755
|
+
touches: [{ clientX: touchStartX, clientY: touchStartYDraw }],
|
|
2756
|
+
preventDefault: () => {}
|
|
2757
|
+
};
|
|
2758
|
+
startDraw(syntheticStart, pageNum);
|
|
2759
|
+
// Then immediately draw to current position for continuity
|
|
2760
|
+
draw(e);
|
|
2761
|
+
touchDrawing = true;
|
|
2762
|
+
}
|
|
2699
2763
|
}, { passive: false, signal });
|
|
2700
|
-
|
|
2701
|
-
svg.addEventListener('
|
|
2764
|
+
|
|
2765
|
+
svg.addEventListener('touchend', (e) => {
|
|
2766
|
+
if (!touchDrawDecided && currentTool && currentTool !== 'eraser' && currentTool !== 'select') {
|
|
2767
|
+
// Tap without moving > 10px → draw a dot at touch position
|
|
2768
|
+
const syntheticStart = {
|
|
2769
|
+
currentTarget: svg,
|
|
2770
|
+
touches: [{ clientX: touchStartX, clientY: touchStartYDraw }],
|
|
2771
|
+
preventDefault: () => {}
|
|
2772
|
+
};
|
|
2773
|
+
startDraw(syntheticStart, pageNum);
|
|
2774
|
+
stopDraw(pageNum);
|
|
2775
|
+
} else if (touchDrawing) {
|
|
2776
|
+
stopDraw(pageNum);
|
|
2777
|
+
}
|
|
2778
|
+
touchDrawDecided = false;
|
|
2779
|
+
touchDrawing = false;
|
|
2780
|
+
}, { signal });
|
|
2781
|
+
svg.addEventListener('touchcancel', () => {
|
|
2782
|
+
if (touchDrawing) stopDraw(pageNum);
|
|
2783
|
+
touchDrawDecided = false;
|
|
2784
|
+
touchDrawing = false;
|
|
2785
|
+
}, { signal });
|
|
2702
2786
|
|
|
2703
2787
|
svg.classList.toggle('active', annotationMode);
|
|
2704
2788
|
}
|
|
@@ -2894,7 +2978,7 @@
|
|
|
2894
2978
|
// Save current state to undo stack with rotation
|
|
2895
2979
|
if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
|
|
2896
2980
|
const stack = undoStacks.get(pageNum);
|
|
2897
|
-
stack.push({ html: svg
|
|
2981
|
+
stack.push({ html: getCleanSvgInnerHTML(svg), rotation: pdfViewer.pagesRotation || 0 });
|
|
2898
2982
|
if (stack.length > MAX_HISTORY) stack.shift();
|
|
2899
2983
|
|
|
2900
2984
|
// Clear redo stack
|
|
@@ -4385,7 +4469,7 @@
|
|
|
4385
4469
|
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
|
|
4386
4470
|
} else {
|
|
4387
4471
|
const el = document.documentElement;
|
|
4388
|
-
if (el.requestFullscreen) el.requestFullscreen().catch(() => {});
|
|
4472
|
+
if (el.requestFullscreen) el.requestFullscreen().catch(() => { });
|
|
4389
4473
|
else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
|
|
4390
4474
|
}
|
|
4391
4475
|
}
|
|
@@ -4502,11 +4586,17 @@
|
|
|
4502
4586
|
});
|
|
4503
4587
|
|
|
4504
4588
|
// ==========================================
|
|
4505
|
-
// PINCH-TO-ZOOM (Touch devices)
|
|
4589
|
+
// PINCH-TO-ZOOM (Touch devices) - Smooth CSS Transform
|
|
4506
4590
|
// ==========================================
|
|
4507
4591
|
let pinchStartDistance = 0;
|
|
4508
4592
|
let pinchStartScale = 1;
|
|
4509
4593
|
let isPinching = false;
|
|
4594
|
+
let pinchMidX = 0;
|
|
4595
|
+
let pinchMidY = 0;
|
|
4596
|
+
let pinchVisualRatio = 1;
|
|
4597
|
+
|
|
4598
|
+
// The viewer div that holds PDF pages
|
|
4599
|
+
const viewerDiv = document.getElementById('viewer');
|
|
4510
4600
|
|
|
4511
4601
|
function getTouchDistance(touches) {
|
|
4512
4602
|
const dx = touches[0].clientX - touches[1].clientX;
|
|
@@ -4514,11 +4604,18 @@
|
|
|
4514
4604
|
return Math.sqrt(dx * dx + dy * dy);
|
|
4515
4605
|
}
|
|
4516
4606
|
|
|
4607
|
+
function getTouchMidpoint(touches) {
|
|
4608
|
+
return {
|
|
4609
|
+
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
4610
|
+
y: (touches[0].clientY + touches[1].clientY) / 2
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4517
4614
|
container.addEventListener('touchstart', (e) => {
|
|
4518
4615
|
if (e.touches.length === 2) {
|
|
4519
4616
|
// Cancel any active drawing and clean up
|
|
4520
4617
|
if (isDrawing && currentDrawingPage) {
|
|
4521
|
-
|
|
4618
|
+
const savePage = currentDrawingPage;
|
|
4522
4619
|
if (currentPath && currentPath.parentNode) currentPath.remove();
|
|
4523
4620
|
currentPath = null;
|
|
4524
4621
|
pathSegments = [];
|
|
@@ -4526,10 +4623,23 @@
|
|
|
4526
4623
|
isDrawing = false;
|
|
4527
4624
|
currentSvg = null;
|
|
4528
4625
|
currentDrawingPage = null;
|
|
4626
|
+
saveAnnotations(savePage);
|
|
4529
4627
|
}
|
|
4530
4628
|
isPinching = true;
|
|
4531
4629
|
pinchStartDistance = getTouchDistance(e.touches);
|
|
4532
4630
|
pinchStartScale = pdfViewer.currentScale;
|
|
4631
|
+
pinchVisualRatio = 1;
|
|
4632
|
+
|
|
4633
|
+
// Get midpoint relative to container
|
|
4634
|
+
const mid = getTouchMidpoint(e.touches);
|
|
4635
|
+
const rect = container.getBoundingClientRect();
|
|
4636
|
+
pinchMidX = mid.x - rect.left + container.scrollLeft;
|
|
4637
|
+
pinchMidY = mid.y - rect.top + container.scrollTop;
|
|
4638
|
+
|
|
4639
|
+
// Prepare for CSS transform - use will-change for GPU acceleration
|
|
4640
|
+
viewerDiv.style.willChange = 'transform';
|
|
4641
|
+
viewerDiv.style.transformOrigin = pinchMidX + 'px ' + pinchMidY + 'px';
|
|
4642
|
+
|
|
4533
4643
|
e.preventDefault();
|
|
4534
4644
|
}
|
|
4535
4645
|
}, { passive: false });
|
|
@@ -4538,18 +4648,177 @@
|
|
|
4538
4648
|
if (isPinching && e.touches.length === 2) {
|
|
4539
4649
|
const dist = getTouchDistance(e.touches);
|
|
4540
4650
|
const ratio = dist / pinchStartDistance;
|
|
4541
|
-
|
|
4542
|
-
|
|
4651
|
+
// Clamp the target scale
|
|
4652
|
+
const targetScale = pinchStartScale * ratio;
|
|
4653
|
+
const clampedScale = Math.min(Math.max(targetScale, 0.5), 5.0);
|
|
4654
|
+
pinchVisualRatio = clampedScale / pinchStartScale;
|
|
4655
|
+
|
|
4656
|
+
// Apply CSS transform for instant smooth visual feedback (no re-render)
|
|
4657
|
+
viewerDiv.style.transform = 'scale(' + pinchVisualRatio + ')';
|
|
4658
|
+
|
|
4543
4659
|
e.preventDefault();
|
|
4544
4660
|
}
|
|
4545
4661
|
}, { passive: false });
|
|
4546
4662
|
|
|
4663
|
+
let pinchJustEnded = false;
|
|
4664
|
+
|
|
4547
4665
|
container.addEventListener('touchend', (e) => {
|
|
4548
|
-
if (e.touches.length < 2) {
|
|
4666
|
+
if (isPinching && e.touches.length < 2) {
|
|
4549
4667
|
isPinching = false;
|
|
4668
|
+
|
|
4669
|
+
// Prevent double-tap false positive right after pinch ends
|
|
4670
|
+
pinchJustEnded = true;
|
|
4671
|
+
setTimeout(() => { pinchJustEnded = false; }, 400);
|
|
4672
|
+
|
|
4673
|
+
// Remove CSS transform
|
|
4674
|
+
viewerDiv.style.transform = '';
|
|
4675
|
+
viewerDiv.style.willChange = '';
|
|
4676
|
+
viewerDiv.style.transformOrigin = '';
|
|
4677
|
+
|
|
4678
|
+
// Apply actual scale with scroll position preservation
|
|
4679
|
+
const finalScale = Math.min(Math.max(pinchStartScale * pinchVisualRatio, 0.5), 5.0);
|
|
4680
|
+
if (Math.abs(finalScale - pdfViewer.currentScale) > 0.01) {
|
|
4681
|
+
const scrollRatio = finalScale / pdfViewer.currentScale;
|
|
4682
|
+
const midRect = container.getBoundingClientRect();
|
|
4683
|
+
const viewCenterX = midRect.width / 2;
|
|
4684
|
+
const viewCenterY = midRect.height / 2;
|
|
4685
|
+
const oldScrollLeft = container.scrollLeft;
|
|
4686
|
+
const oldScrollTop = container.scrollTop;
|
|
4687
|
+
|
|
4688
|
+
pdfViewer.currentScale = finalScale;
|
|
4689
|
+
|
|
4690
|
+
// Adjust scroll after PDF.js re-render to keep view centered
|
|
4691
|
+
requestAnimationFrame(() => {
|
|
4692
|
+
container.scrollLeft = (oldScrollLeft + viewCenterX) * scrollRatio - viewCenterX;
|
|
4693
|
+
container.scrollTop = (oldScrollTop + viewCenterY) * scrollRatio - viewCenterY;
|
|
4694
|
+
});
|
|
4695
|
+
}
|
|
4696
|
+
pinchVisualRatio = 1;
|
|
4550
4697
|
}
|
|
4551
4698
|
});
|
|
4552
4699
|
|
|
4700
|
+
container.addEventListener('touchcancel', (e) => {
|
|
4701
|
+
if (isPinching) {
|
|
4702
|
+
isPinching = false;
|
|
4703
|
+
viewerDiv.style.transform = '';
|
|
4704
|
+
viewerDiv.style.willChange = '';
|
|
4705
|
+
viewerDiv.style.transformOrigin = '';
|
|
4706
|
+
pinchVisualRatio = 1;
|
|
4707
|
+
}
|
|
4708
|
+
});
|
|
4709
|
+
|
|
4710
|
+
// ==========================================
|
|
4711
|
+
// DOUBLE-TAP TO ZOOM (Tablet)
|
|
4712
|
+
// ==========================================
|
|
4713
|
+
let lastTapTime = 0;
|
|
4714
|
+
let lastTapX = 0;
|
|
4715
|
+
let lastTapY = 0;
|
|
4716
|
+
|
|
4717
|
+
container.addEventListener('touchend', (e) => {
|
|
4718
|
+
// Skip during pinch, multi-touch, annotation mode, or right after pinch ends
|
|
4719
|
+
if (isPinching || e.touches.length !== 0 || pinchJustEnded) return;
|
|
4720
|
+
if (annotationMode && currentTool) return;
|
|
4721
|
+
|
|
4722
|
+
const now = Date.now();
|
|
4723
|
+
const touch = e.changedTouches[0];
|
|
4724
|
+
const dx = touch.clientX - lastTapX;
|
|
4725
|
+
const dy = touch.clientY - lastTapY;
|
|
4726
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
4727
|
+
|
|
4728
|
+
if (now - lastTapTime < 300 && dist < 30) {
|
|
4729
|
+
// Double-tap detected
|
|
4730
|
+
e.preventDefault();
|
|
4731
|
+
|
|
4732
|
+
if (pdfViewer.currentScaleValue === 'page-width' ||
|
|
4733
|
+
Math.abs(pdfViewer.currentScale - initialScale) < 0.01) {
|
|
4734
|
+
// Zoom in 2x, centered on tap point
|
|
4735
|
+
const rect = container.getBoundingClientRect();
|
|
4736
|
+
const tapX = touch.clientX - rect.left;
|
|
4737
|
+
const tapY = touch.clientY - rect.top;
|
|
4738
|
+
|
|
4739
|
+
pdfViewer.currentScale = initialScale * 2;
|
|
4740
|
+
|
|
4741
|
+
// Scroll to keep tap point centered
|
|
4742
|
+
container.scrollLeft = (tapX + container.scrollLeft) * 2 - tapX;
|
|
4743
|
+
container.scrollTop = (tapY + container.scrollTop) * 2 - tapY;
|
|
4744
|
+
} else {
|
|
4745
|
+
// Reset to page-width
|
|
4746
|
+
pdfViewer.currentScaleValue = 'page-width';
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
lastTapTime = 0;
|
|
4750
|
+
return;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
lastTapTime = now;
|
|
4754
|
+
lastTapX = touch.clientX;
|
|
4755
|
+
lastTapY = touch.clientY;
|
|
4756
|
+
});
|
|
4757
|
+
|
|
4758
|
+
// ==========================================
|
|
4759
|
+
// TOUCH SCROLL BOUNDARY (Prevent exit on tablet)
|
|
4760
|
+
// ==========================================
|
|
4761
|
+
// On touch devices, prevent scroll from escaping the container
|
|
4762
|
+
// when at the top/bottom boundary. Only fullscreen button should exit.
|
|
4763
|
+
(function initTouchScrollBoundary() {
|
|
4764
|
+
if (!isTouch()) return;
|
|
4765
|
+
|
|
4766
|
+
// Ensure html element also blocks overscroll
|
|
4767
|
+
document.documentElement.style.overscrollBehavior = 'none';
|
|
4768
|
+
|
|
4769
|
+
let touchStartY = 0;
|
|
4770
|
+
|
|
4771
|
+
container.addEventListener('touchstart', (e) => {
|
|
4772
|
+
if (e.touches.length === 1) {
|
|
4773
|
+
touchStartY = e.touches[0].clientY;
|
|
4774
|
+
}
|
|
4775
|
+
}, { passive: true });
|
|
4776
|
+
|
|
4777
|
+
// Container-level boundary prevention
|
|
4778
|
+
container.addEventListener('touchmove', (e) => {
|
|
4779
|
+
if (isPinching || e.touches.length !== 1) return;
|
|
4780
|
+
|
|
4781
|
+
const touchY = e.touches[0].clientY;
|
|
4782
|
+
const deltaY = touchStartY - touchY; // positive = scrolling down
|
|
4783
|
+
|
|
4784
|
+
const atTop = container.scrollTop <= 0;
|
|
4785
|
+
const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
|
|
4786
|
+
|
|
4787
|
+
if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
|
|
4788
|
+
e.preventDefault();
|
|
4789
|
+
}
|
|
4790
|
+
touchStartY = touchY;
|
|
4791
|
+
}, { passive: false });
|
|
4792
|
+
|
|
4793
|
+
// Document-level capture handler: catch overscroll gestures
|
|
4794
|
+
// that escape the container (e.g. browser pull-to-exit-fullscreen)
|
|
4795
|
+
let docTouchStartY = 0;
|
|
4796
|
+
document.addEventListener('touchstart', (e) => {
|
|
4797
|
+
if (e.touches.length === 1) {
|
|
4798
|
+
docTouchStartY = e.touches[0].clientY;
|
|
4799
|
+
}
|
|
4800
|
+
}, { passive: true, capture: true });
|
|
4801
|
+
|
|
4802
|
+
document.addEventListener('touchmove', (e) => {
|
|
4803
|
+
if (e.touches.length !== 1) return;
|
|
4804
|
+
|
|
4805
|
+
// Only prevent when in fullscreen
|
|
4806
|
+
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
|
|
4807
|
+
if (!fsEl) return;
|
|
4808
|
+
|
|
4809
|
+
const touchY = e.touches[0].clientY;
|
|
4810
|
+
const deltaY = docTouchStartY - touchY;
|
|
4811
|
+
const atTop = container.scrollTop <= 0;
|
|
4812
|
+
const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
|
|
4813
|
+
|
|
4814
|
+
// Block pull-down at top and pull-up at bottom in fullscreen
|
|
4815
|
+
if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
|
|
4816
|
+
e.preventDefault();
|
|
4817
|
+
}
|
|
4818
|
+
docTouchStartY = touchY;
|
|
4819
|
+
}, { passive: false, capture: true });
|
|
4820
|
+
})();
|
|
4821
|
+
|
|
4553
4822
|
// ==========================================
|
|
4554
4823
|
// CONTEXT MENU TOUCH HANDLING
|
|
4555
4824
|
// ==========================================
|