nodebb-plugin-pdf-secure 1.2.17 → 1.2.19

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.17",
3
+ "version": "1.2.19",
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": {
@@ -194,7 +194,9 @@
194
194
  iframe.style.top = '0';
195
195
  iframe.style.left = '0';
196
196
  iframe.style.width = '100vw';
197
+ iframe.style.width = '100dvw'; // Override: dynamic viewport (excludes browser chrome)
197
198
  iframe.style.height = '100vh';
199
+ iframe.style.height = '100dvh'; // Override: dynamic viewport (excludes address bar on mobile)
198
200
  iframe.style.zIndex = '2147483647';
199
201
 
200
202
  // Lock body scroll
@@ -525,6 +525,10 @@
525
525
  document.getElementById('sepiaBtn').classList.contains('active'));
526
526
  closeAllDropdowns();
527
527
  };
528
+ document.getElementById('overflowFullscreen').onclick = () => {
529
+ toggleFullscreen();
530
+ closeAllDropdowns();
531
+ };
528
532
 
529
533
  // Close dropdowns when clicking outside
530
534
  document.addEventListener('click', (e) => {
@@ -613,15 +617,18 @@
613
617
  highlightColor = c;
614
618
  if (currentTool === 'highlight') currentColor = c;
615
619
  document.getElementById('highlightWave').setAttribute('stroke', c);
620
+ document.getElementById('highlightColorIndicator').style.background = c;
616
621
  });
617
622
  setupColorPicker('drawColors', c => {
618
623
  drawColor = c;
619
624
  if (currentTool === 'pen') currentColor = c;
620
625
  document.getElementById('drawWave').setAttribute('stroke', c);
626
+ document.getElementById('drawColorIndicator').style.background = c;
621
627
  });
622
628
  setupColorPicker('shapeColors', c => {
623
629
  shapeColor = c;
624
630
  if (currentTool === 'shape') currentColor = c;
631
+ document.getElementById('shapeColorIndicator').style.background = c;
625
632
  });
626
633
 
627
634
  // Highlighter Thickness Slider
@@ -745,13 +752,9 @@
745
752
  e.preventDefault();
746
753
  draw(e);
747
754
  };
748
- if (annotationMode) {
749
- svg.addEventListener('touchstart', touchStartHandler, { passive: false, signal });
750
- svg.addEventListener('touchmove', touchMoveHandler, { passive: false, signal });
751
- } else {
752
- svg.addEventListener('touchstart', touchStartHandler, { signal });
753
- svg.addEventListener('touchmove', touchMoveHandler, { signal });
754
- }
755
+ // Always use passive:false so preventDefault() works when a tool is later activated
756
+ svg.addEventListener('touchstart', touchStartHandler, { passive: false, signal });
757
+ svg.addEventListener('touchmove', touchMoveHandler, { passive: false, signal });
755
758
  svg.addEventListener('touchend', () => stopDraw(pageNum), { signal });
756
759
  svg.addEventListener('touchcancel', () => stopDraw(pageNum), { signal });
757
760
 
@@ -965,7 +968,8 @@
965
968
  // Text tool - create/edit/drag text
966
969
  if (currentTool === 'text') {
967
970
  // Check if clicked on existing text element
968
- const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
971
+ // Use coords (touch-safe) instead of e.clientX which is undefined on TouchEvent
972
+ const elementsUnderClick = document.elementsFromPoint(coords.clientX, coords.clientY);
969
973
  const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
970
974
 
971
975
  if (existingText) {
@@ -973,7 +977,7 @@
973
977
  startTextDrag(e, existingText, svg, scaleX, pageNum);
974
978
  } else {
975
979
  // Create new text
976
- showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
980
+ showTextEditor(coords.clientX, coords.clientY, svg, x, y, scaleX, pageNum);
977
981
  }
978
982
  return;
979
983
  }
