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 +1 -1
- package/static/lib/main.js +46 -0
- package/static/viewer.html +226 -29
package/package.json
CHANGED
package/static/lib/main.js
CHANGED
|
@@ -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 () {
|
package/static/viewer.html
CHANGED
|
@@ -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
|
|
56
|
-
overscroll-behavior:
|
|
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
|
|
630
|
-
overscroll-behavior:
|
|
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:
|
|
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
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
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
|
|
2708
|
-
|
|
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
|
-
|
|
2711
|
-
svg.addEventListener('
|
|
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
|
|
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
|
|
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
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
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
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4418
|
-
|
|
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
|
|
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
|
// ==========================================
|