nodebb-plugin-pdf-secure 1.2.15 → 1.2.17

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.15",
3
+ "version": "1.2.17",
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": {
@@ -22,6 +22,18 @@
22
22
  });
23
23
  })();
24
24
 
25
+ // Fullscreen API support detection
26
+ // On touch devices (tablets, mobiles), always use CSS simulated fullscreen.
27
+ // Native fullscreen has an unblockable browser "scroll-to-exit" gesture on touch devices.
28
+ // Desktop keeps native fullscreen for the proper OS-level experience.
29
+ var isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
30
+ var fullscreenApiSupported = !isTouchDevice && !!(
31
+ document.documentElement.requestFullscreen ||
32
+ document.documentElement.webkitRequestFullscreen
33
+ );
34
+ var simulatedFullscreenIframe = null;
35
+ var savedBodyOverflow = '';
36
+
25
37
  // Loading queue - only load one PDF at a time
26
38
  const loadQueue = [];
27
39
  let isLoading = false;
@@ -76,19 +88,30 @@
76
88
  });
77
89
  if (!sourceIframe) return;
78
90
 
79
- var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
80
- if (fsEl) {
81
- if (document.exitFullscreen) document.exitFullscreen();
82
- else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
91
+ if (fullscreenApiSupported) {
92
+ // Native fullscreen path
93
+ var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
94
+ if (fsEl) {
95
+ if (document.exitFullscreen) document.exitFullscreen();
96
+ else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
97
+ } else {
98
+ if (sourceIframe.requestFullscreen) sourceIframe.requestFullscreen().catch(function () { });
99
+ else if (sourceIframe.webkitRequestFullscreen) sourceIframe.webkitRequestFullscreen();
100
+ }
83
101
  } else {
84
- if (sourceIframe.requestFullscreen) sourceIframe.requestFullscreen().catch(function () { });
85
- else if (sourceIframe.webkitRequestFullscreen) sourceIframe.webkitRequestFullscreen();
102
+ // Simulated fullscreen path (iOS Safari / Chrome)
103
+ if (simulatedFullscreenIframe) {
104
+ exitSimulatedFullscreen();
105
+ } else {
106
+ enterSimulatedFullscreen(sourceIframe);
107
+ }
86
108
  }
87
109
  }
88
110
 
89
111
  // Fullscreen state query from iframe