@@ -1235,14 +1239,18 @@
1235
1239
  textEl.classList.add('dragging');
1236
1240
  hasDragged = false;
1237
1241
 
1238
- dragStartX = e.clientX;
1239
- dragStartY = e.clientY;
1242
+ // Touch-safe coordinate extraction
1243
+ const startCoords = getEventCoords(e);
1244
+ dragStartX = startCoords.clientX;
1245
+ dragStartY = startCoords.clientY;
1240
1246
  textOriginalX = parseFloat(textEl.getAttribute('x'));
1241
1247
  textOriginalY = parseFloat(textEl.getAttribute('y'));
1242
1248
 
1243
- function onMouseMove(ev) {
1244
- const dxScreen = ev.clientX - dragStartX;
1245
- const dyScreen = ev.clientY - dragStartY;
1249
+ function onMove(ev) {
1250
+ ev.preventDefault();
1251
+ const moveCoords = getEventCoords(ev);
1252
+ const dxScreen = moveCoords.clientX - dragStartX;
1253
+ const dyScreen = moveCoords.clientY - dragStartY;
1246
1254
  // Convert screen delta to viewBox delta (rotation-aware)
1247
1255
  const vbDelta = screenDeltaToViewBox(svg, dxScreen, dyScreen, textEl);
1248
1256
 
@@ -1254,29 +1262,31 @@
1254
1262
  textEl.setAttribute('y', (textOriginalY + vbDelta.dy).toFixed(2));
1255
1263
  }
1256
1264
 
1257
- function onMouseUp(ev) {
1258
- document.removeEventListener('mousemove', onMouseMove);
1259
- document.removeEventListener('mouseup', onMouseUp);
1265
+ function onEnd(ev) {
1266
+ document.removeEventListener('mousemove', onMove);
1267
+ document.removeEventListener('mouseup', onEnd);
1268
+ document.removeEventListener('touchmove', onMove);
1269
+ document.removeEventListener('touchend', onEnd);
1260
1270
  textEl.classList.remove('dragging');
1261
1271
 
1262
1272
  if (hasDragged) {
1263
1273
  // Moved - save position
1264
1274
  saveAnnotations(pageNum);
1265
1275
  } else {
1266
- // Not moved - short click = edit
1267
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
1268
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
1276
+ // Not moved - short click/tap = edit
1269
1277
  const svgX = parseFloat(textEl.getAttribute('x'));
1270
1278
  const svgY = parseFloat(textEl.getAttribute('y'));
1271
- // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
1272
- showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
1279
+ const endCoords = getEventCoords(ev);
1280
+ showTextEditor(endCoords.clientX, endCoords.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
1273
1281
  }
1274
1282
 
1275
1283
  draggedText = null;
1276
1284
  }
1277
1285
 
1278
- document.addEventListener('mousemove', onMouseMove);
1279
- document.addEventListener('mouseup', onMouseUp);
1286
+ document.addEventListener('mousemove', onMove);
1287
+ document.addEventListener('mouseup', onEnd);
1288
+ document.addEventListener('touchmove', onMove, { passive: false });
1289
+ document.addEventListener('touchend', onEnd);
1280
1290
  }
1281
1291
 
1282
1292
  // Inline Text Editor
@@ -2417,12 +2427,22 @@
2417
2427
  // ERGONOMIC FEATURES
2418
2428
  // ==========================================
2419
2429
 
2420
- // Fullscreen toggle function
2430
+
2431
+ // Fullscreen state (tracks both native and simulated fullscreen)
2432
+ let isFullscreen = false;
2433
+
2434
+ // Fullscreen toggle function — delegates to parent iframe handler via postMessage
2421
2435
  function toggleFullscreen() {
2422
- if (document.fullscreenElement) {
2423
- document.exitFullscreen();
2436
+ if (window.self !== window.top) {
2437
+ // Inside iframe: ask parent to handle fullscreen
2438
+ window.parent.postMessage({ type: 'pdf-secure-fullscreen-toggle' }, '*');
2424
2439
  } else {
2425
- document.documentElement.requestFullscreen().catch(() => { });
2440
+ // Standalone mode: use native fullscreen
2441
+ if (document.fullscreenElement) {
2442
+ document.exitFullscreen();
2443
+ } else {
2444
+ document.documentElement.requestFullscreen().catch(() => { });
2445
+ }
2426
2446
  }
2427
2447
  }
2428
2448
 
@@ -2430,7 +2450,7 @@
2430
2450
  function updateFullscreenIcon() {
2431
2451
  const icon = document.getElementById('fullscreenIcon');
2432
2452
  const btn = document.getElementById('fullscreenBtn');
2433
- if (document.fullscreenElement) {
2453
+ if (isFullscreen) {
2434
2454
  icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
2435
2455
  btn.classList.add('active');
2436
2456
  } else {
@@ -2439,32 +2459,36 @@
2439
2459
  }
2440
2460
  }
2441
2461
 
2442
- document.addEventListener('fullscreenchange', updateFullscreenIcon);
2443
-
2444
- // Fullscreen button click
2445
- document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
2462
+ // Apply fullscreen CSS class and touch restrictions
2463
+ function applyFullscreenTouchRestrictions() {
2464
+ document.body.classList.toggle('viewer-fullscreen', isFullscreen);
2465
+ if (isFullscreen) {
2466
+ document.documentElement.style.touchAction = 'pan-y pinch-zoom';
2467
+ document.documentElement.style.overscrollBehavior = 'none';
2468
+ } else {
2469
+ document.documentElement.style.touchAction = '';
2470
+ document.documentElement.style.overscrollBehavior = '';
2471
+ }
2472
+ }
2446
2473
 
2447
- // Double-click on page for fullscreen
2448
- let lastClickTime = 0;
2449
- container.addEventListener('click', (e) => {
2450
- const now = Date.now();
2451
- if (now - lastClickTime < 300) {
2452
- toggleFullscreen();
2474
+ // Listen for fullscreen state from parent (simulated fullscreen)
2475
+ window.addEventListener('message', (event) => {
2476
+ if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
2477
+ isFullscreen = event.data.isFullscreen;
2478
+ updateFullscreenIcon();
2479
+ applyFullscreenTouchRestrictions();
2453
2480
  }
2454
- lastClickTime = now;
2455
2481
  });
2456
2482
 
2457
- // Auto-fullscreen when viewer loads inside iframe
2458
- if (window.self !== window.top && window.PDF_SECURE_CONFIG) {
2459
- // We're inside an iframe - request fullscreen on first user interaction
2460
- const autoFullscreen = () => {
2461
- document.documentElement.requestFullscreen().catch(() => { });
2462
- container.removeEventListener('click', autoFullscreen);
2463
- container.removeEventListener('touchstart', autoFullscreen);
2464
- };
2465
- container.addEventListener('click', autoFullscreen, { once: true });
2466
- container.addEventListener('touchstart', autoFullscreen, { once: true });
2467
- }
2483
+ // Local fullscreen events (standalone mode)
2484
+ document.addEventListener('fullscreenchange', () => {
2485
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement);
2486
+ updateFullscreenIcon();
2487
+ applyFullscreenTouchRestrictions();
2488
+ });
2489
+
2490
+ document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
2491
+
2468
2492
 
2469
2493
  // Mouse wheel zoom with Ctrl (debounced, clamped 0.5x-5x)
2470
2494
  let zoomTimeout;
package/static/viewer.css CHANGED
@@ -1482,3 +1482,15 @@ body {
1482
1482
  height: 56px;
1483
1483
  }
1484
1484
  }
1485
+
1486
+ /* ==========================================
1487
+ FULLSCREEN STATE (simulated fullscreen on mobile)
1488
+ ========================================== */
1489
+ /* Ensure bottom toolbar is visible and viewer container makes room for it */
1490
+ body.viewer-fullscreen #bottomToolbar {
1491
+ display: block;
1492
+ }
1493
+
1494
+ body.viewer-fullscreen #viewerContainer {
1495
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1496
+ }
@@ -401,6 +401,18 @@
401
401
  border-radius: 4px 0 0 4px;
402
402
  }
403
403
 
404
+ /* Color indicator bar under tool button */
405
+ .toolColorIndicator {
406
+ position: absolute;
407
+ bottom: 2px;
408
+ left: 6px;
409
+ right: calc(20px + 6px);
410
+ height: 3px;
411
+ border-radius: 2px;
412
+ pointer-events: none;
413
+ transition: background 0.15s;
414
+ }
415
+
404
416
  .dropdownArrow {
405
417
  width: 20px;
406
418
  height: 36px;
@@ -1178,12 +1190,17 @@
1178
1190
  z-index: 100;
1179
1191
  padding: 0 8px;
1180
1192
  padding-bottom: var(--safe-area-bottom);
1193
+ /* Flex layout: scrollable tools + fixed fullscreen button */
1194
+ align-items: center;
1195
+ gap: 0;
1181
1196
  }
1182
1197
 
1183
1198
  .bottomToolbarInner {
1184
1199
  display: flex;
1185
1200
  align-items: center;
1186
1201
  gap: 2px;
1202
+ flex: 1;
1203
+ min-width: 0;
1187
1204
  height: var(--bottom-bar-height);
1188
1205
  overflow-x: auto;
1189
1206
  overflow-y: hidden;
@@ -1196,6 +1213,14 @@
1196
1213
  display: none;
1197
1214
  }
1198
1215
 
1216
+ /* Fullscreen button pinned to right side of bottom toolbar */
1217
+ .bottomFullscreenBtn {
1218
+ flex-shrink: 0;
1219
+ margin-left: 4px;
1220
+ border-left: 1px solid var(--border-color);
1221
+ padding-left: 4px;
1222
+ }
1223
+
1199
1224
  /* Dropdown backdrop overlay */
1200
1225
  #dropdownBackdrop {
1201
1226
  display: none;
@@ -1248,7 +1273,7 @@
1248
1273
 
1249
1274
  /* Show bottom toolbar */
1250
1275
  #bottomToolbar {
1251
- display: block;
1276
+ display: flex;
1252
1277
  }
