nodebb-plugin-pdf-secure 1.2.23 → 1.2.25
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/library.js +11 -6
- package/package.json +1 -1
- package/static/templates/admin/plugins/pdf-secure.tpl +7 -1
- package/static/viewer-app.js +100 -30
- package/static/viewer.css +8 -4
- package/static/viewer.html +58 -25
package/library.js
CHANGED
|
@@ -78,20 +78,24 @@ plugin.init = async (params) => {
|
|
|
78
78
|
return res.status(500).send('Viewer not available');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// Check if user is Premium (admins/global mods always premium)
|
|
81
|
+
// Check if user is Premium or Lite (admins/global mods always premium)
|
|
82
82
|
let isPremium = false;
|
|
83
|
+
let isLite = false;
|
|
83
84
|
if (req.uid) {
|
|
84
|
-
const [isAdmin, isGlobalMod, isPremiumMember] = await Promise.all([
|
|
85
|
+
const [isAdmin, isGlobalMod, isPremiumMember, isLiteMember] = await Promise.all([
|
|
85
86
|
groups.isMember(req.uid, 'administrators'),
|
|
86
87
|
groups.isMember(req.uid, 'Global Moderators'),
|
|
87
88
|
groups.isMember(req.uid, 'Premium'),
|
|
89
|
+
groups.isMember(req.uid, 'Lite'),
|
|
88
90
|
]);
|
|
89
91
|
isPremium = isAdmin || isGlobalMod || isPremiumMember;
|
|
92
|
+
// Lite: full PDF access but restricted UI (no annotations, sidebar, etc.)
|
|
93
|
+
isLite = !isPremium && isLiteMember;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const nonceData = nonceStore.generate(req.uid || 0, safeName,
|
|
96
|
+
// Lite users get full PDF like premium (for nonce/server-side PDF data)
|
|
97
|
+
const hasFullAccess = isPremium || isLite;
|
|
98
|
+
const nonceData = nonceStore.generate(req.uid || 0, safeName, hasFullAccess);
|
|
95
99
|
|
|
96
100
|
// Serve the viewer template with comprehensive security headers
|
|
97
101
|
res.set({
|
|
@@ -121,7 +125,8 @@ plugin.init = async (params) => {
|
|
|
121
125
|
csrfToken: ${JSON.stringify(req.csrfToken ? req.csrfToken() : '')},
|
|
122
126
|
nonce: ${JSON.stringify(nonceData.nonce)},
|
|
123
127
|
dk: ${JSON.stringify(nonceData.xorKey)},
|
|
124
|
-
isPremium: ${JSON.stringify(isPremium)}
|
|
128
|
+
isPremium: ${JSON.stringify(isPremium)},
|
|
129
|
+
isLite: ${JSON.stringify(isLite)}
|
|
125
130
|
};
|
|
126
131
|
</script>
|
|
127
132
|
</head>`);
|
package/package.json
CHANGED
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
<div class="mb-3">
|
|
15
15
|
<label class="form-label" for="premiumGroup">Premium Group Name</label>
|
|
16
16
|
<input type="text" id="premiumGroup" name="premiumGroup" title="Premium Group Name" class="form-control" placeholder="Premium" value="Premium">
|
|
17
|
-
<div class="form-text">Users in this group can view full PDFs. Others can only see the first page.</div>
|
|
17
|
+
<div class="form-text">Users in this group can view full PDFs with all tools. Others can only see the first page.</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="mb-3">
|
|
21
|
+
<label class="form-label" for="liteGroup">Lite Group Name</label>
|
|
22
|
+
<input type="text" id="liteGroup" name="liteGroup" title="Lite Group Name" class="form-control" placeholder="Lite" value="Lite">
|
|
23
|
+
<div class="form-text">Users in this group can view full PDFs but only with zoom and fullscreen. No annotations, sidebar, or other tools.</div>
|
|
18
24
|
</div>
|
|
19
25
|
|
|
20
26
|
<div class="form-check form-switch mb-3">
|
package/static/viewer-app.js
CHANGED
|
@@ -200,6 +200,50 @@
|
|
|
200
200
|
viewerEl.appendChild(overlay);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// ============================================
|
|
204
|
+
// LITE MODE: Hide all tools except fullscreen and zoom
|
|
205
|
+
// Lite users can view full PDF but cannot use annotations,
|
|
206
|
+
// sidebar, rotate, sepia, overflow menu, etc.
|
|
207
|
+
// ============================================
|
|
208
|
+
function applyLiteMode() {
|
|
209
|
+
// Hide sidebar button (İçindekiler)
|
|
210
|
+
const sidebarBtn = document.getElementById('sidebarBtn');
|
|
211
|
+
if (sidebarBtn) sidebarBtn.style.display = 'none';
|
|
212
|
+
|
|
213
|
+
// Close sidebar if open
|
|
214
|
+
const sidebarEl = document.getElementById('sidebar');
|
|
215
|
+
if (sidebarEl) sidebarEl.classList.remove('open');
|
|
216
|
+
|
|
217
|
+
// Hide entire annotation tools group (highlight, draw, eraser, select, undo/redo, text, shapes)
|
|
218
|
+
const annotationGroup = document.querySelector('#toolbar > .toolbarGroup:nth-child(3)');
|
|
219
|
+
if (annotationGroup) annotationGroup.style.display = 'none';
|
|
220
|
+
|
|
221
|
+
// In the zoom/utility group, hide everything except zoomIn and zoomOut
|
|
222
|
+
const keepIds = new Set(['zoomIn', 'zoomOut']);
|
|
223
|
+
const utilityGroup = document.querySelector('#toolbar > .toolbarGroup:nth-child(5)');
|
|
224
|
+
if (utilityGroup) {
|
|
225
|
+
Array.from(utilityGroup.children).forEach(function (child) {
|
|
226
|
+
if (!keepIds.has(child.id)) {
|
|
227
|
+
child.style.display = 'none';
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Hide bottom toolbar annotation tools (mobile), keep only fullscreen button
|
|
233
|
+
const bottomToolbarInner = document.getElementById('bottomToolbarInner');
|
|
234
|
+
if (bottomToolbarInner) bottomToolbarInner.style.display = 'none';
|
|
235
|
+
|
|
236
|
+
// Hide all top-level separators between groups
|
|
237
|
+
document.querySelectorAll('#toolbar > .separator').forEach(function (sep) {
|
|
238
|
+
sep.style.display = 'none';
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Hide page info (Lite users don't need page input)
|
|
242
|
+
// Actually keep page info for navigation awareness
|
|
243
|
+
|
|
244
|
+
console.log('[PDF-Secure] Lite mode applied - restricted toolbar');
|
|
245
|
+
}
|
|
246
|
+
|
|
203
247
|
// ============================================
|
|
204
248
|
// PREMIUM INTEGRITY: Periodic Check (2s interval)
|
|
205
249
|
// Hides pages 2+, recreates overlay if removed, forces page 1
|
|
@@ -342,8 +386,8 @@
|
|
|
342
386
|
pdfBuffer = encodedBuffer;
|
|
343
387
|
}
|
|
344
388
|
|
|
345
|
-
// Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
|
|
346
|
-
if (_cfg.isPremium !== false && window.parent && window.parent !== window) {
|
|
389
|
+
// Send buffer to parent for caching (premium/lite only - non-premium must not leak decoded buffer)
|
|
390
|
+
if ((_cfg.isPremium !== false || _cfg.isLite) && window.parent && window.parent !== window) {
|
|
347
391
|
// Clone buffer for parent (we keep original)
|
|
348
392
|
const bufferCopy = pdfBuffer.slice(0);
|
|
349
393
|
window.parent.postMessage({
|
|
@@ -360,7 +404,7 @@
|
|
|
360
404
|
await loadPDFFromBuffer(pdfBuffer);
|
|
361
405
|
|
|
362
406
|
// Premium Gate: Client-side page restriction for non-premium users
|
|
363
|
-
if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
|
|
407
|
+
if (config.isPremium === false && !config.isLite && pdfDoc && pdfDoc.numPages > 1) {
|
|
364
408
|
premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
|
|
365
409
|
showPremiumLockOverlay(pdfDoc.numPages);
|
|
366
410
|
startPeriodicCheck();
|
|
@@ -369,6 +413,11 @@
|
|
|
369
413
|
premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
|
|
370
414
|
}
|
|
371
415
|
|
|
416
|
+
// Lite Mode: Hide all tools except fullscreen and zoom
|
|
417
|
+
if (config.isLite) {
|
|
418
|
+
applyLiteMode();
|
|
419
|
+
}
|
|
420
|
+
|
|
372
421
|
// Step 5: Moved to pagerendered event for proper timing
|
|
373
422
|
|
|
374
423
|
// Step 6: Security - clear references to prevent extraction
|
|
@@ -653,27 +702,43 @@
|
|
|
653
702
|
|
|
654
703
|
// Overflow menu toggle
|
|
655
704
|
document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
|
|
656
|
-
overflowDropdown.
|
|
705
|
+
overflowDropdown.addEventListener('click', (e) => e.stopPropagation());
|
|
706
|
+
overflowDropdown.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
707
|
+
|
|
708
|
+
// Overflow menu actions — use both click + pointerup for reliable tablet/touch support
|
|
709
|
+
function attachOverflowAction(id, action) {
|
|
710
|
+
const el = document.getElementById(id);
|
|
711
|
+
let fired = false;
|
|
712
|
+
function run() {
|
|
713
|
+
if (fired) return;
|
|
714
|
+
fired = true;
|
|
715
|
+
requestAnimationFrame(() => { fired = false; });
|
|
716
|
+
action();
|
|
717
|
+
}
|
|
718
|
+
el.addEventListener('click', run);
|
|
719
|
+
el.addEventListener('pointerup', (e) => {
|
|
720
|
+
if (e.pointerType === 'touch' || e.pointerType === 'pen') run();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
657
723
|
|
|
658
|
-
|
|
659
|
-
document.getElementById('overflowRotateLeft').onclick = () => {
|
|
724
|
+
attachOverflowAction('overflowRotateLeft', () => {
|
|
660
725
|
rotatePage(-90);
|
|
661
726
|
closeAllDropdowns();
|
|
662
|
-
};
|
|
663
|
-
|
|
727
|
+
});
|
|
728
|
+
attachOverflowAction('overflowRotateRight', () => {
|
|
664
729
|
rotatePage(90);
|
|
665
730
|
closeAllDropdowns();
|
|
666
|
-
};
|
|
667
|
-
|
|
731
|
+
});
|
|
732
|
+
attachOverflowAction('overflowSepia', () => {
|
|
668
733
|
document.getElementById('sepiaBtn').click();
|
|
669
734
|
document.getElementById('overflowSepia').classList.toggle('active',
|
|
670
735
|
document.getElementById('sepiaBtn').classList.contains('active'));
|
|
671
736
|
closeAllDropdowns();
|
|
672
|
-
};
|
|
673
|
-
|
|
737
|
+
});
|
|
738
|
+
attachOverflowAction('overflowFullscreen', () => {
|
|
674
739
|
toggleFullscreen();
|
|
675
740
|
closeAllDropdowns();
|
|
676
|
-
};
|
|
741
|
+
});
|
|
677
742
|
|
|
678
743
|
// Close dropdowns when clicking outside
|
|
679
744
|
document.addEventListener('click', (e) => {
|
|
@@ -2393,46 +2458,51 @@
|
|
|
2393
2458
|
if (e.target.tagName === 'INPUT' || e.target.contentEditable === 'true') return;
|
|
2394
2459
|
|
|
2395
2460
|
const key = e.key.toLowerCase();
|
|
2461
|
+
const isLiteMode = _cfg && _cfg.isLite;
|
|
2396
2462
|
|
|
2397
|
-
//
|
|
2398
|
-
if (key === 'h') { setTool('highlight'); e.preventDefault(); }
|
|
2399
|
-
if (key === 'p') { setTool('pen'); e.preventDefault(); }
|
|
2400
|
-
if (key === 'e') { setTool('eraser'); e.preventDefault(); }
|
|
2401
|
-
if (key === 't') { setTool('text'); e.preventDefault(); }
|
|
2402
|
-
if (key === 'r') { setTool('shape'); e.preventDefault(); }
|
|
2403
|
-
if (key === 'v') { setTool('select'); e.preventDefault(); }
|
|
2463
|
+
// Fullscreen shortcut (always allowed)
|
|
2404
2464
|
if (key === 'f') { toggleFullscreen(); e.preventDefault(); }
|
|
2405
2465
|
|
|
2406
|
-
//
|
|
2407
|
-
if (
|
|
2466
|
+
// Tool shortcuts (blocked in Lite mode)
|
|
2467
|
+
if (!isLiteMode) {
|
|
2468
|
+
if (key === 'h') { setTool('highlight'); e.preventDefault(); }
|
|
2469
|
+
if (key === 'p') { setTool('pen'); e.preventDefault(); }
|
|
2470
|
+
if (key === 'e') { setTool('eraser'); e.preventDefault(); }
|
|
2471
|
+
if (key === 't') { setTool('text'); e.preventDefault(); }
|
|
2472
|
+
if (key === 'r') { setTool('shape'); e.preventDefault(); }
|
|
2473
|
+
if (key === 'v') { setTool('select'); e.preventDefault(); }
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// Delete selected annotation(s) (blocked in Lite mode)
|
|
2477
|
+
if (!isLiteMode && (key === 'delete' || key === 'backspace') && (selectedAnnotation || multiSelectedAnnotations.length > 0)) {
|
|
2408
2478
|
deleteSelectedAnnotation();
|
|
2409
2479
|
e.preventDefault();
|
|
2410
2480
|
}
|
|
2411
2481
|
|
|
2412
|
-
// Undo/Redo
|
|
2413
|
-
if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {
|
|
2482
|
+
// Undo/Redo (blocked in Lite mode)
|
|
2483
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {
|
|
2414
2484
|
performUndo();
|
|
2415
2485
|
e.preventDefault();
|
|
2416
2486
|
return;
|
|
2417
2487
|
}
|
|
2418
|
-
if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {
|
|
2488
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {
|
|
2419
2489
|
performRedo();
|
|
2420
2490
|
e.preventDefault();
|
|
2421
2491
|
return;
|
|
2422
2492
|
}
|
|
2423
2493
|
|
|
2424
|
-
// Copy/Paste annotations
|
|
2425
|
-
if ((e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
|
|
2494
|
+
// Copy/Paste annotations (blocked in Lite mode)
|
|
2495
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
|
|
2426
2496
|
copySelectedAnnotation();
|
|
2427
2497
|
e.preventDefault();
|
|
2428
2498
|
}
|
|
2429
|
-
if ((e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
|
|
2499
|
+
if (!isLiteMode && (e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
|
|
2430
2500
|
pasteAnnotation();
|
|
2431
2501
|
e.preventDefault();
|
|
2432
2502
|
}
|
|
2433
2503
|
|
|
2434
|
-
//
|
|
2435
|
-
if (key === 's') {
|
|
2504
|
+
// Sidebar toggle (blocked in Lite mode)
|
|
2505
|
+
if (!isLiteMode && key === 's') {
|
|
2436
2506
|
document.getElementById('sidebarBtn').click();
|
|
2437
2507
|
e.preventDefault();
|
|
2438
2508
|
}
|
package/static/viewer.css
CHANGED
|
@@ -115,6 +115,7 @@ body {
|
|
|
115
115
|
align-items: center;
|
|
116
116
|
justify-content: center;
|
|
117
117
|
transition: background 0.1s;
|
|
118
|
+
touch-action: manipulation;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
.toolbarBtn:hover {
|
|
@@ -426,8 +427,12 @@ body {
|
|
|
426
427
|
border-radius: 6px;
|
|
427
428
|
font-size: 14px;
|
|
428
429
|
white-space: nowrap;
|
|
430
|
+
touch-action: manipulation;
|
|
431
|
+
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
|
|
432
|
+
user-select: none;
|
|
433
|
+
-webkit-user-select: none;
|
|
429
434
|
}
|
|
430
|
-
.overflowItem:hover { background: var(--bg-tertiary); }
|
|
435
|
+
.overflowItem:hover, .overflowItem:active { background: var(--bg-tertiary); }
|
|
431
436
|
.overflowItem svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; }
|
|
432
437
|
.overflowItem.active { color: var(--accent); }
|
|
433
438
|
.overflowDivider { height: 1px; background: var(--border-color); margin: 6px 0; }
|
|
@@ -436,13 +441,12 @@ body {
|
|
|
436
441
|
#overflowWrapper { display: flex; }
|
|
437
442
|
.overflowSep { display: block; }
|
|
438
443
|
|
|
439
|
-
/* Hide rotate, sepia and their separators (children 3-
|
|
444
|
+
/* Hide rotate, sepia and their separators (children 3-7 of view group) */
|
|
440
445
|
.toolbarGroup:nth-child(5) > :nth-child(3),
|
|
441
446
|
.toolbarGroup:nth-child(5) > :nth-child(4),
|
|
442
447
|
.toolbarGroup:nth-child(5) > :nth-child(5),
|
|
443
448
|
.toolbarGroup:nth-child(5) > :nth-child(6),
|
|
444
|
-
.toolbarGroup:nth-child(5) > :nth-child(7)
|
|
445
|
-
.toolbarGroup:nth-child(5) > :nth-child(8) { display: none !important; }
|
|
449
|
+
.toolbarGroup:nth-child(5) > :nth-child(7) { display: none !important; }
|
|
446
450
|
|
|
447
451
|
/* Shape Grid */
|
|
448
452
|
.shapeGrid {
|
package/static/viewer.html
CHANGED
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
align-items: center;
|
|
133
133
|
justify-content: center;
|
|
134
134
|
transition: background 0.1s;
|
|
135
|
+
touch-action: manipulation;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
.toolbarBtn:hover {
|
|
@@ -455,9 +456,14 @@
|
|
|
455
456
|
border-radius: 6px;
|
|
456
457
|
font-size: 14px;
|
|
457
458
|
white-space: nowrap;
|
|
459
|
+
touch-action: manipulation;
|
|
460
|
+
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.1);
|
|
461
|
+
user-select: none;
|
|
462
|
+
-webkit-user-select: none;
|
|
458
463
|
}
|
|
459
464
|
|
|
460
|
-
.overflowItem:hover
|
|
465
|
+
.overflowItem:hover,
|
|
466
|
+
.overflowItem:active {
|
|
461
467
|
background: var(--bg-tertiary);
|
|
462
468
|
}
|
|
463
469
|
|
|
@@ -487,13 +493,12 @@
|
|
|
487
493
|
display: block;
|
|
488
494
|
}
|
|
489
495
|
|
|
490
|
-
/* Hide rotate, sepia and their separators (children 3-
|
|
496
|
+
/* Hide rotate, sepia and their separators (children 3-7 of view group) */
|
|
491
497
|
.toolbarGroup:nth-child(5)> :nth-child(3),
|
|
492
498
|
.toolbarGroup:nth-child(5)> :nth-child(4),
|
|
493
499
|
.toolbarGroup:nth-child(5)> :nth-child(5),
|
|
494
500
|
.toolbarGroup:nth-child(5)> :nth-child(6),
|
|
495
|
-
.toolbarGroup:nth-child(5)> :nth-child(7)
|
|
496
|
-
.toolbarGroup:nth-child(5)> :nth-child(8) {
|
|
501
|
+
.toolbarGroup:nth-child(5)> :nth-child(7) {
|
|
497
502
|
display: none !important;
|
|
498
503
|
}
|
|
499
504
|
|
|
@@ -1687,6 +1692,13 @@
|
|
|
1687
1692
|
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" />
|
|
1688
1693
|
</svg>
|
|
1689
1694
|
</button>
|
|
1695
|
+
|
|
1696
|
+
<!-- Fullscreen Toggle -->
|
|
1697
|
+
<button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
|
|
1698
|
+
<svg viewBox="0 0 24 24" id="fullscreenIcon">
|
|
1699
|
+
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
|
1700
|
+
</svg>
|
|
1701
|
+
</button>
|
|
1690
1702
|
</div>
|
|
1691
1703
|
|
|
1692
1704
|
|
|
@@ -1952,15 +1964,6 @@
|
|
|
1952
1964
|
</svg>
|
|
1953
1965
|
</button>
|
|
1954
1966
|
|
|
1955
|
-
<div class="separator"></div>
|
|
1956
|
-
|
|
1957
|
-
<!-- Fullscreen Toggle -->
|
|
1958
|
-
<button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
|
|
1959
|
-
<svg viewBox="0 0 24 24" id="fullscreenIcon">
|
|
1960
|
-
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
|
1961
|
-
</svg>
|
|
1962
|
-
</button>
|
|
1963
|
-
|
|
1964
1967
|
<div class="separator overflowSep"></div>
|
|
1965
1968
|
|
|
1966
1969
|
<div class="toolbarBtnWithDropdown" id="overflowWrapper">
|
|
@@ -2657,16 +2660,25 @@
|
|
|
2657
2660
|
// Swipe-to-dismiss controller (declared before closeAllDropdowns which references it)
|
|
2658
2661
|
let swipeAbortController = null;
|
|
2659
2662
|
|
|
2663
|
+
// Track dropdown's original parent so we can return it after closing
|
|
2664
|
+
const dropdownOriginalParents = new Map();
|
|
2665
|
+
|
|
2660
2666
|
function closeAllDropdowns() {
|
|
2661
2667
|
// Clean up swipe listeners
|
|
2662
2668
|
if (swipeAbortController) { swipeAbortController.abort(); swipeAbortController = null; }
|
|
2663
|
-
// Reset inline
|
|
2669
|
+
// Reset inline styles and move dropdowns back to original parents
|
|
2664
2670
|
[highlightDropdown, drawDropdown, shapesDropdown, overflowDropdown].forEach(dd => {
|
|
2665
2671
|
dd.style.transform = '';
|
|
2666
2672
|
dd.style.transition = '';
|
|
2667
2673
|
dd.style.position = '';
|
|
2668
2674
|
dd.style.top = '';
|
|
2669
2675
|
dd.style.left = '';
|
|
2676
|
+
// Return dropdown to its original parent if it was moved to body
|
|
2677
|
+
const origParent = dropdownOriginalParents.get(dd);
|
|
2678
|
+
if (origParent && dd.parentNode === document.body) {
|
|
2679
|
+
origParent.appendChild(dd);
|
|
2680
|
+
}
|
|
2681
|
+
dropdownOriginalParents.delete(dd);
|
|
2670
2682
|
});
|
|
2671
2683
|
highlightDropdown.classList.remove('visible');
|
|
2672
2684
|
drawDropdown.classList.remove('visible');
|
|
@@ -2687,13 +2699,16 @@
|
|
|
2687
2699
|
handle.className = 'bottomSheetHandle';
|
|
2688
2700
|
dropdown.insertBefore(handle, dropdown.firstChild);
|
|
2689
2701
|
}
|
|
2702
|
+
// Move dropdown to <body> to escape any parent stacking context / overflow clipping
|
|
2703
|
+
dropdownOriginalParents.set(dropdown, dropdown.parentNode);
|
|
2704
|
+
document.body.appendChild(dropdown);
|
|
2690
2705
|
dropdown.classList.add('visible');
|
|
2691
2706
|
if (useBottomSheet) {
|
|
2692
2707
|
// Show backdrop on mobile/tablet portrait
|
|
2693
2708
|
dropdownBackdrop.classList.add('visible');
|
|
2694
2709
|
setupBottomSheetSwipe(dropdown);
|
|
2695
2710
|
} else {
|
|
2696
|
-
// Desktop/tablet landscape:
|
|
2711
|
+
// Desktop/tablet landscape: position below the button
|
|
2697
2712
|
const wrapper = e.target.closest('.toolbarBtnWithDropdown');
|
|
2698
2713
|
if (wrapper) {
|
|
2699
2714
|
const rect = wrapper.getBoundingClientRect();
|
|
@@ -2761,27 +2776,45 @@
|
|
|
2761
2776
|
|
|
2762
2777
|
// Overflow menu toggle
|
|
2763
2778
|
document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
|
|
2764
|
-
overflowDropdown.
|
|
2779
|
+
overflowDropdown.addEventListener('click', (e) => e.stopPropagation());
|
|
2780
|
+
overflowDropdown.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
2781
|
+
|
|
2782
|
+
// Overflow menu actions — use both click + pointerup for reliable tablet/touch support
|
|
2783
|
+
// pointerup fires immediately on touch release (no 300ms delay).
|
|
2784
|
+
// A guard flag prevents double-firing when both events reach the handler.
|
|
2785
|
+
function attachOverflowAction(id, action) {
|
|
2786
|
+
const el = document.getElementById(id);
|
|
2787
|
+
let fired = false;
|
|
2788
|
+
function run() {
|
|
2789
|
+
if (fired) return;
|
|
2790
|
+
fired = true;
|
|
2791
|
+
requestAnimationFrame(() => { fired = false; });
|
|
2792
|
+
action();
|
|
2793
|
+
}
|
|
2794
|
+
el.addEventListener('click', run);
|
|
2795
|
+
el.addEventListener('pointerup', (e) => {
|
|
2796
|
+
if (e.pointerType === 'touch' || e.pointerType === 'pen') run();
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2765
2799
|
|
|
2766
|
-
|
|
2767
|
-
document.getElementById('overflowRotateLeft').onclick = () => {
|
|
2800
|
+
attachOverflowAction('overflowRotateLeft', () => {
|
|
2768
2801
|
rotatePage(-90);
|
|
2769
2802
|
closeAllDropdowns();
|
|
2770
|
-
};
|
|
2771
|
-
|
|
2803
|
+
});
|
|
2804
|
+
attachOverflowAction('overflowRotateRight', () => {
|
|
2772
2805
|
rotatePage(90);
|
|
2773
2806
|
closeAllDropdowns();
|
|
2774
|
-
};
|
|
2775
|
-
|
|
2807
|
+
});
|
|
2808
|
+
attachOverflowAction('overflowSepia', () => {
|
|
2776
2809
|
document.getElementById('sepiaBtn').click();
|
|
2777
2810
|
document.getElementById('overflowSepia').classList.toggle('active',
|
|
2778
2811
|
document.getElementById('sepiaBtn').classList.contains('active'));
|
|
2779
2812
|
closeAllDropdowns();
|
|
2780
|
-
};
|
|
2781
|
-
|
|
2813
|
+
});
|
|
2814
|
+
attachOverflowAction('overflowFullscreen', () => {
|
|
2782
2815
|
toggleFullscreen();
|
|
2783
2816
|
closeAllDropdowns();
|
|
2784
|
-
};
|
|
2817
|
+
});
|
|
2785
2818
|
|
|
2786
2819
|
// Close dropdowns when clicking outside
|
|
2787
2820
|
document.addEventListener('click', (e) => {
|