nodebb-plugin-pdf-secure 1.2.13 → 1.2.15

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.13",
3
+ "version": "1.2.15",
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": {
@@ -67,6 +67,36 @@
67
67
  }
68
68
  }
69
69
 
70
+ // Fullscreen toggle request from iframe viewer
71
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-toggle') {
72
+ var sourceIframe = document.querySelector('.pdf-secure-iframe');
73
+ // Find the specific iframe that sent the message
74
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
75
+ if (f.contentWindow === event.source) sourceIframe = f;
76
+ });
77
+ if (!sourceIframe) return;
78
+
79
+ var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
80
+ if (fsEl) {
81
+ if (document.exitFullscreen) document.exitFullscreen();
82
+ else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
83
+ } else {
84
+ if (sourceIframe.requestFullscreen) sourceIframe.requestFullscreen().catch(function () { });
85
+ else if (sourceIframe.webkitRequestFullscreen) sourceIframe.webkitRequestFullscreen();
86
+ }
87
+ }
88
+
89
+ // Fullscreen state query from iframe
90
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-query') {
91
+ var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement);
92
+ if (event.source) {
93
+ event.source.postMessage({
94
+ type: 'pdf-secure-fullscreen-state',
95
+ isFullscreen: fsActive
96
+ }, event.origin);
97
+ }
98
+ }
99
+
70
100
  // Viewer asking for cached buffer
71
101
  if (event.data && event.data.type === 'pdf-secure-cache-request') {
72
102
  const { filename } = event.data;
@@ -93,6 +123,21 @@
93
123
  }
94
124
  });
95
125
 