1253
1278
 
1254
1279
  /* Viewer container adjusted for mobile toolbars */
@@ -1353,6 +1378,18 @@
1353
1378
  }
1354
1379
  }
1355
1380
 
1381
+ /* ==========================================
1382
+ FULLSCREEN STATE (simulated fullscreen on mobile)
1383
+ ========================================== */
1384
+ /* Ensure bottom toolbar is visible and viewer container makes room for it */
1385
+ body.viewer-fullscreen #bottomToolbar {
1386
+ display: flex;
1387
+ }
1388
+
1389
+ body.viewer-fullscreen #viewerContainer {
1390
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1391
+ }
1392
+
1356
1393
  /* ==========================================
1357
1394
  TABLET BREAKPOINT (600px - 1024px)
1358
1395
  ========================================== */
@@ -1418,7 +1455,7 @@
1418
1455
 
1419
1456
  /* Show bottom toolbar */
1420
1457
  #bottomToolbar {
1421
- display: block;
1458
+ display: flex;
1422
1459
  }
1423
1460
 
1424
1461
  /* Viewer container adjusted for both toolbars */
@@ -1561,6 +1598,7 @@
1561
1598
  <path d="M3 21h18v-2H3v2zM5 16h14l-3-10H8l-3 10zM9 8h6l1.5 5h-9L9 8z" opacity="0.7" />