90
112
  if (event.data && event.data.type === 'pdf-secure-fullscreen-query') {
91
- var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement);
113
+ var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement) ||
114
+ (simulatedFullscreenIframe !== null);
92
115
  if (event.source) {
93
116
  event.source.postMessage({
94
117
  type: 'pdf-secure-fullscreen-state',
@@ -126,8 +149,16 @@
126
149
  // Forward fullscreen state changes to all viewer iframes
127
150
  function notifyFullscreenChange() {
128
151
  var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement);
152
+ if (fsActive) {
153
+ document.body.style.overscrollBehavior = 'none';
154
+ document.body.style.overflow = 'hidden';
155
+ } else {
156
+ document.body.style.overscrollBehavior = '';
157
+ document.body.style.overflow = '';
158
+ }
129
159
  document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
130
160
  if (f.contentWindow) {
161
+ f.style.overscrollBehavior = 'none';
131
162
  f.contentWindow.postMessage({
132
163
  type: 'pdf-secure-fullscreen-state',
133
164
  isFullscreen: fsActive
@@ -138,6 +169,85 @@
138
169
  document.addEventListener('fullscreenchange', notifyFullscreenChange);
139
170
  document.addEventListener('webkitfullscreenchange', notifyFullscreenChange);
140
171
 
172
+ // Touch handler to block parent scroll during simulated fullscreen
173
+ function parentFullscreenTouchHandler(e) {
174
+ e.preventDefault();
175
+ }
176
+
177
+ // Enter CSS simulated fullscreen (for iOS Safari/Chrome)
178
+ function enterSimulatedFullscreen(iframe) {
179
+ if (simulatedFullscreenIframe) return; // already in simulated fullscreen
180
+ simulatedFullscreenIframe = iframe;
181
+
182
+ // Save original iframe styles
183
+ iframe._savedStyle = {
184
+ position: iframe.style.position,
185
+ top: iframe.style.top,
186
+ left: iframe.style.left,
187
+ width: iframe.style.width,
188
+ height: iframe.style.height,
189
+ zIndex: iframe.style.zIndex
190
+ };
191
+
192
+ // Apply fullscreen styles
193
+ iframe.style.position = 'fixed';
194
+ iframe.style.top = '0';
195
+ iframe.style.left = '0';
196
+ iframe.style.width = '100vw';
197
+ iframe.style.height = '100vh';
198
+ iframe.style.zIndex = '2147483647';
199
+
200
+ // Lock body scroll
201
+ savedBodyOverflow = document.body.style.overflow;
202
+ document.body.style.overflow = 'hidden';
203
+ document.body.style.overscrollBehavior = 'none';
204
+
205
+ // Block touch scroll on parent
206
+ document.addEventListener('touchmove', parentFullscreenTouchHandler, { passive: false });
207
+
208
+ // Notify iframe it is now fullscreen
209
+ if (iframe.contentWindow) {
210
+ iframe.contentWindow.postMessage({
211
+ type: 'pdf-secure-fullscreen-state',
212
+ isFullscreen: true
213
+ }, window.location.origin);
214
+ }
215
+ }
216
+
217
+ // Exit CSS simulated fullscreen
218
+ function exitSimulatedFullscreen() {
219
+ if (!simulatedFullscreenIframe) return;
220
+ var iframe = simulatedFullscreenIframe;
221
+ simulatedFullscreenIframe = null;
222
+
223
+ // Restore original iframe styles
224
+ if (iframe._savedStyle) {
225
+ iframe.style.position = iframe._savedStyle.position;
226
+ iframe.style.top = iframe._savedStyle.top;
227
+ iframe.style.left = iframe._savedStyle.left;
228
+ iframe.style.width = iframe._savedStyle.width;
229
+ iframe.style.height = iframe._savedStyle.height;
230
+ iframe.style.zIndex = iframe._savedStyle.zIndex;
231
+ delete iframe._savedStyle;
232
+ }
233
+
234
+ // Restore body scroll
235
+ document.body.style.overflow = savedBodyOverflow;
236
+ document.body.style.overscrollBehavior = '';
237
+ savedBodyOverflow = '';
238
+
239
+ // Remove parent touch block
240
+ document.removeEventListener('touchmove', parentFullscreenTouchHandler);
241
+
242
+ // Notify iframe it is no longer fullscreen
243
+ if (iframe.contentWindow) {
244
+ iframe.contentWindow.postMessage({
245
+ type: 'pdf-secure-fullscreen-state',
246
+ isFullscreen: false
247
+ }, window.location.origin);
248
+ }
249
+ }
250
+
141
251
  async function processQueue() {
142
252
  if (isLoading || loadQueue.length === 0) return;
143
253
 
@@ -169,6 +279,8 @@
169
279
  loadQueue.length = 0;
170
280
  isLoading = false;
171
281
  currentResolver = null;
282
+ // Exit simulated fullscreen on SPA navigation
283
+ exitSimulatedFullscreen();
172
284
  interceptPdfLinks();
173
285
  });
174
286
  } catch (err) {
@@ -2708,72 +2708,36 @@
2708
2708
 
2709
2709
  svg.addEventListener('touchstart', (e) => {
2710
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;
2711
+ // In annotation mode, always draw immediately — no direction detection.
2712
+ // Pan and pinch-zoom are blocked at the container level when a tool is active.
2713
+ e.preventDefault();
2724
2714
  touchStartX = e.touches[0].clientX;
2725
2715
  touchStartYDraw = e.touches[0].clientY;
2716
+ startDraw(e, pageNum);
2717
+ touchDrawing = true;
2718
+ touchDrawDecided = true;
2726
2719
  }, { passive: false, signal });
2727
2720
 
2728
2721
  svg.addEventListener('touchmove', (e) => {
2729
2722
  if (!currentTool || e.touches.length !== 1) return;
2730
-
2731
2723
  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
2724
  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
2725
  draw(e);
2761
- touchDrawing = true;
2762
2726
  }
2763
2727
  }, { passive: false, signal });
2764
2728
 
2765
2729
  svg.addEventListener('touchend', (e) => {
2766
- if (!touchDrawDecided && currentTool && currentTool !== 'eraser' && currentTool !== 'select') {
2767
- // Tap without moving > 10px → draw a dot at touch position
2730
+ if (touchDrawing) {
2731
+ stopDraw(pageNum);
2732
+ } else if (currentTool && currentTool !== 'eraser' && currentTool !== 'select') {
2733
+ // Tap without moving → draw a dot at touch position
2768
2734
  const syntheticStart = {
2769
2735
  currentTarget: svg,
2770
2736
  touches: [{ clientX: touchStartX, clientY: touchStartYDraw }],
2771
- preventDefault: () => {}
2737
+ preventDefault: () => { }
2772
2738
  };
2773
2739
  startDraw(syntheticStart, pageNum);
2774
2740
  stopDraw(pageNum);
2775
- } else if (touchDrawing) {
2776
- stopDraw(pageNum);
2777
2741
  }
2778
2742
  touchDrawDecided = false;
2779
2743
  touchDrawing = false;
@@ -4497,11 +4461,23 @@
4497
4461
  }
4498
4462
  }
4499
4463
 
4464
+ // Apply touch restrictions when entering/exiting fullscreen
4465
+ function applyFullscreenTouchRestrictions() {
4466
+ if (isFullscreen) {
4467
+ document.documentElement.style.touchAction = 'pan-y pinch-zoom';
4468
+ document.documentElement.style.overscrollBehavior = 'none';
4469
+ } else {
4470
+ document.documentElement.style.touchAction = '';
4471
+ document.documentElement.style.overscrollBehavior = '';
4472
+ }
4473
+ }
4474
+
4500
4475
  // Listen for fullscreen state from parent (iframe mode)
4501
4476
  window.addEventListener('message', (event) => {
4502
4477
  if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
4503
4478
  isFullscreen = event.data.isFullscreen;
4504
4479
  updateFullscreenIcon();
4480
+ applyFullscreenTouchRestrictions();
4505
4481
  }
4506
4482
  });
4507
4483
 
@@ -4509,10 +4485,12 @@
4509
4485
  document.addEventListener('fullscreenchange', () => {
4510
4486
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4511
4487
  updateFullscreenIcon();
4488
+ applyFullscreenTouchRestrictions();
4512
4489
  });
4513
4490
  document.addEventListener('webkitfullscreenchange', () => {
4514
4491
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4515
4492
  updateFullscreenIcon();
4493
+ applyFullscreenTouchRestrictions();
4516
4494
  });
4517
4495
 
4518
4496
  // Fullscreen button click
@@ -4637,6 +4615,11 @@
4637
4615
 
4638
4616
  container.addEventListener('touchstart', (e) => {
4639
4617
  if (e.touches.length === 2) {
4618
+ // Block pinch-zoom when an annotation tool is active
4619
+ if (annotationMode && currentTool) {
4620
+ e.preventDefault();
4621
+ return;
4622
+ }
4640
4623
  // Cancel any active drawing and clean up
4641
4624
  if (isDrawing && currentDrawingPage) {
4642
4625
  const savePage = currentDrawingPage;
@@ -4802,14 +4785,30 @@
4802
4785
  container.addEventListener('touchmove', (e) => {
4803
4786
  if (isPinching || e.touches.length !== 1) return;
4804
4787
 
4788
+ // Block all container scroll when an annotation tool is active
4789
+ if (annotationMode && currentTool) {
4790
+ e.preventDefault();
4791
+ return;
4792
+ }
4793
+
4805
4794
  const touchY = e.touches[0].clientY;
4806
4795
  const deltaY = touchStartY - touchY; // positive = scrolling down
4807
4796
 
4808
- const atTop = container.scrollTop <= 0;
4809
- const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4797
+ const BUFFER = 5; // px buffer to catch near-boundary gestures
4798
+ const atTop = container.scrollTop <= BUFFER;
4799
+ const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1 - BUFFER;
4810
4800
 
4811
- if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4812
- e.preventDefault();
4801
+ // In fullscreen, use buffer; outside fullscreen, only block at exact boundary
4802
+ if (isFullscreen) {
4803
+ if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4804
+ e.preventDefault();
4805
+ }
4806
+ } else {
4807
+ const atTopExact = container.scrollTop <= 0;
4808
+ const atBottomExact = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4809
+ if ((atTopExact && deltaY < 0) || (atBottomExact && deltaY > 0)) {
4810
+ e.preventDefault();
4811
+ }
4813
4812
  }
4814
4813
  touchStartY = touchY;
4815
4814
  }, { passive: false });
@@ -4831,11 +4830,15 @@
4831
4830
 
4832
4831
  const touchY = e.touches[0].clientY;
4833
4832
  const deltaY = docTouchStartY - touchY;
4834
- const atTop = container.scrollTop <= 0;
4835
- const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4836
4833
 
4837
- // Block pull-down at top and pull-up at bottom in fullscreen
4838
- if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4834
+ const BUFFER = 5; // px buffer to catch near-boundary gestures
4835
+ const atTop = container.scrollTop <= BUFFER;
4836
+ const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1 - BUFFER;
4837
+
4838
+ // If content fits entirely (no scroll needed), block all overscroll
4839
+ const contentFits = container.scrollHeight <= container.clientHeight + 1;
4840
+
4841
+ if (contentFits || (atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4839
4842
  e.preventDefault();
4840
4843
  }
4841
4844
  docTouchStartY = touchY;