126
+ // Forward fullscreen state changes to all viewer iframes
127
+ function notifyFullscreenChange() {
128
+ var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement);
129
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
130
+ if (f.contentWindow) {
131
+ f.contentWindow.postMessage({
132
+ type: 'pdf-secure-fullscreen-state',
133
+ isFullscreen: fsActive
134
+ }, window.location.origin);
135
+ }
136
+ });
137
+ }
138
+ document.addEventListener('fullscreenchange', notifyFullscreenChange);
139
+ document.addEventListener('webkitfullscreenchange', notifyFullscreenChange);
140
+
96
141
  async function processQueue() {
97
142
  if (isLoading || loadQueue.length === 0) return;
98
143
 
@@ -278,6 +323,7 @@
278
323
  iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
279
324
  iframe.setAttribute('frameborder', '0');
280
325
  iframe.setAttribute('allowfullscreen', 'true');
326
+ iframe.setAttribute('allow', 'fullscreen');
281
327
 
282
328
  // Store resolver for postMessage callback
283
329
  currentResolver = function () {
@@ -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
@@ -4387,25 +4461,34 @@
4387
4461
  // ERGONOMIC FEATURES
4388
4462
  // ==========================================
4389
4463
 
4390
- // Fullscreen toggle function (with webkit fallback for iOS Safari)
4464
+ // Fullscreen: track state (parent-managed when in iframe)
4465
+ let isFullscreen = false;
4466
+ const inIframe = window.self !== window.top;
4467
+
4391
4468
  function toggleFullscreen() {
4392
- const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
4393
- if (fsEl) {
4394
- if (document.exitFullscreen) document.exitFullscreen();
4395
- else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
4469
+ if (inIframe) {
4470
+ // In iframe: ask parent to fullscreen the iframe element
4471
+ // Parent manages fullscreen → more stable on tablets
4472
+ window.parent.postMessage({ type: 'pdf-secure-fullscreen-toggle' }, '*');
4396
4473
  } else {
4397
- const el = document.documentElement;
4398
- if (el.requestFullscreen) el.requestFullscreen().catch(() => {});
4399
- else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
4474
+ // Standalone: use local fullscreen
4475
+ const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
4476
+ if (fsEl) {
4477
+ if (document.exitFullscreen) document.exitFullscreen();
4478
+ else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
4479
+ } else {
4480
+ const el = document.documentElement;
4481
+ if (el.requestFullscreen) el.requestFullscreen().catch(() => { });
4482
+ else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
4483
+ }
4400
4484
  }
4401
4485
  }
4402
4486
 
4403
- // Update fullscreen button icon
4487
+ // Update fullscreen button icon based on state
4404
4488
  function updateFullscreenIcon() {
4405
4489
  const icon = document.getElementById('fullscreenIcon');
4406
4490
  const btn = document.getElementById('fullscreenBtn');
4407
- const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
4408
- if (fsEl) {
4491
+ if (isFullscreen) {
4409
4492
  icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4410
4493
  btn.classList.add('active');
4411
4494
  } else {
@@ -4414,8 +4497,23 @@
4414
4497
  }
4415
4498
  }
4416
4499
 
4417
- document.addEventListener('fullscreenchange', updateFullscreenIcon);
4418
- document.addEventListener('webkitfullscreenchange', updateFullscreenIcon);
4500
+ // Listen for fullscreen state from parent (iframe mode)
4501
+ window.addEventListener('message', (event) => {
4502
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
4503
+ isFullscreen = event.data.isFullscreen;
4504
+ updateFullscreenIcon();
4505
+ }
4506
+ });
4507
+
4508
+ // Local fullscreen events (standalone mode)
4509
+ document.addEventListener('fullscreenchange', () => {
4510
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4511
+ updateFullscreenIcon();
4512
+ });
4513
+ document.addEventListener('webkitfullscreenchange', () => {
4514
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4515
+ updateFullscreenIcon();
4516
+ });
4419
4517
 
4420
4518
  // Fullscreen button click
4421
4519
  document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
@@ -4541,6 +4639,7 @@
4541
4639
  if (e.touches.length === 2) {
4542
4640
  // Cancel any active drawing and clean up
4543
4641
  if (isDrawing && currentDrawingPage) {
4642
+ const savePage = currentDrawingPage;
4544
4643
  if (currentPath && currentPath.parentNode) currentPath.remove();
4545
4644
  currentPath = null;
4546
4645
  pathSegments = [];
@@ -4548,6 +4647,7 @@
4548
4647
  isDrawing = false;
4549
4648
  currentSvg = null;
4550
4649
  currentDrawingPage = null;
4650
+ saveAnnotations(savePage);
4551
4651
  }
4552
4652
  isPinching = true;
4553
4653
  pinchStartDistance = getTouchDistance(e.touches);
@@ -4584,19 +4684,38 @@
4584
4684
  }
4585
4685
  }, { passive: false });
4586
4686
 
4687
+ let pinchJustEnded = false;
4688
+
4587
4689
  container.addEventListener('touchend', (e) => {
4588
4690
  if (isPinching && e.touches.length < 2) {
4589
4691
  isPinching = false;
4590
4692
 
4693
+ // Prevent double-tap false positive right after pinch ends
4694
+ pinchJustEnded = true;
4695
+ setTimeout(() => { pinchJustEnded = false; }, 400);
4696
+
4591
4697
  // Remove CSS transform
4592
4698
  viewerDiv.style.transform = '';
4593
4699
  viewerDiv.style.willChange = '';
4594
4700
  viewerDiv.style.transformOrigin = '';
4595
4701
 
4596
- // Apply actual scale (triggers PDF re-render only once)
4702
+ // Apply actual scale with scroll position preservation
4597
4703
  const finalScale = Math.min(Math.max(pinchStartScale * pinchVisualRatio, 0.5), 5.0);
4598
4704
  if (Math.abs(finalScale - pdfViewer.currentScale) > 0.01) {
4705
+ const scrollRatio = finalScale / pdfViewer.currentScale;
4706
+ const midRect = container.getBoundingClientRect();
4707
+ const viewCenterX = midRect.width / 2;
4708
+ const viewCenterY = midRect.height / 2;
4709
+ const oldScrollLeft = container.scrollLeft;
4710
+ const oldScrollTop = container.scrollTop;
4711
+
4599
4712
  pdfViewer.currentScale = finalScale;
4713
+
4714
+ // Adjust scroll after PDF.js re-render to keep view centered
4715
+ requestAnimationFrame(() => {
4716
+ container.scrollLeft = (oldScrollLeft + viewCenterX) * scrollRatio - viewCenterX;
4717
+ container.scrollTop = (oldScrollTop + viewCenterY) * scrollRatio - viewCenterY;
4718
+ });
4600
4719
  }
4601
4720
  pinchVisualRatio = 1;
4602
4721
  }
@@ -4612,6 +4731,54 @@
4612
4731
  }
4613
4732
  });
4614
4733
 