1562
1599
  </svg>
1563
1600
  </button>
1601
+ <div class="toolColorIndicator" id="highlightColorIndicator" style="background:#fff100"></div>
1564
1602
  <button class="dropdownArrow" id="highlightArrow">
1565
1603
  <svg viewBox="0 0 24 24">
1566
1604
  <path d="M7 10l5 5 5-5z" />
@@ -1606,6 +1644,7 @@
1606
1644
  d="M20.71 4.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83zM3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z" />
1607
1645
  </svg>
1608
1646
  </button>
1647
+ <div class="toolColorIndicator" id="drawColorIndicator" style="background:#e81224"></div>
1609
1648
  <button class="dropdownArrow" id="drawArrow">
1610
1649
  <svg viewBox="0 0 24 24">
1611
1650
  <path d="M7 10l5 5 5-5z" />
@@ -1709,6 +1748,7 @@
1709
1748
  <path d="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm13 0a5 5 0 110 10 5 5 0 010-10z" />
1710
1749
  </svg>
1711
1750
  </button>
1751
+ <div class="toolColorIndicator" id="shapeColorIndicator" style="background:#e81224"></div>
1712
1752
  <button class="dropdownArrow" id="shapesArrow">
1713
1753
  <svg viewBox="0 0 24 24">
1714
1754
  <path d="M7 10l5 5 5-5z" />
