nodebb-plugin-pdf-secure 1.2.15 → 1.2.16

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.16",
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) {
@@ -2753,7 +2753,7 @@
2753
2753
  const syntheticStart = {
2754
2754
  currentTarget: svg,
2755
2755
  touches: [{ clientX: touchStartX, clientY: touchStartYDraw }],
2756
- preventDefault: () => {}
2756
+ preventDefault: () => { }
2757
2757
  };
2758
2758
  startDraw(syntheticStart, pageNum);
2759
2759
  // Then immediately draw to current position for continuity
@@ -2768,7 +2768,7 @@
2768
2768
  const syntheticStart = {
2769
2769
  currentTarget: svg,
2770
2770
  touches: [{ clientX: touchStartX, clientY: touchStartYDraw }],
2771
- preventDefault: () => {}
2771
+ preventDefault: () => { }
2772
2772
  };
2773
2773
  startDraw(syntheticStart, pageNum);
2774
2774
  stopDraw(pageNum);
@@ -4497,11 +4497,23 @@
4497
4497
  }
4498
4498
  }
4499
4499
 
4500
+ // Apply touch restrictions when entering/exiting fullscreen
4501
+ function applyFullscreenTouchRestrictions() {
4502
+ if (isFullscreen) {
4503
+ document.documentElement.style.touchAction = 'pan-y pinch-zoom';
4504
+ document.documentElement.style.overscrollBehavior = 'none';
4505
+ } else {
4506
+ document.documentElement.style.touchAction = '';
4507
+ document.documentElement.style.overscrollBehavior = '';
4508
+ }
4509
+ }
4510
+
4500
4511
  // Listen for fullscreen state from parent (iframe mode)
4501
4512
  window.addEventListener('message', (event) => {
4502
4513
  if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
4503
4514
  isFullscreen = event.data.isFullscreen;
4504
4515
  updateFullscreenIcon();
4516
+ applyFullscreenTouchRestrictions();
4505
4517
  }
4506
4518
  });
4507
4519
 
@@ -4509,10 +4521,12 @@
4509
4521
  document.addEventListener('fullscreenchange', () => {
4510
4522
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4511
4523
  updateFullscreenIcon();
4524
+ applyFullscreenTouchRestrictions();
4512
4525
  });
4513
4526
  document.addEventListener('webkitfullscreenchange', () => {
4514
4527
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4515
4528
  updateFullscreenIcon();
4529
+ applyFullscreenTouchRestrictions();
4516
4530
  });
4517
4531
 
4518
4532
  // Fullscreen button click
@@ -4805,11 +4819,21 @@
4805
4819
  const touchY = e.touches[0].clientY;
4806
4820
  const deltaY = touchStartY - touchY; // positive = scrolling down
4807
4821
 
4808
- const atTop = container.scrollTop <= 0;
4809
- const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4822
+ const BUFFER = 5; // px buffer to catch near-boundary gestures
4823
+ const atTop = container.scrollTop <= BUFFER;
4824
+ const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1 - BUFFER;
4810
4825
 
4811
- if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4812
- e.preventDefault();
4826
+ // In fullscreen, use buffer; outside fullscreen, only block at exact boundary
4827
+ if (isFullscreen) {
4828
+ if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4829
+ e.preventDefault();
4830
+ }
4831
+ } else {
4832
+ const atTopExact = container.scrollTop <= 0;
4833
+ const atBottomExact = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4834
+ if ((atTopExact && deltaY < 0) || (atBottomExact && deltaY > 0)) {
4835
+ e.preventDefault();
4836
+ }
4813
4837
  }
4814
4838
  touchStartY = touchY;
4815
4839
  }, { passive: false });
@@ -4831,11 +4855,15 @@
4831
4855
 
4832
4856
  const touchY = e.touches[0].clientY;
4833
4857
  const deltaY = docTouchStartY - touchY;
4834
- const atTop = container.scrollTop <= 0;
4835
- const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4836
4858
 
4837
- // Block pull-down at top and pull-up at bottom in fullscreen
4838
- if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4859
+ const BUFFER = 5; // px buffer to catch near-boundary gestures
4860
+ const atTop = container.scrollTop <= BUFFER;
4861
+ const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1 - BUFFER;
4862
+
4863
+ // If content fits entirely (no scroll needed), block all overscroll
4864
+ const contentFits = container.scrollHeight <= container.clientHeight + 1;
4865
+
4866
+ if (contentFits || (atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4839
4867
  e.preventDefault();
4840
4868
  }
4841
4869
  docTouchStartY = touchY;