nodebb-plugin-pdf-secure 1.2.12 → 1.2.13

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": "nodebb-plugin-pdf-secure",
3
- "version": "1.2.12",
3
+ "version": "1.2.13",
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": {
@@ -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: cached
80
- }, event.origin, [cached.slice(0)]); // Clone buffer since we keep original
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
@@ -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 out of iframe */
56
+ overscroll-behavior: contain;
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 to parent/iframe on touch devices */
630
+ overscroll-behavior: contain;
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: pan-x pan-y;
1536
+ }
1527
1537
  }
1528
1538
  </style>
1529
1539
  </head>
@@ -4502,11 +4512,17 @@
4502
4512
  });
4503
4513
 
4504
4514
  // ==========================================
4505
- // PINCH-TO-ZOOM (Touch devices)
4515
+ // PINCH-TO-ZOOM (Touch devices) - Smooth CSS Transform
4506
4516
  // ==========================================
4507
4517
  let pinchStartDistance = 0;
4508
4518
  let pinchStartScale = 1;
4509
4519
  let isPinching = false;
4520
+ let pinchMidX = 0;
4521
+ let pinchMidY = 0;
4522
+ let pinchVisualRatio = 1;
4523
+
4524
+ // The viewer div that holds PDF pages
4525
+ const viewerDiv = document.getElementById('viewer');
4510
4526
 
4511
4527
  function getTouchDistance(touches) {
4512
4528
  const dx = touches[0].clientX - touches[1].clientX;
@@ -4514,11 +4530,17 @@
4514
4530
  return Math.sqrt(dx * dx + dy * dy);
4515
4531
  }
4516
4532
 
4533
+ function getTouchMidpoint(touches) {
4534
+ return {
4535
+ x: (touches[0].clientX + touches[1].clientX) / 2,
4536
+ y: (touches[0].clientY + touches[1].clientY) / 2
4537
+ };
4538
+ }
4539
+
4517
4540
  container.addEventListener('touchstart', (e) => {
4518
4541
  if (e.touches.length === 2) {
4519
4542
  // Cancel any active drawing and clean up
4520
4543
  if (isDrawing && currentDrawingPage) {
4521
- // Remove incomplete path
4522
4544
  if (currentPath && currentPath.parentNode) currentPath.remove();
4523
4545
  currentPath = null;
4524
4546
  pathSegments = [];
@@ -4530,6 +4552,18 @@
4530
4552
  isPinching = true;
4531
4553
  pinchStartDistance = getTouchDistance(e.touches);
4532
4554
  pinchStartScale = pdfViewer.currentScale;
4555
+ pinchVisualRatio = 1;
4556
+
4557
+ // Get midpoint relative to container
4558
+ const mid = getTouchMidpoint(e.touches);
4559
+ const rect = container.getBoundingClientRect();
4560
+ pinchMidX = mid.x - rect.left + container.scrollLeft;
4561
+ pinchMidY = mid.y - rect.top + container.scrollTop;
4562
+
4563
+ // Prepare for CSS transform - use will-change for GPU acceleration
4564
+ viewerDiv.style.willChange = 'transform';
4565
+ viewerDiv.style.transformOrigin = pinchMidX + 'px ' + pinchMidY + 'px';
4566
+
4533
4567
  e.preventDefault();
4534
4568
  }
4535
4569
  }, { passive: false });
@@ -4538,18 +4572,79 @@
4538
4572
  if (isPinching && e.touches.length === 2) {
4539
4573
  const dist = getTouchDistance(e.touches);
4540
4574
  const ratio = dist / pinchStartDistance;
4541
- const newScale = Math.min(Math.max(pinchStartScale * ratio, 0.5), 5.0);
4542
- pdfViewer.currentScale = newScale;
4575
+ // Clamp the target scale
4576
+ const targetScale = pinchStartScale * ratio;
4577
+ const clampedScale = Math.min(Math.max(targetScale, 0.5), 5.0);
4578
+ pinchVisualRatio = clampedScale / pinchStartScale;
4579
+
4580
+ // Apply CSS transform for instant smooth visual feedback (no re-render)
4581
+ viewerDiv.style.transform = 'scale(' + pinchVisualRatio + ')';
4582
+
4543
4583
  e.preventDefault();
4544
4584
  }
4545
4585
  }, { passive: false });
4546
4586
 
4547
4587
  container.addEventListener('touchend', (e) => {
4548
- if (e.touches.length < 2) {
4588
+ if (isPinching && e.touches.length < 2) {
4549
4589
  isPinching = false;
4590
+
4591
+ // Remove CSS transform
4592
+ viewerDiv.style.transform = '';
4593
+ viewerDiv.style.willChange = '';
4594
+ viewerDiv.style.transformOrigin = '';
4595
+
4596
+ // Apply actual scale (triggers PDF re-render only once)
4597
+ const finalScale = Math.min(Math.max(pinchStartScale * pinchVisualRatio, 0.5), 5.0);
4598
+ if (Math.abs(finalScale - pdfViewer.currentScale) > 0.01) {
4599
+ pdfViewer.currentScale = finalScale;
4600
+ }
4601
+ pinchVisualRatio = 1;
4550
4602
  }
4551
4603
  });
4552
4604
 
4605
+ container.addEventListener('touchcancel', (e) => {
4606
+ if (isPinching) {
4607
+ isPinching = false;
4608
+ viewerDiv.style.transform = '';
4609
+ viewerDiv.style.willChange = '';
4610
+ viewerDiv.style.transformOrigin = '';
4611
+ pinchVisualRatio = 1;
4612
+ }
4613
+ });
4614
+
4615
+ // ==========================================
4616
+ // TOUCH SCROLL BOUNDARY (Prevent exit on tablet)
4617
+ // ==========================================
4618
+ // On touch devices, prevent scroll from escaping the container
4619
+ // when at the top/bottom boundary. Only fullscreen button should exit.
4620
+ (function initTouchScrollBoundary() {
4621
+ if (!isTouch()) return;
4622
+
4623
+ let touchStartY = 0;
4624
+
4625
+ container.addEventListener('touchstart', (e) => {
4626
+ if (e.touches.length === 1) {
4627
+ touchStartY = e.touches[0].clientY;
4628
+ }
4629
+ }, { passive: true });
4630
+
4631
+ container.addEventListener('touchmove', (e) => {
4632
+ // Skip if pinching
4633
+ if (isPinching || e.touches.length !== 1) return;
4634
+
4635
+ const touchY = e.touches[0].clientY;
4636
+ const deltaY = touchStartY - touchY; // positive = scrolling down
4637
+
4638
+ const atTop = container.scrollTop <= 0;
4639
+ const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4640
+
4641
+ // Prevent overscroll at boundaries
4642
+ if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4643
+ e.preventDefault();
4644
+ }
4645
+ }, { passive: false });
4646
+ })();
4647
+
4553
4648
  // ==========================================
4554
4649
  // CONTEXT MENU TOUCH HANDLING
4555
4650
  // ==========================================