@@ -1851,6 +1891,13 @@
1851
1891
  </svg>
1852
1892
  <span>Okuma Modu</span>
1853
1893
  </button>
1894
+ <div class="overflowDivider"></div>
1895
+ <button class="overflowItem" id="overflowFullscreen">
1896
+ <svg viewBox="0 0 24 24">
1897
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
1898
+ </svg>
1899
+ <span>Tam Ekran</span>
1900
+ </button>
1854
1901
  </div>
1855
1902
  </div>
1856
1903
  </div>
@@ -1861,11 +1908,16 @@
1861
1908
  </div>
1862
1909
  </div>
1863
1910
 
1864
- <!-- Bottom Toolbar (Mobile Only) -->
1911
+ <!-- Bottom Toolbar (Mobile/Tablet Portrait) -->
1865
1912
  <div id="bottomToolbar">
1866
1913
  <div class="bottomToolbarInner" id="bottomToolbarInner">
1867
1914
  <!-- Annotation tool buttons will be moved here on mobile via JS -->
1868
1915
  </div>
1916
+ <button class="toolbarBtn bottomFullscreenBtn" id="bottomFullscreenBtn" title="Tam Ekran">
1917
+ <svg viewBox="0 0 24 24">
1918
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
1919
+ </svg>
1920
+ </button>
1869
1921
  </div>
1870
1922
 
1871
1923
  <!-- Dropdown Backdrop (Mobile) -->
@@ -2417,7 +2469,10 @@
2417
2469
  dropdown.addEventListener('touchstart', (e) => {
2418
2470
  const rect = dropdown.getBoundingClientRect();
2419
2471
  const touchY = e.touches[0].clientY;
2420
- if (touchY - rect.top > 40 && dropdown.scrollTop > 0) return;
2472
+ // Only start swipe-to-dismiss from the handle area (top ~40px)
2473
+ // Don't intercept touches on interactive content (color dots, sliders, shape buttons)
2474
+ const isInteractive = e.target.closest('.colorDot, .colorGrid, .thicknessSlider, .shapeBtn, .shapeGrid, input, .strokePreview, .overflowItem');
2475
+ if (touchY - rect.top > 40 || isInteractive) return;
2421
2476
  startY = touchY;
2422
2477
  currentY = startY;
2423
2478
  isDragging = true;
@@ -2475,6 +2530,10 @@
2475
2530
  document.getElementById('sepiaBtn').classList.contains('active'));
2476
2531
  closeAllDropdowns();
2477
2532
  };
2533
+ document.getElementById('overflowFullscreen').onclick = () => {
2534
+ toggleFullscreen();
2535
+ closeAllDropdowns();
2536
+ };
2478
2537
 
