nodebb-plugin-pdf-secure 1.2.22 → 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 +79 -32
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
|
|
|
@@ -1187,7 +1192,7 @@
|
|
|
1187
1192
|
height: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
|
|
1188
1193
|
background: var(--bg-secondary);
|
|
1189
1194
|
border-top: 1px solid var(--border-color);
|
|
1190
|
-
z-index:
|
|
1195
|
+
z-index: 260; /* Above backdrop (250) so dropdowns inside are clickable */
|
|
1191
1196
|
padding: 0 8px;
|
|
1192
1197
|
padding-bottom: var(--safe-area-bottom);
|
|
1193
1198
|
/* Flex layout: scrollable tools + fixed fullscreen button */
|
|
@@ -1381,13 +1386,15 @@
|
|
|
1381
1386
|
/* ==========================================
|
|
1382
1387
|
FULLSCREEN STATE (simulated fullscreen on mobile)
|
|
1383
1388
|
========================================== */
|
|
1384
|
-
/* Ensure bottom toolbar
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1389
|
+
/* Ensure bottom toolbar + viewerContainer adjust in fullscreen on mobile/tablet only */
|
|
1390
|
+
@media (max-width: 1024px) {
|
|
1391
|
+
body.viewer-fullscreen #bottomToolbar {
|
|
1392
|
+
display: flex;
|
|
1393
|
+
}
|
|
1388
1394
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1395
|
+
body.viewer-fullscreen #viewerContainer {
|
|
1396
|
+
bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
|
|
1397
|
+
}
|
|
1391
1398
|
}
|
|
1392
1399
|
|
|
1393
1400
|
/* ==========================================
|
|
@@ -1685,6 +1692,13 @@
|
|
|
1685
1692
|
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" />
|
|
1686
1693
|
</svg>
|
|
1687
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>
|
|
1688
1702
|
</div>
|
|
1689
1703
|
|
|
1690
1704
|
|
|
@@ -1950,15 +1964,6 @@
|
|
|
1950
1964
|
</svg>
|
|
1951
1965
|
</button>
|
|
1952
1966
|
|
|
1953
|
-
<div class="separator"></div>
|
|
1954
|
-
|
|
1955
|
-
<!-- Fullscreen Toggle -->
|
|
1956
|
-
<button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
|
|
1957
|
-
<svg viewBox="0 0 24 24" id="fullscreenIcon">
|
|
1958
|
-
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
|
1959
|
-
</svg>
|
|
1960
|
-
</button>
|
|
1961
|
-
|
|
1962
1967
|
<div class="separator overflowSep"></div>
|
|
1963
1968
|
|
|
1964
1969
|
<div class="toolbarBtnWithDropdown" id="overflowWrapper">
|
|
@@ -2655,13 +2660,25 @@
|
|
|
2655
2660
|
// Swipe-to-dismiss controller (declared before closeAllDropdowns which references it)
|
|
2656
2661
|
let swipeAbortController = null;
|
|
2657
2662
|
|
|
2663
|
+
// Track dropdown's original parent so we can return it after closing
|
|
2664
|
+
const dropdownOriginalParents = new Map();
|
|
2665
|
+
|
|
2658
2666
|
function closeAllDropdowns() {
|
|
2659
2667
|
// Clean up swipe listeners
|
|
2660
2668
|
if (swipeAbortController) { swipeAbortController.abort(); swipeAbortController = null; }
|
|
2661
|
-
// Reset inline
|
|
2669
|
+
// Reset inline styles and move dropdowns back to original parents
|
|
2662
2670
|
[highlightDropdown, drawDropdown, shapesDropdown, overflowDropdown].forEach(dd => {
|
|
2663
2671
|
dd.style.transform = '';
|
|
2664
2672
|
dd.style.transition = '';
|
|
2673
|
+
dd.style.position = '';
|
|
2674
|
+
dd.style.top = '';
|
|
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);
|
|
2665
2682
|
});
|
|
2666
2683
|
highlightDropdown.classList.remove('visible');
|
|
2667
2684
|
drawDropdown.classList.remove('visible');
|
|
@@ -2682,11 +2699,23 @@
|
|
|
2682
2699
|
handle.className = 'bottomSheetHandle';
|
|
2683
2700
|
dropdown.insertBefore(handle, dropdown.firstChild);
|
|
2684
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);
|
|
2685
2705
|
dropdown.classList.add('visible');
|
|
2686
|
-
// Show backdrop on mobile/tablet portrait
|
|
2687
2706
|
if (useBottomSheet) {
|
|
2707
|
+
// Show backdrop on mobile/tablet portrait
|
|
2688
2708
|
dropdownBackdrop.classList.add('visible');
|
|
2689
2709
|
setupBottomSheetSwipe(dropdown);
|
|
2710
|
+
} else {
|
|
2711
|
+
// Desktop/tablet landscape: position below the button
|
|
2712
|
+
const wrapper = e.target.closest('.toolbarBtnWithDropdown');
|
|
2713
|
+
if (wrapper) {
|
|
2714
|
+
const rect = wrapper.getBoundingClientRect();
|
|
2715
|
+
dropdown.style.position = 'fixed';
|
|
2716
|
+
dropdown.style.top = (rect.bottom + 4) + 'px';
|
|
2717
|
+
dropdown.style.left = rect.left + 'px';
|
|
2718
|
+
}
|
|
2690
2719
|
}
|
|
2691
2720
|
}
|
|
2692
2721
|
}
|
|
@@ -2747,27 +2776,45 @@
|
|
|
2747
2776
|
|
|
2748
2777
|
// Overflow menu toggle
|
|
2749
2778
|
document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
|
|
2750
|
-
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
|
+
}
|
|
2751
2799
|
|
|
2752
|
-
|
|
2753
|
-
document.getElementById('overflowRotateLeft').onclick = () => {
|
|
2800
|
+
attachOverflowAction('overflowRotateLeft', () => {
|
|
2754
2801
|
rotatePage(-90);
|
|
2755
2802
|
closeAllDropdowns();
|
|
2756
|
-
};
|
|
2757
|
-
|
|
2803
|
+
});
|
|
2804
|
+
attachOverflowAction('overflowRotateRight', () => {
|
|
2758
2805
|
rotatePage(90);
|
|
2759
2806
|
closeAllDropdowns();
|
|
2760
|
-
};
|
|
2761
|
-
|
|
2807
|
+
});
|
|
2808
|
+
attachOverflowAction('overflowSepia', () => {
|
|
2762
2809
|
document.getElementById('sepiaBtn').click();
|
|
2763
2810
|
document.getElementById('overflowSepia').classList.toggle('active',
|
|
2764
2811
|
document.getElementById('sepiaBtn').classList.contains('active'));
|
|
2765
2812
|
closeAllDropdowns();
|
|
2766
|
-
};
|
|
2767
|
-
|
|
2813
|
+
});
|
|
2814
|
+
attachOverflowAction('overflowFullscreen', () => {
|
|
2768
2815
|
toggleFullscreen();
|
|
2769
2816
|
closeAllDropdowns();
|
|
2770
|
-
};
|
|
2817
|
+
});
|
|
2771
2818
|
|
|
2772
2819
|
// Close dropdowns when clicking outside
|
|
2773
2820
|
document.addEventListener('click', (e) => {
|