nodebb-plugin-pdf-secure 1.2.13 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/static/viewer.html +191 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure",
3
- "version": "1.2.13",
3
+ "version": "1.2.14",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -52,8 +52,8 @@
52
52
  -moz-user-select: none;
53
53
  -ms-user-select: none;
54
54
  user-select: none;
55
- /* Prevent scroll chaining out of iframe */
56
- overscroll-behavior: contain;
55
+ /* Prevent scroll chaining and browser overscroll gestures (e.g. pull-to-exit-fullscreen) */
56
+ overscroll-behavior: none;
57
57
  }
58
58
 
59
59
  /* Print Protection - hide everything when printing */
@@ -626,8 +626,8 @@
626
626
  overflow: auto;
627
627
  background: #525659;
628
628
  z-index: 1;
629
- /* Prevent scroll chaining to parent/iframe on touch devices */
630
- overscroll-behavior: contain;
629
+ /* Prevent scroll chaining and overscroll gestures on touch devices */
630
+ overscroll-behavior: none;
631
631
  -webkit-overflow-scrolling: touch;
632
632
  }
633
633
 
@@ -1532,7 +1532,7 @@
1532
1532
 
1533
1533
  /* Let our JS handle pinch-to-zoom, but allow browser pan-y for scroll */
1534
1534
  #viewerContainer {
1535
- touch-action: pan-x pan-y;
1535
+ touch-action: manipulation;
1536
1536
  }
1537
1537
  }
1538
1538
  </style>
@@ -2253,8 +2253,10 @@
2253
2253
  }
2254
2254
 
2255
2255
  // Events
2256
+ let initialScale = 1;
2256
2257
  eventBus.on('pagesinit', () => {
2257
2258
  pdfViewer.currentScaleValue = 'page-width';
2259
+ initialScale = pdfViewer.currentScale;
2258
2260
  document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
2259
2261
  });
2260
2262
 
@@ -2698,17 +2700,89 @@
2698
2700
  svg.addEventListener('mouseleave', () => stopDraw(pageNum), { signal });
2699
2701
 
2700
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
+
2701
2709
  svg.addEventListener('touchstart', (e) => {
2702
- // Prevent default to avoid scroll while drawing/selecting
2703
- if (currentTool) e.preventDefault();
2704
- startDraw(e, pageNum);
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;
2705
2726
  }, { passive: false, signal });
2727
+
2706
2728
  svg.addEventListener('touchmove', (e) => {
2707
- if (currentTool) e.preventDefault();
2708
- draw(e);
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
+ }
2709
2763
  }, { passive: false, signal });
2710
- svg.addEventListener('touchend', () => stopDraw(pageNum), { signal });
2711
- svg.addEventListener('touchcancel', () => stopDraw(pageNum), { signal });
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 });
2712
2786
 
2713
2787
  svg.classList.toggle('active', annotationMode);
2714
2788
  }
@@ -2904,7 +2978,7 @@
2904
2978
  // Save current state to undo stack with rotation
2905
2979
  if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
2906
2980
  const stack = undoStacks.get(pageNum);
2907
- stack.push({ html: svg.innerHTML, rotation: pdfViewer.pagesRotation || 0 });
2981
+ stack.push({ html: getCleanSvgInnerHTML(svg), rotation: pdfViewer.pagesRotation || 0 });
2908
2982
  if (stack.length > MAX_HISTORY) stack.shift();
2909
2983
 
2910
2984
  // Clear redo stack
@@ -4395,7 +4469,7 @@
4395
4469
  else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
4396
4470
  } else {
4397
4471
  const el = document.documentElement;
4398
- if (el.requestFullscreen) el.requestFullscreen().catch(() => {});
4472
+ if (el.requestFullscreen) el.requestFullscreen().catch(() => { });
4399
4473
  else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
4400
4474
  }
4401
4475
  }
@@ -4541,6 +4615,7 @@
4541
4615
  if (e.touches.length === 2) {
4542
4616
  // Cancel any active drawing and clean up
4543
4617
  if (isDrawing && currentDrawingPage) {
4618
+ const savePage = currentDrawingPage;
4544
4619
  if (currentPath && currentPath.parentNode) currentPath.remove();
4545
4620
  currentPath = null;
4546
4621
  pathSegments = [];
@@ -4548,6 +4623,7 @@
4548
4623
  isDrawing = false;
4549
4624
  currentSvg = null;
4550
4625
  currentDrawingPage = null;
4626
+ saveAnnotations(savePage);
4551
4627
  }
4552
4628
  isPinching = true;
4553
4629
  pinchStartDistance = getTouchDistance(e.touches);
@@ -4584,19 +4660,38 @@
4584
4660
  }
4585
4661
  }, { passive: false });
4586
4662
 
4663
+ let pinchJustEnded = false;
4664
+
4587
4665
  container.addEventListener('touchend', (e) => {
4588
4666
  if (isPinching && e.touches.length < 2) {
4589
4667
  isPinching = false;
4590
4668
 
4669
+ // Prevent double-tap false positive right after pinch ends
4670
+ pinchJustEnded = true;
4671
+ setTimeout(() => { pinchJustEnded = false; }, 400);
4672
+
4591
4673
  // Remove CSS transform
4592
4674
  viewerDiv.style.transform = '';
4593
4675
  viewerDiv.style.willChange = '';
4594
4676
  viewerDiv.style.transformOrigin = '';
4595
4677
 
4596
- // Apply actual scale (triggers PDF re-render only once)
4678
+ // Apply actual scale with scroll position preservation
4597
4679
  const finalScale = Math.min(Math.max(pinchStartScale * pinchVisualRatio, 0.5), 5.0);
4598
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
+
4599
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
+ });
4600
4695
  }
4601
4696
  pinchVisualRatio = 1;
4602
4697
  }
@@ -4612,6 +4707,54 @@
4612
4707
  }
4613
4708
  });
4614
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
+
4615
4758
  // ==========================================
4616
4759
  // TOUCH SCROLL BOUNDARY (Prevent exit on tablet)
4617
4760
  // ==========================================
@@ -4620,6 +4763,9 @@
4620
4763
  (function initTouchScrollBoundary() {
4621
4764
  if (!isTouch()) return;
4622
4765
 
4766
+ // Ensure html element also blocks overscroll
4767
+ document.documentElement.style.overscrollBehavior = 'none';
4768
+
4623
4769
  let touchStartY = 0;
4624
4770
 
4625
4771
  container.addEventListener('touchstart', (e) => {
@@ -4628,8 +4774,8 @@
4628
4774
  }
4629
4775
  }, { passive: true });
4630
4776
 
4777
+ // Container-level boundary prevention
4631
4778
  container.addEventListener('touchmove', (e) => {
4632
- // Skip if pinching
4633
4779
  if (isPinching || e.touches.length !== 1) return;
4634
4780
 
4635
4781
  const touchY = e.touches[0].clientY;
@@ -4638,11 +4784,39 @@
4638
4784
  const atTop = container.scrollTop <= 0;
4639
4785
  const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4640
4786
 
4641
- // Prevent overscroll at boundaries
4642
4787
  if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4643
4788
  e.preventDefault();
4644
4789
  }
4790
+ touchStartY = touchY;
4645
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 });
4646
4820
  })();
4647
4821
 
4648
4822
  // ==========================================