2479
2538
  // Close dropdowns when clicking outside
2480
2539
  document.addEventListener('click', (e) => {
@@ -2557,8 +2616,9 @@
2557
2616
  dot.classList.add('active');
2558
2617
  highlightColor = dot.dataset.color;
2559
2618
  if (currentTool === 'highlight') currentColor = highlightColor;
2560
- // Update preview
2619
+ // Update preview and color indicator
2561
2620
  document.getElementById('highlightWave').setAttribute('stroke', highlightColor);
2621
+ document.getElementById('highlightColorIndicator').style.background = highlightColor;
2562
2622
  };
2563
2623
  });
2564
2624
 
@@ -2570,8 +2630,9 @@
2570
2630
  dot.classList.add('active');
2571
2631
  drawColor = dot.dataset.color;
2572
2632
  if (currentTool === 'pen') currentColor = drawColor;
2573
- // Update preview
2633
+ // Update preview and color indicator
2574
2634
  document.getElementById('drawWave').setAttribute('stroke', drawColor);
2635
+ document.getElementById('drawColorIndicator').style.background = drawColor;
2575
2636
  };
2576
2637
  });
2577
2638
 
@@ -2609,6 +2670,8 @@
2609
2670
  dot.classList.add('active');
2610
2671
  shapeColor = dot.dataset.color;
2611
2672
  if (currentTool === 'shape') currentColor = shapeColor;
2673
+ // Update color indicator
2674
+ document.getElementById('shapeColorIndicator').style.background = shapeColor;
2612
2675
  };
2613
2676
  });
2614
2677
 
@@ -2993,7 +3056,8 @@
2993
3056
  // Text tool - create/edit/drag text
2994
3057
  if (currentTool === 'text') {
2995
3058
  // Check if clicked on existing text element
2996
- const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
3059
+ // Use coords (touch-safe) instead of e.clientX which is undefined on TouchEvent
3060
+ const elementsUnderClick = document.elementsFromPoint(coords.clientX, coords.clientY);
2997
3061
  const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
2998
3062
 
2999
3063
  if (existingText) {
@@ -3001,7 +3065,7 @@
3001
3065
  startTextDrag(e, existingText, svg, scaleX, pageNum);
3002
3066
  } else {
3003
3067
  // Create new text
3004
- showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
3068
+ showTextEditor(coords.clientX, coords.clientY, svg, x, y, scaleX, pageNum);
3005
3069
  }
3006
3070
  return;
3007
3071
  }
@@ -3271,14 +3335,18 @@
3271
3335
  textEl.classList.add('dragging');
3272
3336
  hasDragged = false;
3273
3337
 
3274
- dragStartX = e.clientX;
3275
- dragStartY = e.clientY;
3338
+ // Touch-safe coordinate extraction
3339
+ const startCoords = getEventCoords(e);
3340
+ dragStartX = startCoords.clientX;
3341
+ dragStartY = startCoords.clientY;
3276
3342
  textOriginalX = parseFloat(textEl.getAttribute('x'));
3277
3343
  textOriginalY = parseFloat(textEl.getAttribute('y'));
3278
3344
 
