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 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
- // Always send full PDF (client-side premium gating)
93
- // isPremium flag is sent to client for UI control only
94
- const nonceData = nonceStore.generate(req.uid || 0, safeName, true);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure",
3
- "version": "1.2.22",
3
+ "version": "1.2.25",
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": {
@@ -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">
@@ -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.onclick = (e) => e.stopPropagation();
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
- // Overflow menu actions
659
- document.getElementById('overflowRotateLeft').onclick = () => {
724
+ attachOverflowAction('overflowRotateLeft', () => {
660
725
  rotatePage(-90);
661
726
  closeAllDropdowns();
662
- };
663
- document.getElementById('overflowRotateRight').onclick = () => {
727
+ });
728
+ attachOverflowAction('overflowRotateRight', () => {
664
729
  rotatePage(90);
665
730
  closeAllDropdowns();
666
- };
667
- document.getElementById('overflowSepia').onclick = () => {
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
- document.getElementById('overflowFullscreen').onclick = () => {
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
- // Tool shortcuts
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
- // Delete selected annotation(s)
2407
- if ((key === 'delete' || key === 'backspace') && (selectedAnnotation || multiSelectedAnnotations.length > 0)) {
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
- // Navigation
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-8 of view group) */
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 {
@@ -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-8 of view group) */
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: 100;
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 is visible and viewer container makes room for it */
1385
- body.viewer-fullscreen #bottomToolbar {
1386
- display: flex;
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
- body.viewer-fullscreen #viewerContainer {
1390
- bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
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 transform from swipe gesture
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.onclick = (e) => e.stopPropagation();
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
- // Overflow menu actions
2753
- document.getElementById('overflowRotateLeft').onclick = () => {
2800
+ attachOverflowAction('overflowRotateLeft', () => {
2754
2801
  rotatePage(-90);
2755
2802
  closeAllDropdowns();
2756
- };
2757
- document.getElementById('overflowRotateRight').onclick = () => {
2803
+ });
2804
+ attachOverflowAction('overflowRotateRight', () => {
2758
2805
  rotatePage(90);
2759
2806
  closeAllDropdowns();
2760
- };
2761
- document.getElementById('overflowSepia').onclick = () => {
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
- document.getElementById('overflowFullscreen').onclick = () => {
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) => {