nodebb-plugin-pdf-secure 1.2.14 → 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.14",
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;
@@ -67,6 +79,47 @@
67
79
  }
68
80
  }
69
81
 
82
+ // Fullscreen toggle request from iframe viewer
83
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-toggle') {
84
+ var sourceIframe = document.querySelector('.pdf-secure-iframe');
85
+ // Find the specific iframe that sent the message
86
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
87
+ if (f.contentWindow === event.source) sourceIframe = f;
88
+ });
89
+ if (!sourceIframe) return;
90
+
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
+ }
101
+ } else {
102
+ // Simulated fullscreen path (iOS Safari / Chrome)
103
+ if (simulatedFullscreenIframe) {
104
+ exitSimulatedFullscreen();
105
+ } else {
106
+ enterSimulatedFullscreen(sourceIframe);
107
+ }
108
+ }
109
+ }
110
+
111
+ // Fullscreen state query from iframe
112
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-query') {
113
+ var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement) ||
114
+ (simulatedFullscreenIframe !== null);
115
+ if (event.source) {
116
+ event.source.postMessage({
117
+ type: 'pdf-secure-fullscreen-state',
118
+ isFullscreen: fsActive
119
+ }, event.origin);
120
+ }
121
+ }
122
+
70
123
  // Viewer asking for cached buffer
71
124
  if (event.data && event.data.type === 'pdf-secure-cache-request') {
72
125
  const { filename } = event.data;
@@ -93,6 +146,108 @@
93
146
  }
94
147
  });
95
148
 
149
+ // Forward fullscreen state changes to all viewer iframes
150
+ function notifyFullscreenChange() {
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
+ }
159
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
160
+ if (f.contentWindow) {
161
+ f.style.overscrollBehavior = 'none';
162
+ f.contentWindow.postMessage({
163
+ type: 'pdf-secure-fullscreen-state',
164
+ isFullscreen: fsActive
165
+ }, window.location.origin);
166
+ }
167
+ });
168
+ }
169
+ document.addEventListener('fullscreenchange', notifyFullscreenChange);
170
+ document.addEventListener('webkitfullscreenchange', notifyFullscreenChange);
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
+
96
251
  async function processQueue() {
97
252
  if (isLoading || loadQueue.length === 0) return;
98
253
 
@@ -124,6 +279,8 @@
124
279
  loadQueue.length = 0;
125
280
  isLoading = false;
126
281
  currentResolver = null;
282
+ // Exit simulated fullscreen on SPA navigation
283
+ exitSimulatedFullscreen();
127
284
  interceptPdfLinks();
128
285
  });
129
286
  } catch (err) {
@@ -278,6 +435,7 @@
278
435
  iframe.src = config.relative_path + '/plugins/pdf-secure/viewer?file=' + encodeURIComponent(filename);
279
436
  iframe.setAttribute('frameborder', '0');
280
437
  iframe.setAttribute('allowfullscreen', 'true');
438
+ iframe.setAttribute('allow', 'fullscreen');
281
439
 
282
440
  // Store resolver for postMessage callback
283
441
  currentResolver = function () {
@@ -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);
@@ -4461,25 +4461,34 @@
4461
4461
  // ERGONOMIC FEATURES
4462
4462
  // ==========================================
4463
4463
 
4464
- // 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
+
4465
4468
  function toggleFullscreen() {
4466
- const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
4467
- if (fsEl) {
4468
- if (document.exitFullscreen) document.exitFullscreen();
4469
- 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' }, '*');
4470
4473
  } else {
4471
- const el = document.documentElement;
4472
- if (el.requestFullscreen) el.requestFullscreen().catch(() => { });
4473
- 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
+ }
4474
4484
  }
4475
4485
  }
4476
4486
 
4477
- // Update fullscreen button icon
4487
+ // Update fullscreen button icon based on state
4478
4488
  function updateFullscreenIcon() {
4479
4489
  const icon = document.getElementById('fullscreenIcon');
4480
4490
  const btn = document.getElementById('fullscreenBtn');
4481
- const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
4482
- if (fsEl) {
4491
+ if (isFullscreen) {
4483
4492
  icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4484
4493
  btn.classList.add('active');
4485
4494
  } else {
@@ -4488,8 +4497,37 @@
4488
4497
  }
4489
4498
  }
4490
4499
 
4491
- document.addEventListener('fullscreenchange', updateFullscreenIcon);
4492
- document.addEventListener('webkitfullscreenchange', updateFullscreenIcon);
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
+
4511
+ // Listen for fullscreen state from parent (iframe mode)
4512
+ window.addEventListener('message', (event) => {
4513
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
4514
+ isFullscreen = event.data.isFullscreen;
4515
+ updateFullscreenIcon();
4516
+ applyFullscreenTouchRestrictions();
4517
+ }
4518
+ });
4519
+
4520
+ // Local fullscreen events (standalone mode)
4521
+ document.addEventListener('fullscreenchange', () => {
4522
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4523
+ updateFullscreenIcon();
4524
+ applyFullscreenTouchRestrictions();
4525
+ });
4526
+ document.addEventListener('webkitfullscreenchange', () => {
4527
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
4528
+ updateFullscreenIcon();
4529
+ applyFullscreenTouchRestrictions();
4530
+ });
4493
4531
 
4494
4532
  // Fullscreen button click
4495
4533
  document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
@@ -4781,11 +4819,21 @@
4781
4819
  const touchY = e.touches[0].clientY;
4782
4820
  const deltaY = touchStartY - touchY; // positive = scrolling down
4783
4821
 
4784
- const atTop = container.scrollTop <= 0;
4785
- 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;
4786
4825
 
4787
- if ((atTop && deltaY < 0) || (atBottom && deltaY > 0)) {
4788
- 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
+ }
4789
4837
  }
4790
4838
  touchStartY = touchY;
4791
4839
  }, { passive: false });
@@ -4802,17 +4850,20 @@
4802
4850
  document.addEventListener('touchmove', (e) => {
4803
4851
  if (e.touches.length !== 1) return;
4804
4852
 
4805
- // Only prevent when in fullscreen
4806
- const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
4807
- if (!fsEl) return;
4853
+ // Only prevent when in fullscreen (isFullscreen works in both iframe and standalone)
4854
+ if (!isFullscreen) return;
4808
4855
 
4809
4856
  const touchY = e.touches[0].clientY;
4810
4857
  const deltaY = docTouchStartY - touchY;
4811
- const atTop = container.scrollTop <= 0;
4812
- const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
4813
4858
 
4814
- // Block pull-down at top and pull-up at bottom in fullscreen
4815
- 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)) {
4816
4867
  e.preventDefault();
4817
4868
  }
4818
4869
  docTouchStartY = touchY;