3279
- function onMouseMove(ev) {
3280
- const dxScreen = ev.clientX - dragStartX;
3281
- const dyScreen = ev.clientY - dragStartY;
3345
+ function onMove(ev) {
3346
+ ev.preventDefault();
3347
+ const moveCoords = getEventCoords(ev);
3348
+ const dxScreen = moveCoords.clientX - dragStartX;
3349
+ const dyScreen = moveCoords.clientY - dragStartY;
3282
3350
  // Convert screen delta to viewBox delta (rotation-aware)
3283
3351
  const vbDelta = screenDeltaToViewBox(svg, dxScreen, dyScreen);
3284
3352
 
@@ -3290,29 +3358,31 @@
3290
3358
  textEl.setAttribute('y', (textOriginalY + vbDelta.dy).toFixed(2));
3291
3359
  }
3292
3360
 
3293
- function onMouseUp(ev) {
3294
- document.removeEventListener('mousemove', onMouseMove);
3295
- document.removeEventListener('mouseup', onMouseUp);
3361
+ function onEnd(ev) {
3362
+ document.removeEventListener('mousemove', onMove);
3363
+ document.removeEventListener('mouseup', onEnd);
3364
+ document.removeEventListener('touchmove', onMove);
3365
+ document.removeEventListener('touchend', onEnd);
3296
3366
  textEl.classList.remove('dragging');
3297
3367
 
3298
3368
  if (hasDragged) {
3299
3369
  // Moved - save position
3300
3370
  saveAnnotations(pageNum);
3301
3371
  } else {
3302
- // Not moved - short click = edit
3303
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
3304
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
3372
+ // Not moved - short click/tap = edit
3305
3373
  const svgX = parseFloat(textEl.getAttribute('x'));
3306
3374
  const svgY = parseFloat(textEl.getAttribute('y'));
3307
- // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
3308
- showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
3375
+ const endCoords = getEventCoords(ev);
3376
+ showTextEditor(endCoords.clientX, endCoords.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
3309
3377
  }
3310
3378
 
3311
3379
  draggedText = null;
3312
3380
  }
3313
3381
 
3314
- document.addEventListener('mousemove', onMouseMove);
3315
- document.addEventListener('mouseup', onMouseUp);
3382
+ document.addEventListener('mousemove', onMove);
3383
+ document.addEventListener('mouseup', onEnd);
3384
+ document.addEventListener('touchmove', onMove, { passive: false });
3385
+ document.addEventListener('touchend', onEnd);
3316
3386
  }
3317
3387
 
3318
3388
  // Inline Text Editor
@@ -4452,17 +4522,29 @@
4452
4522
  function updateFullscreenIcon() {
4453
4523
  const icon = document.getElementById('fullscreenIcon');
4454
4524
  const btn = document.getElementById('fullscreenBtn');
4525
+ const bottomBtn = document.getElementById('bottomFullscreenBtn');
4526
+ const exitPath = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4527
+ const enterPath = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
4455
4528
  if (isFullscreen) {
4456
- icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4529
+ icon.innerHTML = exitPath;
4457
4530
  btn.classList.add('active');
4531
+ if (bottomBtn) {
4532
+ bottomBtn.querySelector('svg').innerHTML = exitPath;
4533
+ bottomBtn.classList.add('active');
4534
+ }
4458
4535
  } else {
4459
- icon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
4536
+ icon.innerHTML = enterPath;
4460
4537
  btn.classList.remove('active');
4538
+ if (bottomBtn) {
4539
+ bottomBtn.querySelector('svg').innerHTML = enterPath;
4540
+ bottomBtn.classList.remove('active');
4541
+ }
4461
4542
  }
4462
4543
  }
4463
4544
 
4464
4545
  // Apply touch restrictions when entering/exiting fullscreen
4465
4546
  function applyFullscreenTouchRestrictions() {
4547
+ document.body.classList.toggle('viewer-fullscreen', isFullscreen);
4466
4548
  if (isFullscreen) {
4467
4549
  document.documentElement.style.touchAction = 'pan-y pinch-zoom';
4468
4550
  document.documentElement.style.overscrollBehavior = 'none';
@@ -4493,8 +4575,9 @@
4493
4575
  applyFullscreenTouchRestrictions();
4494
4576
  });
4495
4577
 
4496
- // Fullscreen button click
4578
+ // Fullscreen button click (top toolbar + bottom toolbar)
4497
4579
  document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
4580
+ document.getElementById('bottomFullscreenBtn').onclick = () => toggleFullscreen();
4498
4581
 
4499
4582
 
4500
4583