4734
+ // ==========================================
4735
+ // DOUBLE-TAP TO ZOOM (Tablet)
4736
+ // ==========================================
4737
+ let lastTapTime = 0;
4738
+ let lastTapX = 0;
4739
+ let lastTapY = 0;
4740
+
4741
+ container.addEventListener('touchend', (e) => {
4742
+ // Skip during pinch, multi-touch, annotation mode, or right after pinch ends
4743
+ if (isPinching || e.touches.length !== 0 || pinchJustEnded) return;
4744
+ if (annotationMode && currentTool) return;
4745
+
4746
+ const now = Date.now();
4747
+ const touch = e.changedTouches[0];
4748
+ const dx = touch.clientX - lastTapX;
4749
+ const dy = touch.clientY - lastTapY;
4750
+ const dist = Math.sqrt(dx * dx + dy * dy);
4751
+
4752
+ if (now - lastTapTime < 300 && dist < 30) {
4753
+ // Double-tap detected
4754
+ e.preventDefault();
4755
+
4756
+ if (pdfViewer.currentScaleValue === 'page-width' ||
4757
+ Math.abs(pdfViewer.currentScale - initialScale) < 0.01) {
4758
+ // Zoom in 2x, centered on tap point
4759
+ const rect = container.getBoundingClientRect();
4760
+ const tapX = touch.clientX - rect.left;
4761
+ const tapY = touch.clientY - rect.top;
4762
+
4763
+ pdfViewer.currentScale = initialScale * 2;
4764
+
4765
+ // Scroll to keep tap point centered
4766
+ container.scrollLeft = (tapX + container.scrollLeft) * 2 - tapX;
4767
+ container.scrollTop = (tapY + container.scrollTop) * 2 - tapY;
4768
+ } else {
4769
+ // Reset to page-width
4770
+ pdfViewer.currentScaleValue = 'page-width';
4771
+ }
4772
+
4773
+ lastTapTime = 0;
4774
+ return;
4775
+ }
4776
+
4777
+ lastTapTime = now;
4778
+ lastTapX = touch.clientX;
4779
+ lastTapY = touch.clientY;
4780
+ });
4781
+
4615
4782
  // ==========================================
4616
4783
  // TOUCH SCROLL BOUNDARY (Prevent exit on tablet)
4617
4784
  // ==========================================
@@ -4620,6 +4787,9 @@
4620
4787
  (function initTouchScrollBoundary() {
4621
4788
  if (!isTouch()) return;
4622
4789
 
4790
+ // Ensure html element also blocks overscroll
4791
+ document.documentElement.style.overscrollBehavior = 'none';
4792
+
4623
4793
  let touchStartY = 0;
4624
4794
 
4625
4795
  container.addEventListener('touchstart', (e) => {
@@ -4628,8 +4798,8 @@
4628
4798
  }
4629
4799
  }, { passive: true });
4630
4800
 
4801
+ // Container-level boundary prevention
4631
4802
  container.addEventListener('touchmove', (e) => {
4632
- // Skip if pinching
4633
4803
  if (isPinching || e.touches.length !== 1) return;
4634
4804
 
4635
4805
  const touchY = e.touches[0].clientY;
@@ -4638,11 +4808,38 @@
4638
4808
  const atTop = container.scrollTop <= 0;
4639
4809
  const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4640
4810
 
4641
- // Prevent overscroll at boundaries
4642
4811
  if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4643
4812
  e.preventDefault();
4644
4813
  }
4814
+ touchStartY = touchY;
4645
4815
  }, { passive: false });
4816
+
4817
+ // Document-level capture handler: catch overscroll gestures
4818
+ // that escape the container (e.g. browser pull-to-exit-fullscreen)
4819
+ let docTouchStartY = 0;
4820
+ document.addEventListener('touchstart', (e) => {
4821
+ if (e.touches.length === 1) {
4822
+ docTouchStartY = e.touches[0].clientY;
4823
+ }
4824
+ }, { passive: true, capture: true });
4825
+
4826
+ document.addEventListener('touchmove', (e) => {
4827
+ if (e.touches.length !== 1) return;
4828
+
4829
+ // Only prevent when in fullscreen (isFullscreen works in both iframe and standalone)
4830
+ if (!isFullscreen) return;
4831
+
4832
+ const touchY = e.touches[0].clientY;
4833
+ const deltaY = docTouchStartY - touchY;
4834
+ const atTop = container.scrollTop <= 0;
4835
+ const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4836
+
4837
+ // Block pull-down at top and pull-up at bottom in fullscreen
4838
+ if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4839
+ e.preventDefault();
4840
+ }
4841
+ docTouchStartY = touchY;
4842
+ }, { passive: false, capture: true });
4646
4843
  })();
4647
4844
 
4648
4845
  // ==========================================