nodebb-plugin-pdf-secure 1.2.4 → 1.2.6

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.
@@ -1330,813 +1330,855 @@
1330
1330
  </div>
1331
1331
 
1332
1332
  <script>
1333
- pdfjsLib.GlobalWorkerOptions.workerSrc = '';
1334
-
1335
- // State
1336
- let pdfDoc = null;
1337
- let pdfViewer = null;
1338
- let annotationMode = false;
1339
- let currentTool = null; // null, 'pen', 'highlight', 'eraser'
1340
- let currentColor = '#e81224';
1341
- let currentWidth = 2;
1342
- let isDrawing = false;
1343
- let currentPath = null;
1344
- let currentDrawingPage = null;
1345
-
1346
- // Annotation persistence - stores SVG innerHTML per page
1347
- const annotationsStore = new Map();
1348
-
1349
- // Store base dimensions (scale=1.0) for each page - ensures consistent coordinates
1350
- const pageBaseDimensions = new Map();
1351
-
1352
- // Current SVG reference for drawing
1353
- let currentSvg = null;
1354
-
1355
- // Elements
1356
- const container = document.getElementById('viewerContainer');
1357
- const uploadOverlay = document.getElementById('uploadOverlay');
1358
- const fileInput = document.getElementById('fileInput');
1359
- const sidebar = document.getElementById('sidebar');
1360
- const thumbnailContainer = document.getElementById('thumbnailContainer');
1361
-
1362
- // Initialize PDFViewer
1363
- const eventBus = new pdfjsViewer.EventBus();
1364
- const linkService = new pdfjsViewer.PDFLinkService({ eventBus });
1365
-
1366
- pdfViewer = new pdfjsViewer.PDFViewer({
1367
- container: container,
1368
- eventBus: eventBus,
1369
- linkService: linkService,
1370
- removePageBorders: true,
1371
- textLayerMode: 2
1372
- });
1373
- linkService.setViewer(pdfViewer);
1374
-
1375
- // File Handling
1376
- document.getElementById('dropzone').onclick = () => fileInput.click();
1377
-
1378
- fileInput.onchange = async (e) => {
1379
- const file = e.target.files[0];
1380
- if (file) await loadPDF(file);
1381
- };
1382
-
1383
- uploadOverlay.ondragover = (e) => e.preventDefault();
1384
- uploadOverlay.ondrop = async (e) => {
1385
- e.preventDefault();
1386
- const file = e.dataTransfer.files[0];
1387
- if (file?.type === 'application/pdf') await loadPDF(file);
1388
- };
1389
-
1390
- async function loadPDF(file) {
1391
- uploadOverlay.classList.add('hidden');
1392
-
1393
- const data = await file.arrayBuffer();
1394
- pdfDoc = await pdfjsLib.getDocument({ data }).promise;
1395
-
1396
- pdfViewer.setDocument(pdfDoc);
1397
- linkService.setDocument(pdfDoc);
1398
-
1399
- ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
1400
- document.getElementById(id).disabled = false;
1333
+ // IIFE to prevent global access to pdfDoc, pdfViewer
1334
+ (function () {
1335
+ 'use strict';
1336
+
1337
+ // ============================================
1338
+ // CANVAS EXPORT PROTECTION
1339
+ // Block toDataURL/toBlob for PDF render canvas only
1340
+ // Allows: thumbnails, annotations, other canvases
1341
+ // ============================================
1342
+ const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
1343
+ const originalToBlob = HTMLCanvasElement.prototype.toBlob;
1344
+
1345
+ HTMLCanvasElement.prototype.toDataURL = function () {
1346
+ // Block only main PDF page canvases (inside .page elements in #viewerContainer)
1347
+ if (this.closest && this.closest('.page') && this.closest('#viewerContainer')) {
1348
+ console.warn('[Security] Canvas toDataURL blocked for PDF page');
1349
+ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; // 1x1 transparent
1350
+ }
1351
+ return originalToDataURL.apply(this, arguments);
1352
+ };
1353
+
1354
+ HTMLCanvasElement.prototype.toBlob = function (callback) {
1355
+ // Block only main PDF page canvases
1356
+ if (this.closest && this.closest('.page') && this.closest('#viewerContainer')) {
1357
+ console.warn('[Security] Canvas toBlob blocked for PDF page');
1358
+ // Return empty blob
1359
+ if (callback) callback(new Blob([], { type: 'image/png' }));
1360
+ return;
1361
+ }
1362
+ return originalToBlob.apply(this, arguments);
1363
+ };
1364
+
1365
+ pdfjsLib.GlobalWorkerOptions.workerSrc = '';
1366
+
1367
+ // State - now private, not accessible from console
1368
+ let pdfDoc = null;
1369
+ let pdfViewer = null;
1370
+ let annotationMode = false;
1371
+ let currentTool = null; // null, 'pen', 'highlight', 'eraser'
1372
+ let currentColor = '#e81224';
1373
+ let currentWidth = 2;
1374
+ let isDrawing = false;
1375
+ let currentPath = null;
1376
+ let currentDrawingPage = null;
1377
+
1378
+ // Annotation persistence - stores SVG innerHTML per page
1379
+ const annotationsStore = new Map();
1380
+
1381
+ // Store base dimensions (scale=1.0) for each page - ensures consistent coordinates
1382
+ const pageBaseDimensions = new Map();
1383
+
1384
+ // Current SVG reference for drawing
1385
+ let currentSvg = null;
1386
+
1387
+ // Elements
1388
+ const container = document.getElementById('viewerContainer');
1389
+ const uploadOverlay = document.getElementById('uploadOverlay');
1390
+ const fileInput = document.getElementById('fileInput');
1391
+ const sidebar = document.getElementById('sidebar');
1392
+ const thumbnailContainer = document.getElementById('thumbnailContainer');
1393
+
1394
+ // Initialize PDFViewer
1395
+ const eventBus = new pdfjsViewer.EventBus();
1396
+ const linkService = new pdfjsViewer.PDFLinkService({ eventBus });
1397
+
1398
+ pdfViewer = new pdfjsViewer.PDFViewer({
1399
+ container: container,
1400
+ eventBus: eventBus,
1401
+ linkService: linkService,
1402
+ removePageBorders: true,
1403
+ textLayerMode: 2
1404
+ });
1405
+ linkService.setViewer(pdfViewer);
1406
+
1407
+ // Track first page render for queue system
1408
+ let firstPageRendered = false;
1409
+ eventBus.on('pagerendered', function (evt) {
1410
+ if (!firstPageRendered && evt.pageNumber === 1) {
1411
+ firstPageRendered = true;
1412
+ // Notify parent that PDF is fully rendered (for queue system)
1413
+ if (window.parent && window.parent !== window) {
1414
+ const config = window.PDF_SECURE_CONFIG || {};
1415
+ window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, '*');
1416
+ console.log('[PDF-Secure] First page rendered, notifying parent');
1417
+ }
1418
+ }
1401
1419
  });
1402
1420
 
1403
- // Generate thumbnails
1404
- generateThumbnails();
1405
- }
1421
+ // File Handling
1422
+ document.getElementById('dropzone').onclick = () => fileInput.click();
1406
1423
 
1407
- // Load PDF from ArrayBuffer (for secure nonce-based loading)
1408
- async function loadPDFFromBuffer(arrayBuffer) {
1409
- uploadOverlay.classList.add('hidden');
1424
+ fileInput.onchange = async (e) => {
1425
+ const file = e.target.files[0];
1426
+ if (file) await loadPDF(file);
1427
+ };
1410
1428
 
1411
- pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
1429
+ uploadOverlay.ondragover = (e) => e.preventDefault();
1430
+ uploadOverlay.ondrop = async (e) => {
1431
+ e.preventDefault();
1432
+ const file = e.dataTransfer.files[0];
1433
+ if (file?.type === 'application/pdf') await loadPDF(file);
1434
+ };
1412
1435
 
1413
- pdfViewer.setDocument(pdfDoc);
1414
- linkService.setDocument(pdfDoc);
1436
+ async function loadPDF(file) {
1437
+ uploadOverlay.classList.add('hidden');
1415
1438
 
1416
- ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
1417
- document.getElementById(id).disabled = false;
1418
- });
1439
+ const data = await file.arrayBuffer();
1440
+ pdfDoc = await pdfjsLib.getDocument({ data }).promise;
1419
1441
 
1420
- generateThumbnails();
1421
- }
1442
+ pdfViewer.setDocument(pdfDoc);
1443
+ linkService.setDocument(pdfDoc);
1422
1444
 
1423
- // Partial XOR decoder - must match backend encoding
1424
- function partialXorDecode(encodedData, keyBase64) {
1425
- const key = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
1426
- const data = new Uint8Array(encodedData);
1427
- const keyLen = key.length;
1445
+ ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
1446
+ document.getElementById(id).disabled = false;
1447
+ });
1428
1448
 
1429
- // Decrypt first 10KB fully
1430
- const fullDecryptLen = Math.min(10240, data.length);
1431
- for (let i = 0; i < fullDecryptLen; i++) {
1432
- data[i] = data[i] ^ key[i % keyLen];
1449
+ // Thumbnails will be generated on-demand when sidebar opens
1433
1450
  }
1434
1451
 
1435
- // Decrypt every 50th byte after that
1436
- for (let i = fullDecryptLen; i < data.length; i += 50) {
1437
- data[i] = data[i] ^ key[i % keyLen];
1452
+ // Load PDF from ArrayBuffer (for secure nonce-based loading)
1453
+ async function loadPDFFromBuffer(arrayBuffer) {
1454
+ uploadOverlay.classList.add('hidden');
1455
+
1456
+ pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
1457
+
1458
+ pdfViewer.setDocument(pdfDoc);
1459
+ linkService.setDocument(pdfDoc);
1460
+
1461
+ ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
1462
+ document.getElementById(id).disabled = false;
1463
+ });
1464
+
1465
+ // Thumbnails will be generated on-demand when sidebar opens
1438
1466
  }
1439
1467
 
1440
- return data.buffer;
1441
- }
1468
+ // Partial XOR decoder - must match backend encoding
1469
+ function partialXorDecode(encodedData, keyBase64) {
1470
+ const key = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
1471
+ const data = new Uint8Array(encodedData);
1472
+ const keyLen = key.length;
1473
+
1474
+ // Decrypt first 10KB fully
1475
+ const fullDecryptLen = Math.min(10240, data.length);
1476
+ for (let i = 0; i < fullDecryptLen; i++) {
1477
+ data[i] = data[i] ^ key[i % keyLen];
1478
+ }
1442
1479
 
1443
- // Auto-load PDF if config is present (injected by NodeBB plugin)
1444
- async function autoLoadSecurePDF() {
1445
- if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
1446
- console.log('[PDF-Secure] No config found, showing file picker');
1447
- return;
1480
+ // Decrypt every 50th byte after that
1481
+ for (let i = fullDecryptLen; i < data.length; i += 50) {
1482
+ data[i] = data[i] ^ key[i % keyLen];
1483
+ }
1484
+
1485
+ return data.buffer;
1448
1486
  }
1449
1487
 
1450
- const config = window.PDF_SECURE_CONFIG;
1451
- console.log('[PDF-Secure] Auto-loading:', config.filename);
1488
+ // Auto-load PDF if config is present (injected by NodeBB plugin)
1489
+ async function autoLoadSecurePDF() {
1490
+ if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
1491
+ console.log('[PDF-Secure] No config found, showing file picker');
1492
+ return;
1493
+ }
1494
+
1495
+ const config = window.PDF_SECURE_CONFIG;
1496
+ console.log('[PDF-Secure] Auto-loading:', config.filename);
1452
1497
 
1453
- // Show loading state
1454
- const dropzone = document.getElementById('dropzone');
1455
- if (dropzone) {
1456
- dropzone.innerHTML = `
1498
+ // Show loading state
1499
+ const dropzone = document.getElementById('dropzone');
1500
+ if (dropzone) {
1501
+ dropzone.innerHTML = `
1457
1502
  <svg viewBox="0 0 24 24" class="spin">
1458
1503
  <path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z" />
1459
1504
  </svg>
1460
1505
  <h2>PDF Yükleniyor...</h2>
1461
1506
  <p>${config.filename}</p>
1462
1507
  `;
1463
- }
1508
+ }
1464
1509
 
1465
- try {
1466
- // Step 1: Get nonce
1467
- const nonceUrl = config.relativePath + '/api/v3/plugins/pdf-secure/nonce?file=' + encodeURIComponent(config.filename);
1468
- const nonceRes = await fetch(nonceUrl, {
1469
- credentials: 'same-origin',
1470
- headers: { 'x-csrf-token': config.csrfToken }
1471
- });
1510
+ try {
1511
+ // Nonce and key are embedded in HTML config (not fetched from API)
1512
+ // This improves security - key is ONLY in HTML source, not in any network response
1513
+ const nonce = config.nonce;
1514
+ const xorKey = config.dk;
1472
1515
 
1473
- if (!nonceRes.ok) {
1474
- throw new Error(nonceRes.status === 401 ? 'Giriş yapmanız gerekiyor' : 'Nonce alınamadı');
1475
- }
1516
+ // Fetch encrypted PDF binary
1517
+ const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
1518
+ const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
1476
1519
 
1477
- const nonceData = await nonceRes.json();
1478
- const nonce = nonceData.response.nonce;
1520
+ if (!pdfRes.ok) {
1521
+ throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
1522
+ }
1479
1523
 
1480
- // Step 2: Fetch encrypted PDF binary
1481
- const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
1482
- const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
1524
+ const encodedBuffer = await pdfRes.arrayBuffer();
1483
1525
 
1484
- if (!pdfRes.ok) {
1485
- throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
1486
- }
1526
+ console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
1487
1527
 
1488
- // Get XOR key from header
1489
- const xorKey = pdfRes.headers.get('X-PDF-Key');
1490
- const encodedBuffer = await pdfRes.arrayBuffer();
1528
+ // Step 3: Decode XOR encrypted data
1529
+ let pdfBuffer;
1530
+ if (xorKey) {
1531
+ console.log('[PDF-Secure] Decoding XOR encrypted data...');
1532
+ pdfBuffer = partialXorDecode(encodedBuffer, xorKey);
1533
+ } else {
1534
+ // Fallback for backward compatibility
1535
+ pdfBuffer = encodedBuffer;
1536
+ }
1491
1537
 
1492
- console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
1538
+ console.log('[PDF-Secure] PDF decoded successfully');
1493
1539
 
1494
- // Step 3: Decode XOR encrypted data
1495
- let pdfBuffer;
1496
- if (xorKey) {
1497
- console.log('[PDF-Secure] Decoding XOR encrypted data...');
1498
- pdfBuffer = partialXorDecode(encodedBuffer, xorKey);
1499
- } else {
1500
- // Fallback for backward compatibility
1501
- pdfBuffer = encodedBuffer;
1502
- }
1540
+ // Step 4: Load into viewer
1541
+ await loadPDFFromBuffer(pdfBuffer);
1503
1542
 
1504
- console.log('[PDF-Secure] PDF decoded successfully');
1543
+ // Step 5: Moved to pagerendered event for proper timing
1505
1544
 
1506
- // Step 4: Load into viewer
1507
- await loadPDFFromBuffer(pdfBuffer);
1545
+ // Step 6: Security - clear references to prevent extraction
1546
+ pdfBuffer = null;
1508
1547
 
1509
- // Step 5: Notify parent that PDF is fully loaded (for queue system)
1510
- if (window.parent && window.parent !== window) {
1511
- window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, '*');
1512
- }
1548
+ // Security: Delete config containing sensitive data (nonce, key)
1549
+ delete window.PDF_SECURE_CONFIG;
1513
1550
 
1514
- // Step 6: Security - clear references to prevent extraction
1515
- pdfBuffer = null;
1551
+ // Security: Remove PDF.js globals to prevent console manipulation
1552
+ delete window.pdfjsLib;
1553
+ delete window.pdfjsViewer;
1516
1554
 
1517
- console.log('[PDF-Secure] PDF fully loaded and ready');
1555
+ // Security: Block dangerous PDF.js methods
1556
+ if (pdfDoc) {
1557
+ pdfDoc.getData = function () {
1558
+ console.warn('[Security] getData() is blocked');
1559
+ return Promise.reject(new Error('Access denied'));
1560
+ };
1561
+ pdfDoc.saveDocument = function () {
1562
+ console.warn('[Security] saveDocument() is blocked');
1563
+ return Promise.reject(new Error('Access denied'));
1564
+ };
1565
+ }
1518
1566
 
1519
- } catch (err) {
1520
- console.error('[PDF-Secure] Auto-load error:', err);
1521
- if (dropzone) {
1522
- dropzone.innerHTML = `
1567
+ console.log('[PDF-Secure] PDF fully loaded and ready');
1568
+
1569
+ } catch (err) {
1570
+ console.error('[PDF-Secure] Auto-load error:', err);
1571
+ if (dropzone) {
1572
+ dropzone.innerHTML = `
1523
1573
  <svg viewBox="0 0 24 24" style="fill: #e81224;">
1524
1574
  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
1525
1575
  </svg>
1526
1576
  <h2>Hata</h2>
1527
1577
  <p>${err.message}</p>
1528
1578
  `;
1579
+ }
1529
1580
  }
1530
1581
  }
1531
- }
1532
1582
 
1533
- // Run auto-load on page ready
1534
- autoLoadSecurePDF();
1583
+ // Run auto-load on page ready
1584
+ autoLoadSecurePDF();
1535
1585
 
1536
- // Generate Thumbnails
1537
- async function generateThumbnails() {
1538
- thumbnailContainer.innerHTML = '';
1586
+ // Generate Thumbnails (deferred - only when sidebar opens)
1587
+ let thumbnailsGenerated = false;
1588
+ async function generateThumbnails() {
1589
+ if (thumbnailsGenerated) return;
1590
+ thumbnailsGenerated = true;
1591
+ thumbnailContainer.innerHTML = '';
1539
1592
 
1540
- for (let i = 1; i <= pdfDoc.numPages; i++) {
1541
- const page = await pdfDoc.getPage(i);
1542
- const viewport = page.getViewport({ scale: 0.2 });
1593
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
1594
+ const page = await pdfDoc.getPage(i);
1595
+ const viewport = page.getViewport({ scale: 0.2 });
1543
1596
 
1544
- const canvas = document.createElement('canvas');
1545
- canvas.width = viewport.width;
1546
- canvas.height = viewport.height;
1597
+ const canvas = document.createElement('canvas');
1598
+ canvas.width = viewport.width;
1599
+ canvas.height = viewport.height;
1547
1600
 
1548
- await page.render({
1549
- canvasContext: canvas.getContext('2d'),
1550
- viewport: viewport
1551
- }).promise;
1601
+ await page.render({
1602
+ canvasContext: canvas.getContext('2d'),
1603
+ viewport: viewport
1604
+ }).promise;
1552
1605
 
1553
- const thumb = document.createElement('div');
1554
- thumb.className = 'thumbnail' + (i === 1 ? ' active' : '');
1555
- thumb.dataset.page = i;
1556
- thumb.innerHTML = `<div class="thumbnailNum">${i}</div>`;
1557
- thumb.insertBefore(canvas, thumb.firstChild);
1606
+ const thumb = document.createElement('div');
1607
+ thumb.className = 'thumbnail' + (i === 1 ? ' active' : '');
1608
+ thumb.dataset.page = i;
1609
+ thumb.innerHTML = `<div class="thumbnailNum">${i}</div>`;
1610
+ thumb.insertBefore(canvas, thumb.firstChild);
1558
1611
 
1559
- thumb.onclick = () => {
1560
- pdfViewer.currentPageNumber = i;
1561
- document.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'));
1562
- thumb.classList.add('active');
1563
- };
1612
+ thumb.onclick = () => {
1613
+ pdfViewer.currentPageNumber = i;
1614
+ document.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'));
1615
+ thumb.classList.add('active');
1616
+ };
1564
1617
 
1565
- thumbnailContainer.appendChild(thumb);
1618
+ thumbnailContainer.appendChild(thumb);
1619
+ }
1566
1620
  }
1567
- }
1568
1621
 
1569
- // Events
1570
- eventBus.on('pagesinit', () => {
1571
- pdfViewer.currentScaleValue = 'page-width';
1572
- document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
1573
- });
1622
+ // Events
1623
+ eventBus.on('pagesinit', () => {
1624
+ pdfViewer.currentScaleValue = 'page-width';
1625
+ document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
1626
+ });
1574
1627
 
1575
- eventBus.on('pagechanging', (evt) => {
1576
- document.getElementById('pageInput').value = evt.pageNumber;
1577
- // Update active thumbnail
1578
- document.querySelectorAll('.thumbnail').forEach(t => {
1579
- t.classList.toggle('active', parseInt(t.dataset.page) === evt.pageNumber);
1628
+ eventBus.on('pagechanging', (evt) => {
1629
+ document.getElementById('pageInput').value = evt.pageNumber;
1630
+ // Update active thumbnail
1631
+ document.querySelectorAll('.thumbnail').forEach(t => {
1632
+ t.classList.toggle('active', parseInt(t.dataset.page) === evt.pageNumber);
1633
+ });
1580
1634
  });
1581
- });
1582
1635
 
1583
- eventBus.on('pagerendered', (evt) => {
1584
- if (annotationMode) injectAnnotationLayer(evt.pageNumber);
1585
- });
1636
+ eventBus.on('pagerendered', (evt) => {
1637
+ if (annotationMode) injectAnnotationLayer(evt.pageNumber);
1638
+ });
1586
1639
 
1587
- // Page Navigation
1588
- document.getElementById('pageInput').onchange = (e) => {
1589
- const num = parseInt(e.target.value);
1590
- if (num >= 1 && num <= pdfViewer.pagesCount) {
1591
- pdfViewer.currentPageNumber = num;
1592
- }
1593
- };
1594
-
1595
- // Zoom
1596
- document.getElementById('zoomIn').onclick = () => pdfViewer.currentScale += 0.25;
1597
- document.getElementById('zoomOut').onclick = () => pdfViewer.currentScale -= 0.25;
1598
-
1599
- // Sepia Reading Mode
1600
- let sepiaMode = false;
1601
- document.getElementById('sepiaBtn').onclick = () => {
1602
- sepiaMode = !sepiaMode;
1603
- document.getElementById('viewer').classList.toggle('sepia', sepiaMode);
1604
- container.classList.toggle('sepia', sepiaMode);
1605
- document.getElementById('sepiaBtn').classList.toggle('active', sepiaMode);
1606
- };
1607
-
1608
- // Page Rotation
1609
- const pageRotations = new Map(); // Store rotation per page
1610
-
1611
- function rotatePage(delta) {
1612
- const pageNum = pdfViewer.currentPageNumber;
1613
- const currentRotation = pageRotations.get(pageNum) || 0;
1614
- const newRotation = (currentRotation + delta + 360) % 360;
1615
- pageRotations.set(pageNum, newRotation);
1616
-
1617
- // Apply rotation only to the canvas (not the whole page div)
1618
- const pageView = pdfViewer.getPageView(pageNum - 1);
1619
- if (pageView?.div) {
1620
- const canvas = pageView.div.querySelector('canvas');
1621
- const textLayer = pageView.div.querySelector('.textLayer');
1622
- const annotationLayer = pageView.div.querySelector('.annotationLayer');
1623
-
1624
- if (canvas) {
1625
- canvas.style.transform = `rotate(${newRotation}deg)`;
1626
- canvas.style.transformOrigin = 'center center';
1627
- }
1628
- if (textLayer) {
1629
- textLayer.style.transform = `rotate(${newRotation}deg)`;
1630
- textLayer.style.transformOrigin = 'center center';
1631
- }
1632
- if (annotationLayer) {
1633
- annotationLayer.style.transform = `rotate(${newRotation}deg)`;
1634
- annotationLayer.style.transformOrigin = 'center center';
1635
- }
1636
-
1637
- // Also rotate text highlight container
1638
- const textHighlightContainer = pageView.div.querySelector('.textHighlightContainer');
1639
- if (textHighlightContainer) {
1640
- textHighlightContainer.style.transform = `rotate(${newRotation}deg)`;
1641
- textHighlightContainer.style.transformOrigin = 'center center';
1640
+ // Page Navigation
1641
+ document.getElementById('pageInput').onchange = (e) => {
1642
+ const num = parseInt(e.target.value);
1643
+ if (num >= 1 && num <= pdfViewer.pagesCount) {
1644
+ pdfViewer.currentPageNumber = num;
1642
1645
  }
1643
- }
1644
- }
1646
+ };
1647
+
1648
+ // Zoom
1649
+ document.getElementById('zoomIn').onclick = () => pdfViewer.currentScale += 0.25;
1650
+ document.getElementById('zoomOut').onclick = () => pdfViewer.currentScale -= 0.25;
1651
+
1652
+ // Sidebar toggle (deferred thumbnail generation)
1653
+ const sidebarEl = document.getElementById('sidebar');
1654
+ const sidebarBtnEl = document.getElementById('sidebarBtn');
1655
+ const closeSidebarBtn = document.getElementById('closeSidebar');
1656
+
1657
+ sidebarBtnEl.onclick = () => {
1658
+ const isOpening = !sidebarEl.classList.contains('open');
1659
+ sidebarEl.classList.toggle('open');
1660
+ sidebarBtnEl.classList.toggle('active');
1645
1661
 
1646
- document.getElementById('rotateLeft').onclick = () => rotatePage(-90);
1647
- document.getElementById('rotateRight').onclick = () => rotatePage(90);
1648
-
1649
- // Sidebar Toggle
1650
- document.getElementById('sidebarBtn').onclick = () => {
1651
- sidebar.classList.toggle('open');
1652
- container.classList.toggle('withSidebar', sidebar.classList.contains('open'));
1653
- document.getElementById('sidebarBtn').classList.toggle('active', sidebar.classList.contains('open'));
1654
- };
1655
-
1656
- document.getElementById('closeSidebar').onclick = () => {
1657
- sidebar.classList.remove('open');
1658
- container.classList.remove('withSidebar');
1659
- document.getElementById('sidebarBtn').classList.remove('active');
1660
- };
1661
-
1662
-
1663
- // Tool settings - separate for each tool
1664
- let highlightColor = '#fff100';
1665
- let highlightWidth = 4;
1666
- let drawColor = '#e81224';
1667
- let drawWidth = 2;
1668
- let shapeColor = '#e81224';
1669
- let shapeWidth = 2;
1670
- let currentShape = 'rectangle'; // rectangle, circle, line, arrow
1671
-
1672
- // Dropdown Panel Logic
1673
- const highlightDropdown = document.getElementById('highlightDropdown');
1674
- const drawDropdown = document.getElementById('drawDropdown');
1675
- const shapesDropdown = document.getElementById('shapesDropdown');
1676
- const highlightWrapper = document.getElementById('highlightWrapper');
1677
- const drawWrapper = document.getElementById('drawWrapper');
1678
- const shapesWrapper = document.getElementById('shapesWrapper');
1679
-
1680
- function closeAllDropdowns() {
1681
- highlightDropdown.classList.remove('visible');
1682
- drawDropdown.classList.remove('visible');
1683
- shapesDropdown.classList.remove('visible');
1684
- }
1685
-
1686
- function toggleDropdown(dropdown, e) {
1687
- e.stopPropagation();
1688
- const isVisible = dropdown.classList.contains('visible');
1689
- closeAllDropdowns();
1690
- if (!isVisible) {
1691
- dropdown.classList.add('visible');
1662
+ // Generate thumbnails on first open (deferred loading)
1663
+ if (isOpening && pdfDoc) {
1664
+ generateThumbnails();
1665
+ }
1666
+ };
1667
+
1668
+ closeSidebarBtn.onclick = () => {
1669
+ sidebarEl.classList.remove('open');
1670
+ sidebarBtnEl.classList.remove('active');
1671
+ };
1672
+
1673
+ // Sepia Reading Mode
1674
+ let sepiaMode = false;
1675
+ document.getElementById('sepiaBtn').onclick = () => {
1676
+ sepiaMode = !sepiaMode;
1677
+ document.getElementById('viewer').classList.toggle('sepia', sepiaMode);
1678
+ container.classList.toggle('sepia', sepiaMode);
1679
+ document.getElementById('sepiaBtn').classList.toggle('active', sepiaMode);
1680
+ };
1681
+
1682
+ // Page Rotation
1683
+ const pageRotations = new Map(); // Store rotation per page
1684
+
1685
+ function rotatePage(delta) {
1686
+ const pageNum = pdfViewer.currentPageNumber;
1687
+ const currentRotation = pageRotations.get(pageNum) || 0;
1688
+ const newRotation = (currentRotation + delta + 360) % 360;
1689
+ pageRotations.set(pageNum, newRotation);
1690
+
1691
+ // Apply rotation only to the canvas (not the whole page div)
1692
+ const pageView = pdfViewer.getPageView(pageNum - 1);
1693
+ if (pageView?.div) {
1694
+ const canvas = pageView.div.querySelector('canvas');
1695
+ const textLayer = pageView.div.querySelector('.textLayer');
1696
+ const annotationLayer = pageView.div.querySelector('.annotationLayer');
1697
+
1698
+ if (canvas) {
1699
+ canvas.style.transform = `rotate(${newRotation}deg)`;
1700
+ canvas.style.transformOrigin = 'center center';
1701
+ }
1702
+ if (textLayer) {
1703
+ textLayer.style.transform = `rotate(${newRotation}deg)`;
1704
+ textLayer.style.transformOrigin = 'center center';
1705
+ }
1706
+ if (annotationLayer) {
1707
+ annotationLayer.style.transform = `rotate(${newRotation}deg)`;
1708
+ annotationLayer.style.transformOrigin = 'center center';
1709
+ }
1710
+
1711
+ // Also rotate text highlight container
1712
+ const textHighlightContainer = pageView.div.querySelector('.textHighlightContainer');
1713
+ if (textHighlightContainer) {
1714
+ textHighlightContainer.style.transform = `rotate(${newRotation}deg)`;
1715
+ textHighlightContainer.style.transformOrigin = 'center center';
1716
+ }
1717
+ }
1692
1718
  }
1693
- }
1694
1719
 
1695
- // Arrow buttons toggle dropdowns
1696
- document.getElementById('highlightArrow').onclick = (e) => toggleDropdown(highlightDropdown, e);
1697
- document.getElementById('drawArrow').onclick = (e) => toggleDropdown(drawDropdown, e);
1698
- document.getElementById('shapesArrow').onclick = (e) => toggleDropdown(shapesDropdown, e);
1720
+ document.getElementById('rotateLeft').onclick = () => rotatePage(-90);
1721
+ document.getElementById('rotateRight').onclick = () => rotatePage(90);
1722
+
1723
+ // Sidebar Toggle
1724
+ document.getElementById('sidebarBtn').onclick = () => {
1725
+ sidebar.classList.toggle('open');
1726
+ container.classList.toggle('withSidebar', sidebar.classList.contains('open'));
1727
+ document.getElementById('sidebarBtn').classList.toggle('active', sidebar.classList.contains('open'));
1728
+ };
1699
1729
 
1700
- // Close dropdowns when clicking outside
1701
- document.addEventListener('click', (e) => {
1702
- if (!e.target.closest('.toolDropdown') && !e.target.closest('.dropdownArrow')) {
1730
+ document.getElementById('closeSidebar').onclick = () => {
1731
+ sidebar.classList.remove('open');
1732
+ container.classList.remove('withSidebar');
1733
+ document.getElementById('sidebarBtn').classList.remove('active');
1734
+ };
1735
+
1736
+
1737
+ // Tool settings - separate for each tool
1738
+ let highlightColor = '#fff100';
1739
+ let highlightWidth = 4;
1740
+ let drawColor = '#e81224';
1741
+ let drawWidth = 2;
1742
+ let shapeColor = '#e81224';
1743
+ let shapeWidth = 2;
1744
+ let currentShape = 'rectangle'; // rectangle, circle, line, arrow
1745
+
1746
+ // Dropdown Panel Logic
1747
+ const highlightDropdown = document.getElementById('highlightDropdown');
1748
+ const drawDropdown = document.getElementById('drawDropdown');
1749
+ const shapesDropdown = document.getElementById('shapesDropdown');
1750
+ const highlightWrapper = document.getElementById('highlightWrapper');
1751
+ const drawWrapper = document.getElementById('drawWrapper');
1752
+ const shapesWrapper = document.getElementById('shapesWrapper');
1753
+
1754
+ function closeAllDropdowns() {
1755
+ highlightDropdown.classList.remove('visible');
1756
+ drawDropdown.classList.remove('visible');
1757
+ shapesDropdown.classList.remove('visible');
1758
+ }
1759
+
1760
+ function toggleDropdown(dropdown, e) {
1761
+ e.stopPropagation();
1762
+ const isVisible = dropdown.classList.contains('visible');
1703
1763
  closeAllDropdowns();
1764
+ if (!isVisible) {
1765
+ dropdown.classList.add('visible');
1766
+ }
1704
1767
  }
1705
- });
1706
-
1707
- // Prevent dropdown from closing when clicking inside
1708
- highlightDropdown.onclick = (e) => e.stopPropagation();
1709
- drawDropdown.onclick = (e) => e.stopPropagation();
1710
- shapesDropdown.onclick = (e) => e.stopPropagation();
1711
-
1712
- // Drawing Tools - Toggle Behavior
1713
- async function setTool(tool) {
1714
- // If same tool clicked again, deactivate
1715
- if (currentTool === tool) {
1716
- currentTool = null;
1717
- annotationMode = false;
1718
- document.querySelectorAll('.annotationLayer').forEach(el => el.classList.remove('active'));
1719
- } else {
1720
- currentTool = tool;
1721
- annotationMode = true;
1722
-
1723
- // Set color and width based on tool
1724
- if (tool === 'highlight') {
1725
- currentColor = highlightColor;
1726
- currentWidth = highlightWidth;
1727
- } else if (tool === 'pen') {
1728
- currentColor = drawColor;
1729
- currentWidth = drawWidth;
1730
- } else if (tool === 'shape') {
1731
- currentColor = shapeColor;
1732
- currentWidth = shapeWidth;
1733
- }
1734
-
1735
- // BUGFIX: Save current annotation state BEFORE re-injecting layers
1736
- // This prevents deleted content from being restored when switching tools
1737
- for (let i = 0; i < pdfViewer.pagesCount; i++) {
1738
- const pageView = pdfViewer.getPageView(i);
1739
- const svg = pageView?.div?.querySelector('.annotationLayer');
1740
- if (svg) {
1741
- const pageNum = i + 1;
1742
- if (svg.innerHTML.trim()) {
1743
- annotationsStore.set(pageNum, svg.innerHTML);
1744
- } else {
1745
- // Clear from store if empty (all content deleted)
1746
- annotationsStore.delete(pageNum);
1768
+
1769
+ // Arrow buttons toggle dropdowns
1770
+ document.getElementById('highlightArrow').onclick = (e) => toggleDropdown(highlightDropdown, e);
1771
+ document.getElementById('drawArrow').onclick = (e) => toggleDropdown(drawDropdown, e);
1772
+ document.getElementById('shapesArrow').onclick = (e) => toggleDropdown(shapesDropdown, e);
1773
+
1774
+ // Close dropdowns when clicking outside
1775
+ document.addEventListener('click', (e) => {
1776
+ if (!e.target.closest('.toolDropdown') && !e.target.closest('.dropdownArrow')) {
1777
+ closeAllDropdowns();
1778
+ }
1779
+ });
1780
+
1781
+ // Prevent dropdown from closing when clicking inside
1782
+ highlightDropdown.onclick = (e) => e.stopPropagation();
1783
+ drawDropdown.onclick = (e) => e.stopPropagation();
1784
+ shapesDropdown.onclick = (e) => e.stopPropagation();
1785
+
1786
+ // Drawing Tools - Toggle Behavior
1787
+ async function setTool(tool) {
1788
+ // If same tool clicked again, deactivate
1789
+ if (currentTool === tool) {
1790
+ currentTool = null;
1791
+ annotationMode = false;
1792
+ document.querySelectorAll('.annotationLayer').forEach(el => el.classList.remove('active'));
1793
+ } else {
1794
+ currentTool = tool;
1795
+ annotationMode = true;
1796
+
1797
+ // Set color and width based on tool
1798
+ if (tool === 'highlight') {
1799
+ currentColor = highlightColor;
1800
+ currentWidth = highlightWidth;
1801
+ } else if (tool === 'pen') {
1802
+ currentColor = drawColor;
1803
+ currentWidth = drawWidth;
1804
+ } else if (tool === 'shape') {
1805
+ currentColor = shapeColor;
1806
+ currentWidth = shapeWidth;
1807
+ }
1808
+
1809
+ // BUGFIX: Save current annotation state BEFORE re-injecting layers
1810
+ // This prevents deleted content from being restored when switching tools
1811
+ for (let i = 0; i < pdfViewer.pagesCount; i++) {
1812
+ const pageView = pdfViewer.getPageView(i);
1813
+ const svg = pageView?.div?.querySelector('.annotationLayer');
1814
+ if (svg) {
1815
+ const pageNum = i + 1;
1816
+ if (svg.innerHTML.trim()) {
1817
+ annotationsStore.set(pageNum, svg.innerHTML);
1818
+ } else {
1819
+ // Clear from store if empty (all content deleted)
1820
+ annotationsStore.delete(pageNum);
1821
+ }
1747
1822
  }
1748
1823
  }
1749
- }
1750
1824
 
1751
- // Inject annotation layers (await all)
1752
- const promises = [];
1753
- for (let i = 0; i < pdfViewer.pagesCount; i++) {
1754
- const pageView = pdfViewer.getPageView(i);
1755
- if (pageView?.div) {
1756
- promises.push(injectAnnotationLayer(i + 1));
1825
+ // Inject annotation layers (await all)
1826
+ const promises = [];
1827
+ for (let i = 0; i < pdfViewer.pagesCount; i++) {
1828
+ const pageView = pdfViewer.getPageView(i);
1829
+ if (pageView?.div) {
1830
+ promises.push(injectAnnotationLayer(i + 1));
1831
+ }
1757
1832
  }
1833
+ await Promise.all(promises);
1758
1834
  }
1759
- await Promise.all(promises);
1760
- }
1761
1835
 
1762
- // Update button states
1763
- highlightWrapper.classList.toggle('active', currentTool === 'highlight');
1764
- drawWrapper.classList.toggle('active', currentTool === 'pen');
1765
- shapesWrapper.classList.toggle('active', currentTool === 'shape');
1766
- document.getElementById('eraserBtn').classList.toggle('active', currentTool === 'eraser');
1767
- document.getElementById('textBtn').classList.toggle('active', currentTool === 'text');
1768
- document.getElementById('selectBtn').classList.toggle('active', currentTool === 'select');
1769
-
1770
- // Toggle select-mode class on annotation layers
1771
- document.querySelectorAll('.annotationLayer').forEach(layer => {
1772
- layer.classList.toggle('select-mode', currentTool === 'select');
1773
- });
1836
+ // Update button states
1837
+ highlightWrapper.classList.toggle('active', currentTool === 'highlight');
1838
+ drawWrapper.classList.toggle('active', currentTool === 'pen');
1839
+ shapesWrapper.classList.toggle('active', currentTool === 'shape');
1840
+ document.getElementById('eraserBtn').classList.toggle('active', currentTool === 'eraser');
1841
+ document.getElementById('textBtn').classList.toggle('active', currentTool === 'text');
1842
+ document.getElementById('selectBtn').classList.toggle('active', currentTool === 'select');
1843
+
1844
+ // Toggle select-mode class on annotation layers
1845
+ document.querySelectorAll('.annotationLayer').forEach(layer => {
1846
+ layer.classList.toggle('select-mode', currentTool === 'select');
1847
+ });
1774
1848
 
1775
- // Clear selection when switching tools
1776
- if (currentTool !== 'select') {
1777
- clearAnnotationSelection();
1849
+ // Clear selection when switching tools
1850
+ if (currentTool !== 'select') {
1851
+ clearAnnotationSelection();
1852
+ }
1778
1853
  }
1779
- }
1780
1854
 
1781
- document.getElementById('drawBtn').onclick = () => setTool('pen');
1782
- document.getElementById('highlightBtn').onclick = () => setTool('highlight');
1783
- document.getElementById('shapesBtn').onclick = () => setTool('shape');
1784
- document.getElementById('eraserBtn').onclick = () => setTool('eraser');
1785
- document.getElementById('textBtn').onclick = () => setTool('text');
1786
- document.getElementById('selectBtn').onclick = () => setTool('select');
1855
+ document.getElementById('drawBtn').onclick = () => setTool('pen');
1856
+ document.getElementById('highlightBtn').onclick = () => setTool('highlight');
1857
+ document.getElementById('shapesBtn').onclick = () => setTool('shape');
1858
+ document.getElementById('eraserBtn').onclick = () => setTool('eraser');
1859
+ document.getElementById('textBtn').onclick = () => setTool('text');
1860
+ document.getElementById('selectBtn').onclick = () => setTool('select');
1787
1861
 
1788
- // Highlighter Colors
1789
- document.querySelectorAll('#highlightColors .colorDot').forEach(dot => {
1790
- dot.onclick = (e) => {
1791
- e.stopPropagation();
1792
- document.querySelectorAll('#highlightColors .colorDot').forEach(d => d.classList.remove('active'));
1793
- dot.classList.add('active');
1794
- highlightColor = dot.dataset.color;
1795
- if (currentTool === 'highlight') currentColor = highlightColor;
1796
- // Update preview
1797
- document.getElementById('highlightWave').setAttribute('stroke', highlightColor);
1862
+ // Highlighter Colors
1863
+ document.querySelectorAll('#highlightColors .colorDot').forEach(dot => {
1864
+ dot.onclick = (e) => {
1865
+ e.stopPropagation();
1866
+ document.querySelectorAll('#highlightColors .colorDot').forEach(d => d.classList.remove('active'));
1867
+ dot.classList.add('active');
1868
+ highlightColor = dot.dataset.color;
1869
+ if (currentTool === 'highlight') currentColor = highlightColor;
1870
+ // Update preview
1871
+ document.getElementById('highlightWave').setAttribute('stroke', highlightColor);
1872
+ };
1873
+ });
1874
+
1875
+ // Pen Colors
1876
+ document.querySelectorAll('#drawColors .colorDot').forEach(dot => {
1877
+ dot.onclick = (e) => {
1878
+ e.stopPropagation();
1879
+ document.querySelectorAll('#drawColors .colorDot').forEach(d => d.classList.remove('active'));
1880
+ dot.classList.add('active');
1881
+ drawColor = dot.dataset.color;
1882
+ if (currentTool === 'pen') currentColor = drawColor;
1883
+ // Update preview
1884
+ document.getElementById('drawWave').setAttribute('stroke', drawColor);
1885
+ };
1886
+ });
1887
+
1888
+ // Highlighter Thickness Slider
1889
+ document.getElementById('highlightThickness').oninput = (e) => {
1890
+ highlightWidth = parseInt(e.target.value);
1891
+ if (currentTool === 'highlight') currentWidth = highlightWidth;
1892
+ // Update preview - highlighter uses width * 2 for display
1893
+ document.getElementById('highlightWave').setAttribute('stroke-width', highlightWidth * 2);
1798
1894
  };
1799
- });
1800
1895
 
1801
- // Pen Colors
1802
- document.querySelectorAll('#drawColors .colorDot').forEach(dot => {
1803
- dot.onclick = (e) => {
1804
- e.stopPropagation();
1805
- document.querySelectorAll('#drawColors .colorDot').forEach(d => d.classList.remove('active'));
1806
- dot.classList.add('active');
1807
- drawColor = dot.dataset.color;
1808
- if (currentTool === 'pen') currentColor = drawColor;
1896
+ // Pen Thickness Slider
1897
+ document.getElementById('drawThickness').oninput = (e) => {
1898
+ drawWidth = parseInt(e.target.value);
1899
+ if (currentTool === 'pen') currentWidth = drawWidth;
1809
1900
  // Update preview
1810
- document.getElementById('drawWave').setAttribute('stroke', drawColor);
1901
+ document.getElementById('drawWave').setAttribute('stroke-width', drawWidth);
1811
1902
  };
1812
- });
1813
-
1814
- // Highlighter Thickness Slider
1815
- document.getElementById('highlightThickness').oninput = (e) => {
1816
- highlightWidth = parseInt(e.target.value);
1817
- if (currentTool === 'highlight') currentWidth = highlightWidth;
1818
- // Update preview - highlighter uses width * 2 for display
1819
- document.getElementById('highlightWave').setAttribute('stroke-width', highlightWidth * 2);
1820
- };
1821
-
1822
- // Pen Thickness Slider
1823
- document.getElementById('drawThickness').oninput = (e) => {
1824
- drawWidth = parseInt(e.target.value);
1825
- if (currentTool === 'pen') currentWidth = drawWidth;
1826
- // Update preview
1827
- document.getElementById('drawWave').setAttribute('stroke-width', drawWidth);
1828
- };
1829
-
1830
- // Shape Selection
1831
- document.querySelectorAll('.shapeBtn').forEach(btn => {
1832
- btn.onclick = (e) => {
1833
- e.stopPropagation();
1834
- document.querySelectorAll('.shapeBtn').forEach(b => b.classList.remove('active'));
1835
- btn.classList.add('active');
1836
- currentShape = btn.dataset.shape;
1837
- };
1838
- });
1839
1903
 
1840
- // Shape Colors
1841
- document.querySelectorAll('#shapeColors .colorDot').forEach(dot => {
1842
- dot.onclick = (e) => {
1843
- e.stopPropagation();
1844
- document.querySelectorAll('#shapeColors .colorDot').forEach(d => d.classList.remove('active'));
1845
- dot.classList.add('active');
1846
- shapeColor = dot.dataset.color;
1847
- if (currentTool === 'shape') currentColor = shapeColor;
1848
- };
1849
- });
1850
-
1851
- // Shape Thickness Slider
1852
- document.getElementById('shapeThickness').oninput = (e) => {
1853
- shapeWidth = parseInt(e.target.value);
1854
- if (currentTool === 'shape') currentWidth = shapeWidth;
1855
- };
1856
-
1857
- // Annotation Layer with Persistence
1858
- async function injectAnnotationLayer(pageNum) {
1859
- const pageView = pdfViewer.getPageView(pageNum - 1);
1860
- if (!pageView?.div) return;
1861
-
1862
- // Remove old SVG if exists (may have stale reference)
1863
- const oldSvg = pageView.div.querySelector('.annotationLayer');
1864
- if (oldSvg) oldSvg.remove();
1865
-
1866
- // Get or calculate base dimensions (scale=1.0) - FIXED reference
1867
- let baseDims = pageBaseDimensions.get(pageNum);
1868
- if (!baseDims) {
1869
- const page = await pdfDoc.getPage(pageNum);
1870
- const baseViewport = page.getViewport({ scale: 1.0 });
1871
- baseDims = { width: baseViewport.width, height: baseViewport.height };
1872
- pageBaseDimensions.set(pageNum, baseDims);
1873
- }
1904
+ // Shape Selection
1905
+ document.querySelectorAll('.shapeBtn').forEach(btn => {
1906
+ btn.onclick = (e) => {
1907
+ e.stopPropagation();
1908
+ document.querySelectorAll('.shapeBtn').forEach(b => b.classList.remove('active'));
1909
+ btn.classList.add('active');
1910
+ currentShape = btn.dataset.shape;
1911
+ };
1912
+ });
1874
1913
 
1875
- // Create fresh SVG with FIXED viewBox (always scale=1.0 dimensions)
1876
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1877
- svg.setAttribute('class', 'annotationLayer');
1878
- svg.setAttribute('viewBox', `0 0 ${baseDims.width} ${baseDims.height}`);
1879
- svg.setAttribute('preserveAspectRatio', 'none');
1880
- svg.style.width = '100%';
1881
- svg.style.height = '100%';
1882
- svg.dataset.page = pageNum;
1883
- svg.dataset.viewboxWidth = baseDims.width;
1884
- svg.dataset.viewboxHeight = baseDims.height;
1885
- svg.dataset.viewboxHeight = baseDims.height;
1886
- pageView.div.appendChild(svg);
1914
+ // Shape Colors
1915
+ document.querySelectorAll('#shapeColors .colorDot').forEach(dot => {
1916
+ dot.onclick = (e) => {
1917
+ e.stopPropagation();
1918
+ document.querySelectorAll('#shapeColors .colorDot').forEach(d => d.classList.remove('active'));
1919
+ dot.classList.add('active');
1920
+ shapeColor = dot.dataset.color;
1921
+ if (currentTool === 'shape') currentColor = shapeColor;
1922
+ };
1923
+ });
1887
1924
 
1925
+ // Shape Thickness Slider
1926
+ document.getElementById('shapeThickness').oninput = (e) => {
1927
+ shapeWidth = parseInt(e.target.value);
1928
+ if (currentTool === 'shape') currentWidth = shapeWidth;
1929
+ };
1888
1930
 
1931
+ // Annotation Layer with Persistence
1932
+ async function injectAnnotationLayer(pageNum) {
1933
+ const pageView = pdfViewer.getPageView(pageNum - 1);
1934
+ if (!pageView?.div) return;
1935
+
1936
+ // Remove old SVG if exists (may have stale reference)
1937
+ const oldSvg = pageView.div.querySelector('.annotationLayer');
1938
+ if (oldSvg) oldSvg.remove();
1939
+
1940
+ // Get or calculate base dimensions (scale=1.0) - FIXED reference
1941
+ let baseDims = pageBaseDimensions.get(pageNum);
1942
+ if (!baseDims) {
1943
+ const page = await pdfDoc.getPage(pageNum);
1944
+ const baseViewport = page.getViewport({ scale: 1.0 });
1945
+ baseDims = { width: baseViewport.width, height: baseViewport.height };
1946
+ pageBaseDimensions.set(pageNum, baseDims);
1947
+ }
1889
1948
 
1890
- // Restore saved annotations for this page
1891
- if (annotationsStore.has(pageNum)) {
1892
- svg.innerHTML = annotationsStore.get(pageNum);
1893
- }
1949
+ // Create fresh SVG with FIXED viewBox (always scale=1.0 dimensions)
1950
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1951
+ svg.setAttribute('class', 'annotationLayer');
1952
+ svg.setAttribute('viewBox', `0 0 ${baseDims.width} ${baseDims.height}`);
1953
+ svg.setAttribute('preserveAspectRatio', 'none');
1954
+ svg.style.width = '100%';
1955
+ svg.style.height = '100%';
1956
+ svg.dataset.page = pageNum;
1957
+ svg.dataset.viewboxWidth = baseDims.width;
1958
+ svg.dataset.viewboxHeight = baseDims.height;
1959
+ svg.dataset.viewboxHeight = baseDims.height;
1960
+ pageView.div.appendChild(svg);
1894
1961
 
1895
- svg.addEventListener('mousedown', (e) => startDraw(e, pageNum));
1896
- svg.addEventListener('mousemove', draw);
1897
- svg.addEventListener('mouseup', () => stopDraw(pageNum));
1898
- svg.addEventListener('mouseleave', () => stopDraw(pageNum));
1899
1962
 
1900
- // Touch support for tablets
1901
- svg.addEventListener('touchstart', (e) => {
1902
- // Prevent default to avoid scroll while drawing/selecting
1903
- if (currentTool) e.preventDefault();
1904
- startDraw(e, pageNum);
1905
- }, { passive: false });
1906
- svg.addEventListener('touchmove', (e) => {
1907
- if (currentTool) e.preventDefault();
1908
- draw(e);
1909
- }, { passive: false });
1910
- svg.addEventListener('touchend', () => stopDraw(pageNum));
1911
- svg.addEventListener('touchcancel', () => stopDraw(pageNum));
1912
1963
 
1913
- svg.classList.toggle('active', annotationMode);
1914
- }
1964
+ // Restore saved annotations for this page
1965
+ if (annotationsStore.has(pageNum)) {
1966
+ svg.innerHTML = annotationsStore.get(pageNum);
1967
+ }
1915
1968
 
1916
- // Save annotations for a page
1917
- function saveAnnotations(pageNum) {
1918
- const pageView = pdfViewer.getPageView(pageNum - 1);
1919
- const svg = pageView?.div?.querySelector('.annotationLayer');
1920
- if (svg && svg.innerHTML.trim()) {
1921
- annotationsStore.set(pageNum, svg.innerHTML);
1969
+ svg.addEventListener('mousedown', (e) => startDraw(e, pageNum));
1970
+ svg.addEventListener('mousemove', draw);
1971
+ svg.addEventListener('mouseup', () => stopDraw(pageNum));
1972
+ svg.addEventListener('mouseleave', () => stopDraw(pageNum));
1973
+
1974
+ // Touch support for tablets
1975
+ svg.addEventListener('touchstart', (e) => {
1976
+ // Prevent default to avoid scroll while drawing/selecting
1977
+ if (currentTool) e.preventDefault();
1978
+ startDraw(e, pageNum);
1979
+ }, { passive: false });
1980
+ svg.addEventListener('touchmove', (e) => {
1981
+ if (currentTool) e.preventDefault();
1982
+ draw(e);
1983
+ }, { passive: false });
1984
+ svg.addEventListener('touchend', () => stopDraw(pageNum));
1985
+ svg.addEventListener('touchcancel', () => stopDraw(pageNum));
1986
+
1987
+ svg.classList.toggle('active', annotationMode);
1988
+ }
1989
+
1990
+ // Save annotations for a page
1991
+ function saveAnnotations(pageNum) {
1992
+ const pageView = pdfViewer.getPageView(pageNum - 1);
1993
+ const svg = pageView?.div?.querySelector('.annotationLayer');
1994
+ if (svg && svg.innerHTML.trim()) {
1995
+ annotationsStore.set(pageNum, svg.innerHTML);
1996
+ }
1922
1997
  }
1923
- }
1924
1998
 
1925
- function startDraw(e, pageNum) {
1926
- if (!annotationMode || !currentTool) return;
1999
+ function startDraw(e, pageNum) {
2000
+ if (!annotationMode || !currentTool) return;
1927
2001
 
1928
- e.preventDefault(); // Prevent text selection
2002
+ e.preventDefault(); // Prevent text selection
1929
2003
 
1930
- const svg = e.currentTarget;
1931
- if (!svg || !svg.dataset.viewboxWidth) return; // Defensive check
2004
+ const svg = e.currentTarget;
2005
+ if (!svg || !svg.dataset.viewboxWidth) return; // Defensive check
1932
2006
 
1933
- // Handle select tool separately
1934
- if (currentTool === 'select') {
1935
- if (handleSelectMouseDown(e, svg, pageNum)) {
1936
- return; // Select tool handled the event
2007
+ // Handle select tool separately
2008
+ if (currentTool === 'select') {
2009
+ if (handleSelectMouseDown(e, svg, pageNum)) {
2010
+ return; // Select tool handled the event
2011
+ }
1937
2012
  }
1938
- }
1939
2013
 
1940
- isDrawing = true;
1941
- currentDrawingPage = pageNum;
1942
- currentSvg = svg; // Store reference
2014
+ isDrawing = true;
2015
+ currentDrawingPage = pageNum;
2016
+ currentSvg = svg; // Store reference
1943
2017
 
1944
- const rect = svg.getBoundingClientRect();
2018
+ const rect = svg.getBoundingClientRect();
1945
2019
 
1946
- // Convert screen coords to viewBox coords
1947
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
1948
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
1949
- const scaleX = viewBoxWidth / rect.width;
1950
- const scaleY = viewBoxHeight / rect.height;
2020
+ // Convert screen coords to viewBox coords
2021
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2022
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2023
+ const scaleX = viewBoxWidth / rect.width;
2024
+ const scaleY = viewBoxHeight / rect.height;
1951
2025
 
1952
- // Get coordinates from mouse or touch event
1953
- const coords = getEventCoords(e);
1954
- const x = (coords.clientX - rect.left) * scaleX;
1955
- const y = (coords.clientY - rect.top) * scaleY;
1956
-
1957
- if (currentTool === 'eraser') {
1958
- eraseAt(svg, x, y, scaleX);
1959
- saveAnnotations(pageNum);
1960
- return;
1961
- }
2026
+ // Get coordinates from mouse or touch event
2027
+ const coords = getEventCoords(e);
2028
+ const x = (coords.clientX - rect.left) * scaleX;
2029
+ const y = (coords.clientY - rect.top) * scaleY;
1962
2030
 
1963
- // Text tool - create/edit/drag text
1964
- if (currentTool === 'text') {
1965
- // Check if clicked on existing text element
1966
- const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
1967
- const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
2031
+ if (currentTool === 'eraser') {
2032
+ eraseAt(svg, x, y, scaleX);
2033
+ saveAnnotations(pageNum);
2034
+ return;
2035
+ }
1968
2036
 
1969
- if (existingText) {
1970
- // Start dragging (double-click will edit via separate handler)
1971
- startTextDrag(e, existingText, svg, scaleX, scaleY, pageNum);
1972
- } else {
1973
- // Create new text
1974
- showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
2037
+ // Text tool - create/edit/drag text
2038
+ if (currentTool === 'text') {
2039
+ // Check if clicked on existing text element
2040
+ const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
2041
+ const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
2042
+
2043
+ if (existingText) {
2044
+ // Start dragging (double-click will edit via separate handler)
2045
+ startTextDrag(e, existingText, svg, scaleX, scaleY, pageNum);
2046
+ } else {
2047
+ // Create new text
2048
+ showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
2049
+ }
2050
+ return;
1975
2051
  }
1976
- return;
1977
- }
1978
2052
 
1979
- // Shape tool - create shapes
1980
- if (currentTool === 'shape') {
1981
- isDrawing = true;
1982
- // Store start position for shape drawing
1983
- svg.dataset.shapeStartX = x;
1984
- svg.dataset.shapeStartY = y;
1985
- svg.dataset.shapeScaleX = scaleX;
1986
- svg.dataset.shapeScaleY = scaleY;
1987
-
1988
- let shapeEl;
1989
- if (currentShape === 'rectangle') {
1990
- shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
1991
- shapeEl.setAttribute('x', x);
1992
- shapeEl.setAttribute('y', y);
1993
- shapeEl.setAttribute('width', 0);
1994
- shapeEl.setAttribute('height', 0);
1995
- } else if (currentShape === 'circle') {
1996
- shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
1997
- shapeEl.setAttribute('cx', x);
1998
- shapeEl.setAttribute('cy', y);
1999
- shapeEl.setAttribute('rx', 0);
2000
- shapeEl.setAttribute('ry', 0);
2001
- } else if (currentShape === 'line' || currentShape === 'arrow') {
2002
- shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'line');
2003
- shapeEl.setAttribute('x1', x);
2004
- shapeEl.setAttribute('y1', y);
2005
- shapeEl.setAttribute('x2', x);
2006
- shapeEl.setAttribute('y2', y);
2007
- }
2008
-
2009
- shapeEl.setAttribute('stroke', currentColor);
2010
- shapeEl.setAttribute('stroke-width', currentWidth * scaleX);
2011
- shapeEl.setAttribute('fill', 'none');
2012
- shapeEl.classList.add('current-shape');
2013
- svg.appendChild(shapeEl);
2014
- return;
2015
- }
2053
+ // Shape tool - create shapes
2054
+ if (currentTool === 'shape') {
2055
+ isDrawing = true;
2056
+ // Store start position for shape drawing
2057
+ svg.dataset.shapeStartX = x;
2058
+ svg.dataset.shapeStartY = y;
2059
+ svg.dataset.shapeScaleX = scaleX;
2060
+ svg.dataset.shapeScaleY = scaleY;
2061
+
2062
+ let shapeEl;
2063
+ if (currentShape === 'rectangle') {
2064
+ shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
2065
+ shapeEl.setAttribute('x', x);
2066
+ shapeEl.setAttribute('y', y);
2067
+ shapeEl.setAttribute('width', 0);
2068
+ shapeEl.setAttribute('height', 0);
2069
+ } else if (currentShape === 'circle') {
2070
+ shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
2071
+ shapeEl.setAttribute('cx', x);
2072
+ shapeEl.setAttribute('cy', y);
2073
+ shapeEl.setAttribute('rx', 0);
2074
+ shapeEl.setAttribute('ry', 0);
2075
+ } else if (currentShape === 'line' || currentShape === 'arrow') {
2076
+ shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'line');
2077
+ shapeEl.setAttribute('x1', x);
2078
+ shapeEl.setAttribute('y1', y);
2079
+ shapeEl.setAttribute('x2', x);
2080
+ shapeEl.setAttribute('y2', y);
2081
+ }
2016
2082
 
2017
- currentPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2018
- currentPath.setAttribute('stroke', currentColor);
2019
- currentPath.setAttribute('fill', 'none');
2020
-
2021
- if (currentTool === 'highlight') {
2022
- // Highlighter uses stroke size * 5 for thicker strokes
2023
- currentPath.setAttribute('stroke-width', String(currentWidth * 5 * scaleX));
2024
- currentPath.setAttribute('stroke-opacity', '0.35');
2025
- } else {
2026
- currentPath.setAttribute('stroke-width', String(currentWidth * scaleX));
2027
- currentPath.setAttribute('stroke-opacity', '1');
2028
- }
2083
+ shapeEl.setAttribute('stroke', currentColor);
2084
+ shapeEl.setAttribute('stroke-width', currentWidth * scaleX);
2085
+ shapeEl.setAttribute('fill', 'none');
2086
+ shapeEl.classList.add('current-shape');
2087
+ svg.appendChild(shapeEl);
2088
+ return;
2089
+ }
2029
2090
 
2030
- currentPath.setAttribute('d', `M${x.toFixed(2)},${y.toFixed(2)}`);
2031
- svg.appendChild(currentPath);
2032
- }
2091
+ currentPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2092
+ currentPath.setAttribute('stroke', currentColor);
2093
+ currentPath.setAttribute('fill', 'none');
2033
2094
 
2034
- function draw(e) {
2035
- if (!isDrawing || !currentSvg) return;
2095
+ if (currentTool === 'highlight') {
2096
+ // Highlighter uses stroke size * 5 for thicker strokes
2097
+ currentPath.setAttribute('stroke-width', String(currentWidth * 5 * scaleX));
2098
+ currentPath.setAttribute('stroke-opacity', '0.35');
2099
+ } else {
2100
+ currentPath.setAttribute('stroke-width', String(currentWidth * scaleX));
2101
+ currentPath.setAttribute('stroke-opacity', '1');
2102
+ }
2036
2103
 
2037
- e.preventDefault(); // Prevent text selection
2104
+ currentPath.setAttribute('d', `M${x.toFixed(2)},${y.toFixed(2)}`);
2105
+ svg.appendChild(currentPath);
2106
+ }
2038
2107
 
2039
- const svg = currentSvg; // Use stored reference
2040
- if (!svg || !svg.dataset.viewboxWidth) return;
2108
+ function draw(e) {
2109
+ if (!isDrawing || !currentSvg) return;
2041
2110
 
2042
- const rect = svg.getBoundingClientRect();
2111
+ e.preventDefault(); // Prevent text selection
2043
2112
 
2044
- // Convert screen coords to viewBox coords
2045
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2046
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2047
- const scaleX = viewBoxWidth / rect.width;
2048
- const scaleY = viewBoxHeight / rect.height;
2113
+ const svg = currentSvg; // Use stored reference
2114
+ if (!svg || !svg.dataset.viewboxWidth) return;
2049
2115
 
2050
- // Get coordinates from mouse or touch event
2051
- const coords = getEventCoords(e);
2052
- const x = (coords.clientX - rect.left) * scaleX;
2053
- const y = (coords.clientY - rect.top) * scaleY;
2116
+ const rect = svg.getBoundingClientRect();
2054
2117
 
2055
- if (currentTool === 'eraser') {
2056
- eraseAt(svg, x, y, scaleX);
2057
- return;
2058
- }
2118
+ // Convert screen coords to viewBox coords
2119
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2120
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2121
+ const scaleX = viewBoxWidth / rect.width;
2122
+ const scaleY = viewBoxHeight / rect.height;
2059
2123
 
2060
- // Shape tool - update shape size
2061
- if (currentTool === 'shape') {
2062
- const shapeEl = svg.querySelector('.current-shape');
2063
- if (!shapeEl) return;
2064
-
2065
- const startX = parseFloat(svg.dataset.shapeStartX);
2066
- const startY = parseFloat(svg.dataset.shapeStartY);
2067
-
2068
- if (currentShape === 'rectangle') {
2069
- const width = Math.abs(x - startX);
2070
- const height = Math.abs(y - startY);
2071
- shapeEl.setAttribute('x', Math.min(x, startX));
2072
- shapeEl.setAttribute('y', Math.min(y, startY));
2073
- shapeEl.setAttribute('width', width);
2074
- shapeEl.setAttribute('height', height);
2075
- } else if (currentShape === 'circle') {
2076
- const rx = Math.abs(x - startX) / 2;
2077
- const ry = Math.abs(y - startY) / 2;
2078
- shapeEl.setAttribute('cx', (startX + x) / 2);
2079
- shapeEl.setAttribute('cy', (startY + y) / 2);
2080
- shapeEl.setAttribute('rx', rx);
2081
- shapeEl.setAttribute('ry', ry);
2082
- } else if (currentShape === 'line' || currentShape === 'arrow' || currentShape === 'callout') {
2083
- shapeEl.setAttribute('x2', x);
2084
- shapeEl.setAttribute('y2', y);
2085
- }
2086
- return;
2087
- }
2124
+ // Get coordinates from mouse or touch event
2125
+ const coords = getEventCoords(e);
2126
+ const x = (coords.clientX - rect.left) * scaleX;
2127
+ const y = (coords.clientY - rect.top) * scaleY;
2088
2128
 
2089
- if (currentPath) {
2090
- currentPath.setAttribute('d', currentPath.getAttribute('d') + ` L${x.toFixed(2)},${y.toFixed(2)}`);
2091
- }
2092
- }
2129
+ if (currentTool === 'eraser') {
2130
+ eraseAt(svg, x, y, scaleX);
2131
+ return;
2132
+ }
2093
2133
 
2094
- function stopDraw(pageNum) {
2095
- // Handle arrow marker
2096
- if (currentTool === 'shape' && currentShape === 'arrow' && currentSvg) {
2097
- const shapeEl = currentSvg.querySelector('.current-shape');
2098
- if (shapeEl && shapeEl.tagName === 'line') {
2099
- // Create arrow head as a group
2100
- const x1 = parseFloat(shapeEl.getAttribute('x1'));
2101
- const y1 = parseFloat(shapeEl.getAttribute('y1'));
2102
- const x2 = parseFloat(shapeEl.getAttribute('x2'));
2103
- const y2 = parseFloat(shapeEl.getAttribute('y2'));
2104
-
2105
- // Calculate arrow head
2106
- const angle = Math.atan2(y2 - y1, x2 - x1);
2107
- const headLength = 15 * parseFloat(currentSvg.dataset.shapeScaleX || 1);
2108
-
2109
- const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2110
- const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
2111
- const p1y = y2 - headLength * Math.sin(angle - Math.PI / 6);
2112
- const p2x = x2 - headLength * Math.cos(angle + Math.PI / 6);
2113
- const p2y = y2 - headLength * Math.sin(angle + Math.PI / 6);
2114
-
2115
- arrowHead.setAttribute('d', `M${x2},${y2} L${p1x},${p1y} M${x2},${y2} L${p2x},${p2y}`);
2116
- arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
2117
- arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
2118
- arrowHead.setAttribute('fill', 'none');
2119
- currentSvg.appendChild(arrowHead);
2134
+ // Shape tool - update shape size
2135
+ if (currentTool === 'shape') {
2136
+ const shapeEl = svg.querySelector('.current-shape');
2137
+ if (!shapeEl) return;
2138
+
2139
+ const startX = parseFloat(svg.dataset.shapeStartX);
2140
+ const startY = parseFloat(svg.dataset.shapeStartY);
2141
+
2142
+ if (currentShape === 'rectangle') {
2143
+ const width = Math.abs(x - startX);
2144
+ const height = Math.abs(y - startY);
2145
+ shapeEl.setAttribute('x', Math.min(x, startX));
2146
+ shapeEl.setAttribute('y', Math.min(y, startY));
2147
+ shapeEl.setAttribute('width', width);
2148
+ shapeEl.setAttribute('height', height);
2149
+ } else if (currentShape === 'circle') {
2150
+ const rx = Math.abs(x - startX) / 2;
2151
+ const ry = Math.abs(y - startY) / 2;
2152
+ shapeEl.setAttribute('cx', (startX + x) / 2);
2153
+ shapeEl.setAttribute('cy', (startY + y) / 2);
2154
+ shapeEl.setAttribute('rx', rx);
2155
+ shapeEl.setAttribute('ry', ry);
2156
+ } else if (currentShape === 'line' || currentShape === 'arrow' || currentShape === 'callout') {
2157
+ shapeEl.setAttribute('x2', x);
2158
+ shapeEl.setAttribute('y2', y);
2159
+ }
2160
+ return;
2161
+ }
2162
+
2163
+ if (currentPath) {
2164
+ currentPath.setAttribute('d', currentPath.getAttribute('d') + ` L${x.toFixed(2)},${y.toFixed(2)}`);
2120
2165
  }
2121
2166
  }
2122
2167
 
2123
- // Handle callout - arrow with text at the start, pointing to end
2124
- // UX: Click where you want text box, drag to point at something
2125
- if (currentTool === 'shape' && currentShape === 'callout' && currentSvg) {
2126
- const shapeEl = currentSvg.querySelector('.current-shape');
2127
- if (shapeEl && shapeEl.tagName === 'line') {
2128
- const x1 = parseFloat(shapeEl.getAttribute('x1')); // Start - where text box goes
2129
- const y1 = parseFloat(shapeEl.getAttribute('y1'));
2130
- const x2 = parseFloat(shapeEl.getAttribute('x2')); // End - where arrow points
2131
- const y2 = parseFloat(shapeEl.getAttribute('y2'));
2132
-
2133
- // Only create callout if line has been drawn (not just a click)
2134
- if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) {
2135
- const scaleX = parseFloat(currentSvg.dataset.shapeScaleX || 1);
2136
-
2137
- // Arrow head points TO the end (x2,y2) - where user wants to point at
2168
+ function stopDraw(pageNum) {
2169
+ // Handle arrow marker
2170
+ if (currentTool === 'shape' && currentShape === 'arrow' && currentSvg) {
2171
+ const shapeEl = currentSvg.querySelector('.current-shape');
2172
+ if (shapeEl && shapeEl.tagName === 'line') {
2173
+ // Create arrow head as a group
2174
+ const x1 = parseFloat(shapeEl.getAttribute('x1'));
2175
+ const y1 = parseFloat(shapeEl.getAttribute('y1'));
2176
+ const x2 = parseFloat(shapeEl.getAttribute('x2'));
2177
+ const y2 = parseFloat(shapeEl.getAttribute('y2'));
2178
+
2179
+ // Calculate arrow head
2138
2180
  const angle = Math.atan2(y2 - y1, x2 - x1);
2139
- const headLength = 12 * scaleX;
2181
+ const headLength = 15 * parseFloat(currentSvg.dataset.shapeScaleX || 1);
2140
2182
 
2141
2183
  const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2142
2184
  const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
@@ -2148,382 +2190,414 @@
2148
2190
  arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
2149
2191
  arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
2150
2192
  arrowHead.setAttribute('fill', 'none');
2151
- arrowHead.classList.add('callout-arrow');
2152
2193
  currentSvg.appendChild(arrowHead);
2194
+ }
2195
+ }
2153
2196
 
2154
- // Store references for text editor
2155
- const svg = currentSvg;
2156
- const currentPageNum = currentDrawingPage;
2157
- const arrowColor = shapeEl.getAttribute('stroke');
2158
-
2159
- // Calculate screen position for text editor at START of arrow (x1,y1)
2160
- // This is where the user clicked first - where they want the text
2161
- const rect = svg.getBoundingClientRect();
2162
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2163
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2164
- const screenX = rect.left + (x1 / viewBoxWidth) * rect.width;
2165
- const screenY = rect.top + (y1 / viewBoxHeight) * rect.height;
2166
-
2167
- // Remove the current-shape class before showing editor
2168
- shapeEl.classList.remove('current-shape');
2169
-
2170
- // Save first, then open text editor
2171
- saveAnnotations(currentPageNum);
2172
-
2173
- // Open text editor at the START of the arrow (where user clicked)
2174
- setTimeout(() => {
2175
- showTextEditor(screenX, screenY, svg, x1, y1, scaleX, currentPageNum, null, arrowColor);
2176
- }, 50);
2177
-
2178
- // Reset state
2179
- isDrawing = false;
2180
- currentPath = null;
2181
- currentSvg = null;
2182
- currentDrawingPage = null;
2183
- return; // Exit early, text editor will handle the rest
2197
+ // Handle callout - arrow with text at the start, pointing to end
2198
+ // UX: Click where you want text box, drag to point at something
2199
+ if (currentTool === 'shape' && currentShape === 'callout' && currentSvg) {
2200
+ const shapeEl = currentSvg.querySelector('.current-shape');
2201
+ if (shapeEl && shapeEl.tagName === 'line') {
2202
+ const x1 = parseFloat(shapeEl.getAttribute('x1')); // Start - where text box goes
2203
+ const y1 = parseFloat(shapeEl.getAttribute('y1'));
2204
+ const x2 = parseFloat(shapeEl.getAttribute('x2')); // End - where arrow points
2205
+ const y2 = parseFloat(shapeEl.getAttribute('y2'));
2206
+
2207
+ // Only create callout if line has been drawn (not just a click)
2208
+ if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) {
2209
+ const scaleX = parseFloat(currentSvg.dataset.shapeScaleX || 1);
2210
+
2211
+ // Arrow head points TO the end (x2,y2) - where user wants to point at
2212
+ const angle = Math.atan2(y2 - y1, x2 - x1);
2213
+ const headLength = 12 * scaleX;
2214
+
2215
+ const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2216
+ const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
2217
+ const p1y = y2 - headLength * Math.sin(angle - Math.PI / 6);
2218
+ const p2x = x2 - headLength * Math.cos(angle + Math.PI / 6);
2219
+ const p2y = y2 - headLength * Math.sin(angle + Math.PI / 6);
2220
+
2221
+ arrowHead.setAttribute('d', `M${x2},${y2} L${p1x},${p1y} M${x2},${y2} L${p2x},${p2y}`);
2222
+ arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
2223
+ arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
2224
+ arrowHead.setAttribute('fill', 'none');
2225
+ arrowHead.classList.add('callout-arrow');
2226
+ currentSvg.appendChild(arrowHead);
2227
+
2228
+ // Store references for text editor
2229
+ const svg = currentSvg;
2230
+ const currentPageNum = currentDrawingPage;
2231
+ const arrowColor = shapeEl.getAttribute('stroke');
2232
+
2233
+ // Calculate screen position for text editor at START of arrow (x1,y1)
2234
+ // This is where the user clicked first - where they want the text
2235
+ const rect = svg.getBoundingClientRect();
2236
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2237
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2238
+ const screenX = rect.left + (x1 / viewBoxWidth) * rect.width;
2239
+ const screenY = rect.top + (y1 / viewBoxHeight) * rect.height;
2240
+
2241
+ // Remove the current-shape class before showing editor
2242
+ shapeEl.classList.remove('current-shape');
2243
+
2244
+ // Save first, then open text editor
2245
+ saveAnnotations(currentPageNum);
2246
+
2247
+ // Open text editor at the START of the arrow (where user clicked)
2248
+ setTimeout(() => {
2249
+ showTextEditor(screenX, screenY, svg, x1, y1, scaleX, currentPageNum, null, arrowColor);
2250
+ }, 50);
2251
+
2252
+ // Reset state
2253
+ isDrawing = false;
2254
+ currentPath = null;
2255
+ currentSvg = null;
2256
+ currentDrawingPage = null;
2257
+ return; // Exit early, text editor will handle the rest
2258
+ }
2184
2259
  }
2185
2260
  }
2186
- }
2187
2261
 
2188
- // Remove the current-shape class
2189
- if (currentSvg) {
2190
- const shapeEl = currentSvg.querySelector('.current-shape');
2191
- if (shapeEl) shapeEl.classList.remove('current-shape');
2192
- }
2262
+ // Remove the current-shape class
2263
+ if (currentSvg) {
2264
+ const shapeEl = currentSvg.querySelector('.current-shape');
2265
+ if (shapeEl) shapeEl.classList.remove('current-shape');
2266
+ }
2193
2267
 
2194
- if (isDrawing && currentDrawingPage) {
2195
- saveAnnotations(currentDrawingPage);
2268
+ if (isDrawing && currentDrawingPage) {
2269
+ saveAnnotations(currentDrawingPage);
2270
+ }
2271
+ isDrawing = false;
2272
+ currentPath = null;
2273
+ currentSvg = null;
2274
+ currentDrawingPage = null;
2196
2275
  }
2197
- isDrawing = false;
2198
- currentPath = null;
2199
- currentSvg = null;
2200
- currentDrawingPage = null;
2201
- }
2202
2276
 
2203
- // Text Drag-and-Drop
2204
- let draggedText = null;
2205
- let dragStartX = 0;
2206
- let dragStartY = 0;
2207
- let textOriginalX = 0;
2208
- let textOriginalY = 0;
2209
- let hasDragged = false;
2277
+ // Text Drag-and-Drop
2278
+ let draggedText = null;
2279
+ let dragStartX = 0;
2280
+ let dragStartY = 0;
2281
+ let textOriginalX = 0;
2282
+ let textOriginalY = 0;
2283
+ let hasDragged = false;
2210
2284
 
2211
- function startTextDrag(e, textEl, svg, scaleX, scaleY, pageNum) {
2212
- e.preventDefault();
2213
- e.stopPropagation();
2285
+ function startTextDrag(e, textEl, svg, scaleX, scaleY, pageNum) {
2286
+ e.preventDefault();
2287
+ e.stopPropagation();
2214
2288
 
2215
- draggedText = textEl;
2216
- textEl.classList.add('dragging');
2217
- hasDragged = false;
2289
+ draggedText = textEl;
2290
+ textEl.classList.add('dragging');
2291
+ hasDragged = false;
2292
+
2293
+ const rect = svg.getBoundingClientRect();
2294
+ dragStartX = e.clientX;
2295
+ dragStartY = e.clientY;
2296
+ textOriginalX = parseFloat(textEl.getAttribute('x'));
2297
+ textOriginalY = parseFloat(textEl.getAttribute('y'));
2218
2298
 
2219
- const rect = svg.getBoundingClientRect();
2220
- dragStartX = e.clientX;
2221
- dragStartY = e.clientY;
2222
- textOriginalX = parseFloat(textEl.getAttribute('x'));
2223
- textOriginalY = parseFloat(textEl.getAttribute('y'));
2299
+ function onMouseMove(ev) {
2300
+ const dx = (ev.clientX - dragStartX) * scaleX;
2301
+ const dy = (ev.clientY - dragStartY) * scaleY;
2224
2302
 
2225
- function onMouseMove(ev) {
2226
- const dx = (ev.clientX - dragStartX) * scaleX;
2227
- const dy = (ev.clientY - dragStartY) * scaleY;
2303
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
2304
+ hasDragged = true;
2305
+ }
2228
2306
 
2229
- if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
2230
- hasDragged = true;
2307
+ textEl.setAttribute('x', (textOriginalX + dx).toFixed(2));
2308
+ textEl.setAttribute('y', (textOriginalY + dy).toFixed(2));
2231
2309
  }
2232
2310
 
2233
- textEl.setAttribute('x', (textOriginalX + dx).toFixed(2));
2234
- textEl.setAttribute('y', (textOriginalY + dy).toFixed(2));
2235
- }
2311
+ function onMouseUp(ev) {
2312
+ document.removeEventListener('mousemove', onMouseMove);
2313
+ document.removeEventListener('mouseup', onMouseUp);
2314
+ textEl.classList.remove('dragging');
2236
2315
 
2237
- function onMouseUp(ev) {
2238
- document.removeEventListener('mousemove', onMouseMove);
2239
- document.removeEventListener('mouseup', onMouseUp);
2240
- textEl.classList.remove('dragging');
2316
+ if (hasDragged) {
2317
+ // Moved - save position
2318
+ saveAnnotations(pageNum);
2319
+ } else {
2320
+ // Not moved - short click = edit
2321
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2322
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2323
+ const svgX = parseFloat(textEl.getAttribute('x'));
2324
+ const svgY = parseFloat(textEl.getAttribute('y'));
2325
+ // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
2326
+ showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
2327
+ }
2241
2328
 
2242
- if (hasDragged) {
2243
- // Moved - save position
2244
- saveAnnotations(pageNum);
2245
- } else {
2246
- // Not moved - short click = edit
2247
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2248
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2249
- const svgX = parseFloat(textEl.getAttribute('x'));
2250
- const svgY = parseFloat(textEl.getAttribute('y'));
2251
- // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
2252
- showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
2329
+ draggedText = null;
2253
2330
  }
2254
2331
 
2255
- draggedText = null;
2332
+ document.addEventListener('mousemove', onMouseMove);
2333
+ document.addEventListener('mouseup', onMouseUp);
2256
2334
  }
2257
2335
 
2258
- document.addEventListener('mousemove', onMouseMove);
2259
- document.addEventListener('mouseup', onMouseUp);
2260
- }
2336
+ // Inline Text Editor
2337
+ let textFontSize = 14;
2261
2338
 
2262
- // Inline Text Editor
2263
- let textFontSize = 14;
2339
+ function showTextEditor(screenX, screenY, svg, svgX, svgY, scale, pageNum, existingTextEl = null, overrideColor = null) {
2340
+ // Remove existing editor if any
2341
+ const existingOverlay = document.querySelector('.textEditorOverlay');
2342
+ if (existingOverlay) existingOverlay.remove();
2264
2343
 
2265
- function showTextEditor(screenX, screenY, svg, svgX, svgY, scale, pageNum, existingTextEl = null, overrideColor = null) {
2266
- // Remove existing editor if any
2267
- const existingOverlay = document.querySelector('.textEditorOverlay');
2268
- if (existingOverlay) existingOverlay.remove();
2344
+ // Use override color (for callout) or current color
2345
+ const textColor = overrideColor || currentColor;
2269
2346
 
2270
- // Use override color (for callout) or current color
2271
- const textColor = overrideColor || currentColor;
2347
+ // If editing existing text, get its properties
2348
+ let editingText = null;
2349
+ if (existingTextEl && typeof existingTextEl === 'object' && existingTextEl.textContent !== undefined) {
2350
+ editingText = existingTextEl.textContent;
2351
+ textFontSize = parseFloat(existingTextEl.getAttribute('font-size')) / scale || 14;
2352
+ }
2272
2353
 
2273
- // If editing existing text, get its properties
2274
- let editingText = null;
2275
- if (existingTextEl && typeof existingTextEl === 'object' && existingTextEl.textContent !== undefined) {
2276
- editingText = existingTextEl.textContent;
2277
- textFontSize = parseFloat(existingTextEl.getAttribute('font-size')) / scale || 14;
2278
- }
2354
+ // Create overlay
2355
+ const overlay = document.createElement('div');
2356
+ overlay.className = 'textEditorOverlay';
2357
+
2358
+ // Create editor box
2359
+ const box = document.createElement('div');
2360
+ box.className = 'textEditorBox';
2361
+ box.style.left = screenX + 'px';
2362
+ box.style.top = screenY + 'px';
2363
+
2364
+ // Input area
2365
+ const input = document.createElement('div');
2366
+ input.className = 'textEditorInput';
2367
+ input.contentEditable = true;
2368
+ input.style.color = textColor;
2369
+ input.style.fontSize = textFontSize + 'px';
2370
+ if (editingText) {
2371
+ input.textContent = editingText;
2372
+ }
2279
2373
 
2280
- // Create overlay
2281
- const overlay = document.createElement('div');
2282
- overlay.className = 'textEditorOverlay';
2283
-
2284
- // Create editor box
2285
- const box = document.createElement('div');
2286
- box.className = 'textEditorBox';
2287
- box.style.left = screenX + 'px';
2288
- box.style.top = screenY + 'px';
2289
-
2290
- // Input area
2291
- const input = document.createElement('div');
2292
- input.className = 'textEditorInput';
2293
- input.contentEditable = true;
2294
- input.style.color = textColor;
2295
- input.style.fontSize = textFontSize + 'px';
2296
- if (editingText) {
2297
- input.textContent = editingText;
2298
- }
2374
+ // Toolbar
2375
+ const toolbar = document.createElement('div');
2376
+ toolbar.className = 'textEditorToolbar';
2299
2377
 
2300
- // Toolbar
2301
- const toolbar = document.createElement('div');
2302
- toolbar.className = 'textEditorToolbar';
2378
+ // Color indicator
2379
+ const colorDot = document.createElement('div');
2380
+ colorDot.className = 'textEditorColorDot active';
2381
+ colorDot.style.background = textColor;
2303
2382
 
2304
- // Color indicator
2305
- const colorDot = document.createElement('div');
2306
- colorDot.className = 'textEditorColorDot active';
2307
- colorDot.style.background = textColor;
2383
+ // Font size decrease
2384
+ const decreaseBtn = document.createElement('button');
2385
+ decreaseBtn.className = 'textEditorBtn';
2386
+ decreaseBtn.innerHTML = 'A<sup>-</sup>';
2387
+ decreaseBtn.onclick = (e) => {
2388
+ e.stopPropagation();
2389
+ if (textFontSize > 10) {
2390
+ textFontSize -= 2;
2391
+ input.style.fontSize = textFontSize + 'px';
2392
+ }
2393
+ };
2308
2394
 
2309
- // Font size decrease
2310
- const decreaseBtn = document.createElement('button');
2311
- decreaseBtn.className = 'textEditorBtn';
2312
- decreaseBtn.innerHTML = 'A<sup>-</sup>';
2313
- decreaseBtn.onclick = (e) => {
2314
- e.stopPropagation();
2315
- if (textFontSize > 10) {
2316
- textFontSize -= 2;
2317
- input.style.fontSize = textFontSize + 'px';
2318
- }
2319
- };
2395
+ // Font size increase
2396
+ const increaseBtn = document.createElement('button');
2397
+ increaseBtn.className = 'textEditorBtn';
2398
+ increaseBtn.innerHTML = 'A<sup>+</sup>';
2399
+ increaseBtn.onclick = (e) => {
2400
+ e.stopPropagation();
2401
+ if (textFontSize < 32) {
2402
+ textFontSize += 2;
2403
+ input.style.fontSize = textFontSize + 'px';
2404
+ }
2405
+ };
2320
2406
 
2321
- // Font size increase
2322
- const increaseBtn = document.createElement('button');
2323
- increaseBtn.className = 'textEditorBtn';
2324
- increaseBtn.innerHTML = 'A<sup>+</sup>';
2325
- increaseBtn.onclick = (e) => {
2326
- e.stopPropagation();
2327
- if (textFontSize < 32) {
2328
- textFontSize += 2;
2329
- input.style.fontSize = textFontSize + 'px';
2330
- }
2331
- };
2407
+ // Delete button - also deletes existing element if editing
2408
+ const deleteBtn = document.createElement('button');
2409
+ deleteBtn.className = 'textEditorBtn delete';
2410
+ deleteBtn.innerHTML = '🗑️';
2411
+ deleteBtn.onclick = (e) => {
2412
+ e.stopPropagation();
2413
+ if (existingTextEl) {
2414
+ existingTextEl.remove();
2415
+ saveAnnotations(pageNum);
2416
+ }
2417
+ overlay.remove();
2418
+ };
2332
2419
 
2333
- // Delete button - also deletes existing element if editing
2334
- const deleteBtn = document.createElement('button');
2335
- deleteBtn.className = 'textEditorBtn delete';
2336
- deleteBtn.innerHTML = '🗑️';
2337
- deleteBtn.onclick = (e) => {
2338
- e.stopPropagation();
2339
- if (existingTextEl) {
2340
- existingTextEl.remove();
2341
- saveAnnotations(pageNum);
2420
+ toolbar.appendChild(colorDot);
2421
+ toolbar.appendChild(decreaseBtn);
2422
+ toolbar.appendChild(increaseBtn);
2423
+ toolbar.appendChild(deleteBtn);
2424
+
2425
+ box.appendChild(input);
2426
+ box.appendChild(toolbar);
2427
+ overlay.appendChild(box);
2428
+ document.body.appendChild(overlay);
2429
+
2430
+ // Focus input and select all if editing
2431
+ setTimeout(() => {
2432
+ input.focus();
2433
+ if (editingText) {
2434
+ const range = document.createRange();
2435
+ range.selectNodeContents(input);
2436
+ const sel = window.getSelection();
2437
+ sel.removeAllRanges();
2438
+ sel.addRange(range);
2439
+ }
2440
+ }, 50);
2441
+
2442
+ // Confirm on click outside or Enter
2443
+ function confirmText() {
2444
+ const text = input.textContent.trim();
2445
+ if (text) {
2446
+ if (existingTextEl) {
2447
+ // Update existing text element
2448
+ existingTextEl.textContent = text;
2449
+ existingTextEl.setAttribute('fill', textColor);
2450
+ existingTextEl.setAttribute('font-size', String(textFontSize * scale));
2451
+ } else {
2452
+ // Create new text element
2453
+ const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
2454
+ textEl.setAttribute('x', svgX.toFixed(2));
2455
+ textEl.setAttribute('y', svgY.toFixed(2));
2456
+ textEl.setAttribute('fill', textColor);
2457
+ textEl.setAttribute('font-size', String(textFontSize * scale));
2458
+ textEl.setAttribute('font-family', 'Segoe UI, Arial, sans-serif');
2459
+ textEl.textContent = text;
2460
+ svg.appendChild(textEl);
2461
+ }
2462
+ saveAnnotations(pageNum);
2463
+ } else if (existingTextEl) {
2464
+ // Empty text = delete existing
2465
+ existingTextEl.remove();
2466
+ saveAnnotations(pageNum);
2467
+ }
2468
+ overlay.remove();
2342
2469
  }
2343
- overlay.remove();
2344
- };
2345
-
2346
- toolbar.appendChild(colorDot);
2347
- toolbar.appendChild(decreaseBtn);
2348
- toolbar.appendChild(increaseBtn);
2349
- toolbar.appendChild(deleteBtn);
2350
2470
 
2351
- box.appendChild(input);
2352
- box.appendChild(toolbar);
2353
- overlay.appendChild(box);
2354
- document.body.appendChild(overlay);
2471
+ overlay.addEventListener('click', (e) => {
2472
+ if (e.target === overlay) confirmText();
2473
+ });
2355
2474
 
2356
- // Focus input and select all if editing
2357
- setTimeout(() => {
2358
- input.focus();
2359
- if (editingText) {
2360
- const range = document.createRange();
2361
- range.selectNodeContents(input);
2362
- const sel = window.getSelection();
2363
- sel.removeAllRanges();
2364
- sel.addRange(range);
2365
- }
2366
- }, 50);
2367
-
2368
- // Confirm on click outside or Enter
2369
- function confirmText() {
2370
- const text = input.textContent.trim();
2371
- if (text) {
2372
- if (existingTextEl) {
2373
- // Update existing text element
2374
- existingTextEl.textContent = text;
2375
- existingTextEl.setAttribute('fill', textColor);
2376
- existingTextEl.setAttribute('font-size', String(textFontSize * scale));
2377
- } else {
2378
- // Create new text element
2379
- const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
2380
- textEl.setAttribute('x', svgX.toFixed(2));
2381
- textEl.setAttribute('y', svgY.toFixed(2));
2382
- textEl.setAttribute('fill', textColor);
2383
- textEl.setAttribute('font-size', String(textFontSize * scale));
2384
- textEl.setAttribute('font-family', 'Segoe UI, Arial, sans-serif');
2385
- textEl.textContent = text;
2386
- svg.appendChild(textEl);
2475
+ input.addEventListener('keydown', (e) => {
2476
+ if (e.key === 'Enter' && !e.shiftKey) {
2477
+ e.preventDefault();
2478
+ confirmText();
2387
2479
  }
2388
- saveAnnotations(pageNum);
2389
- } else if (existingTextEl) {
2390
- // Empty text = delete existing
2391
- existingTextEl.remove();
2392
- saveAnnotations(pageNum);
2393
- }
2394
- overlay.remove();
2480
+ if (e.key === 'Escape') {
2481
+ overlay.remove();
2482
+ }
2483
+ });
2395
2484
  }
2396
2485
 
2397
- overlay.addEventListener('click', (e) => {
2398
- if (e.target === overlay) confirmText();
2399
- });
2486
+ function eraseAt(svg, x, y, scale = 1) {
2487
+ const hitRadius = 15 * scale; // Scale hit radius with viewBox
2488
+ // Erase paths, text, and shape elements (rect, ellipse, line)
2489
+ svg.querySelectorAll('path, text, rect, ellipse, line').forEach(el => {
2490
+ const bbox = el.getBBox();
2491
+ if (x >= bbox.x - hitRadius && x <= bbox.x + bbox.width + hitRadius &&
2492
+ y >= bbox.y - hitRadius && y <= bbox.y + bbox.height + hitRadius) {
2493
+ el.remove();
2494
+ }
2495
+ });
2400
2496
 
2401
- input.addEventListener('keydown', (e) => {
2402
- if (e.key === 'Enter' && !e.shiftKey) {
2403
- e.preventDefault();
2404
- confirmText();
2405
- }
2406
- if (e.key === 'Escape') {
2407
- overlay.remove();
2497
+ // Also erase text highlights (in separate container)
2498
+ const pageDiv = svg.closest('.page');
2499
+ if (pageDiv) {
2500
+ const highlightContainer = pageDiv.querySelector('.textHighlightContainer');
2501
+ if (highlightContainer) {
2502
+ const pageRect = pageDiv.getBoundingClientRect();
2503
+ const svgRect = svg.getBoundingClientRect();
2504
+ // Convert viewBox coords to screen coords, then to percentages
2505
+ const screenX = (x / scale) + svgRect.left - pageRect.left;
2506
+ const screenY = (y / scale) + svgRect.top - pageRect.top;
2507
+ const screenXPercent = (screenX / pageRect.width) * 100;
2508
+ const screenYPercent = (screenY / pageRect.height) * 100;
2509
+
2510
+ highlightContainer.querySelectorAll('.textHighlight').forEach(el => {
2511
+ const left = parseFloat(el.style.left); // Already in %
2512
+ const top = parseFloat(el.style.top);
2513
+ const width = parseFloat(el.style.width);
2514
+ const height = parseFloat(el.style.height);
2515
+
2516
+ if (screenXPercent >= left - 2 && screenXPercent <= left + width + 2 &&
2517
+ screenYPercent >= top - 2 && screenYPercent <= top + height + 2) {
2518
+ el.remove();
2519
+ // Save changes
2520
+ const pageNum = parseInt(pageDiv.dataset.pageNumber);
2521
+ saveTextHighlights(pageNum, pageDiv);
2522
+ }
2523
+ });
2524
+ }
2408
2525
  }
2409
- });
2410
- }
2526
+ }
2411
2527
 
2412
- function eraseAt(svg, x, y, scale = 1) {
2413
- const hitRadius = 15 * scale; // Scale hit radius with viewBox
2414
- // Erase paths, text, and shape elements (rect, ellipse, line)
2415
- svg.querySelectorAll('path, text, rect, ellipse, line').forEach(el => {
2416
- const bbox = el.getBBox();
2417
- if (x >= bbox.x - hitRadius && x <= bbox.x + bbox.width + hitRadius &&
2418
- y >= bbox.y - hitRadius && y <= bbox.y + bbox.height + hitRadius) {
2419
- el.remove();
2420
- }
2421
- });
2528
+ // ==========================================
2529
+ // TEXT SELECTION HIGHLIGHTING (Adobe/Edge style)
2530
+ // ==========================================
2531
+ let highlightPopup = null;
2422
2532
 
2423
- // Also erase text highlights (in separate container)
2424
- const pageDiv = svg.closest('.page');
2425
- if (pageDiv) {
2426
- const highlightContainer = pageDiv.querySelector('.textHighlightContainer');
2427
- if (highlightContainer) {
2428
- const pageRect = pageDiv.getBoundingClientRect();
2429
- const svgRect = svg.getBoundingClientRect();
2430
- // Convert viewBox coords to screen coords, then to percentages
2431
- const screenX = (x / scale) + svgRect.left - pageRect.left;
2432
- const screenY = (y / scale) + svgRect.top - pageRect.top;
2433
- const screenXPercent = (screenX / pageRect.width) * 100;
2434
- const screenYPercent = (screenY / pageRect.height) * 100;
2435
-
2436
- highlightContainer.querySelectorAll('.textHighlight').forEach(el => {
2437
- const left = parseFloat(el.style.left); // Already in %
2438
- const top = parseFloat(el.style.top);
2439
- const width = parseFloat(el.style.width);
2440
- const height = parseFloat(el.style.height);
2441
-
2442
- if (screenXPercent >= left - 2 && screenXPercent <= left + width + 2 &&
2443
- screenYPercent >= top - 2 && screenYPercent <= top + height + 2) {
2444
- el.remove();
2445
- // Save changes
2446
- const pageNum = parseInt(pageDiv.dataset.pageNumber);
2447
- saveTextHighlights(pageNum, pageDiv);
2448
- }
2449
- });
2533
+ function removeHighlightPopup() {
2534
+ if (highlightPopup) {
2535
+ highlightPopup.remove();
2536
+ highlightPopup = null;
2450
2537
  }
2451
2538
  }
2452
- }
2453
-
2454
- // ==========================================
2455
- // TEXT SELECTION HIGHLIGHTING (Adobe/Edge style)
2456
- // ==========================================
2457
- let highlightPopup = null;
2458
2539
 
2459
- function removeHighlightPopup() {
2460
- if (highlightPopup) {
2461
- highlightPopup.remove();
2462
- highlightPopup = null;
2463
- }
2464
- }
2540
+ function getSelectionRects() {
2541
+ const selection = window.getSelection();
2542
+ if (!selection || selection.isCollapsed || !selection.rangeCount) return null;
2465
2543
 
2466
- function getSelectionRects() {
2467
- const selection = window.getSelection();
2468
- if (!selection || selection.isCollapsed || !selection.rangeCount) return null;
2544
+ const range = selection.getRangeAt(0);
2545
+ const rects = range.getClientRects();
2546
+ if (rects.length === 0) return null;
2469
2547
 
2470
- const range = selection.getRangeAt(0);
2471
- const rects = range.getClientRects();
2472
- if (rects.length === 0) return null;
2548
+ // Find which page the selection is in
2549
+ const startNode = range.startContainer.parentElement;
2550
+ const textLayer = startNode?.closest('.textLayer');
2551
+ if (!textLayer) return null;
2473
2552
 
2474
- // Find which page the selection is in
2475
- const startNode = range.startContainer.parentElement;
2476
- const textLayer = startNode?.closest('.textLayer');
2477
- if (!textLayer) return null;
2553
+ const pageDiv = textLayer.closest('.page');
2554
+ if (!pageDiv) return null;
2478
2555
 
2479
- const pageDiv = textLayer.closest('.page');
2480
- if (!pageDiv) return null;
2556
+ const pageNum = parseInt(pageDiv.dataset.pageNumber);
2557
+ const pageRect = pageDiv.getBoundingClientRect();
2481
2558
 
2482
- const pageNum = parseInt(pageDiv.dataset.pageNumber);
2483
- const pageRect = pageDiv.getBoundingClientRect();
2559
+ // Convert rects to page-relative coordinates
2560
+ const relativeRects = [];
2561
+ for (let i = 0; i < rects.length; i++) {
2562
+ const rect = rects[i];
2563
+ relativeRects.push({
2564
+ x: rect.left - pageRect.left,
2565
+ y: rect.top - pageRect.top,
2566
+ width: rect.width,
2567
+ height: rect.height
2568
+ });
2569
+ }
2484
2570
 
2485
- // Convert rects to page-relative coordinates
2486
- const relativeRects = [];
2487
- for (let i = 0; i < rects.length; i++) {
2488
- const rect = rects[i];
2489
- relativeRects.push({
2490
- x: rect.left - pageRect.left,
2491
- y: rect.top - pageRect.top,
2492
- width: rect.width,
2493
- height: rect.height
2494
- });
2571
+ return { pageNum, pageDiv, relativeRects, lastRect: rects[rects.length - 1] };
2495
2572
  }
2496
2573
 
2497
- return { pageNum, pageDiv, relativeRects, lastRect: rects[rects.length - 1] };
2498
- }
2499
-
2500
- function createTextHighlights(pageDiv, rects, color) {
2501
- // Find or create highlight container
2502
- let highlightContainer = pageDiv.querySelector('.textHighlightContainer');
2503
- if (!highlightContainer) {
2504
- highlightContainer = document.createElement('div');
2505
- highlightContainer.className = 'textHighlightContainer';
2506
- highlightContainer.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
2507
- pageDiv.insertBefore(highlightContainer, pageDiv.firstChild);
2508
- }
2574
+ function createTextHighlights(pageDiv, rects, color) {
2575
+ // Find or create highlight container
2576
+ let highlightContainer = pageDiv.querySelector('.textHighlightContainer');
2577
+ if (!highlightContainer) {
2578
+ highlightContainer = document.createElement('div');
2579
+ highlightContainer.className = 'textHighlightContainer';
2580
+ highlightContainer.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
2581
+ pageDiv.insertBefore(highlightContainer, pageDiv.firstChild);
2582
+ }
2509
2583
 
2510
- // Get page dimensions for percentage calculation
2511
- const pageRect = pageDiv.getBoundingClientRect();
2512
- const pageWidth = pageRect.width;
2513
- const pageHeight = pageRect.height;
2584
+ // Get page dimensions for percentage calculation
2585
+ const pageRect = pageDiv.getBoundingClientRect();
2586
+ const pageWidth = pageRect.width;
2587
+ const pageHeight = pageRect.height;
2514
2588
 
2515
- // Add highlight rectangles with percentage positioning
2516
- rects.forEach(rect => {
2517
- const div = document.createElement('div');
2518
- div.className = 'textHighlight';
2589
+ // Add highlight rectangles with percentage positioning
2590
+ rects.forEach(rect => {
2591
+ const div = document.createElement('div');
2592
+ div.className = 'textHighlight';
2519
2593
 
2520
- // Convert to percentages for zoom-independent positioning
2521
- const leftPercent = (rect.x / pageWidth) * 100;
2522
- const topPercent = (rect.y / pageHeight) * 100;
2523
- const widthPercent = (rect.width / pageWidth) * 100;
2524
- const heightPercent = (rect.height / pageHeight) * 100;
2594
+ // Convert to percentages for zoom-independent positioning
2595
+ const leftPercent = (rect.x / pageWidth) * 100;
2596
+ const topPercent = (rect.y / pageHeight) * 100;
2597
+ const widthPercent = (rect.width / pageWidth) * 100;
2598
+ const heightPercent = (rect.height / pageHeight) * 100;
2525
2599
 
2526
- div.style.cssText = `
2600
+ div.style.cssText = `
2527
2601
  left: ${leftPercent}%;
2528
2602
  top: ${topPercent}%;
2529
2603
  width: ${widthPercent}%;
@@ -2531,107 +2605,107 @@
2531
2605
  background: ${color};
2532
2606
  opacity: 0.35;
2533
2607
  `;
2534
- highlightContainer.appendChild(div);
2535
- });
2608
+ highlightContainer.appendChild(div);
2609
+ });
2536
2610
 
2537
- // Save to annotations store
2538
- const pageNum = parseInt(pageDiv.dataset.pageNumber);
2539
- saveTextHighlights(pageNum, pageDiv);
2540
- }
2611
+ // Save to annotations store
2612
+ const pageNum = parseInt(pageDiv.dataset.pageNumber);
2613
+ saveTextHighlights(pageNum, pageDiv);
2614
+ }
2541
2615
 
2542
- function saveTextHighlights(pageNum, pageDiv) {
2543
- const container = pageDiv.querySelector('.textHighlightContainer');
2544
- if (container) {
2545
- const key = `textHighlight_${pageNum}`;
2546
- localStorage.setItem(key, container.innerHTML);
2616
+ function saveTextHighlights(pageNum, pageDiv) {
2617
+ const container = pageDiv.querySelector('.textHighlightContainer');
2618
+ if (container) {
2619
+ const key = `textHighlight_${pageNum}`;
2620
+ localStorage.setItem(key, container.innerHTML);
2621
+ }
2547
2622
  }
2548
- }
2549
2623
 
2550
- function loadTextHighlights(pageNum, pageDiv) {
2551
- const key = `textHighlight_${pageNum}`;
2552
- const saved = localStorage.getItem(key);
2553
- if (saved) {
2554
- let container = pageDiv.querySelector('.textHighlightContainer');
2555
- if (!container) {
2556
- container = document.createElement('div');
2557
- container.className = 'textHighlightContainer';
2558
- container.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
2559
- pageDiv.insertBefore(container, pageDiv.firstChild);
2624
+ function loadTextHighlights(pageNum, pageDiv) {
2625
+ const key = `textHighlight_${pageNum}`;
2626
+ const saved = localStorage.getItem(key);
2627
+ if (saved) {
2628
+ let container = pageDiv.querySelector('.textHighlightContainer');
2629
+ if (!container) {
2630
+ container = document.createElement('div');
2631
+ container.className = 'textHighlightContainer';
2632
+ container.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
2633
+ pageDiv.insertBefore(container, pageDiv.firstChild);
2634
+ }
2635
+ container.innerHTML = saved;
2560
2636
  }
2561
- container.innerHTML = saved;
2562
2637
  }
2563
- }
2564
2638
 
2565
- function showHighlightPopup(x, y, pageDiv, rects) {
2566
- removeHighlightPopup();
2639
+ function showHighlightPopup(x, y, pageDiv, rects) {
2640
+ removeHighlightPopup();
2567
2641
 
2568
- highlightPopup = document.createElement('div');
2569
- highlightPopup.className = 'highlightPopup';
2570
- highlightPopup.style.left = x + 'px';
2571
- highlightPopup.style.top = (y + 10) + 'px';
2642
+ highlightPopup = document.createElement('div');
2643
+ highlightPopup.className = 'highlightPopup';
2644
+ highlightPopup.style.left = x + 'px';
2645
+ highlightPopup.style.top = (y + 10) + 'px';
2646
+
2647
+ const colors = ['#fff100', '#16c60c', '#00b7c3', '#0078d4', '#886ce4', '#e81224'];
2648
+ colors.forEach(color => {
2649
+ const btn = document.createElement('button');
2650
+ btn.style.background = color;
2651
+ btn.title = 'Vurgula';
2652
+ btn.onclick = (e) => {
2653
+ e.stopPropagation();
2654
+ createTextHighlights(pageDiv, rects, color);
2655
+ window.getSelection().removeAllRanges();
2656
+ removeHighlightPopup();
2657
+ };
2658
+ highlightPopup.appendChild(btn);
2659
+ });
2572
2660
 
2573
- const colors = ['#fff100', '#16c60c', '#00b7c3', '#0078d4', '#886ce4', '#e81224'];
2574
- colors.forEach(color => {
2575
- const btn = document.createElement('button');
2576
- btn.style.background = color;
2577
- btn.title = 'Vurgula';
2578
- btn.onclick = (e) => {
2579
- e.stopPropagation();
2580
- createTextHighlights(pageDiv, rects, color);
2581
- window.getSelection().removeAllRanges();
2582
- removeHighlightPopup();
2583
- };
2584
- highlightPopup.appendChild(btn);
2585
- });
2661
+ document.body.appendChild(highlightPopup);
2662
+ }
2586
2663
 
2587
- document.body.appendChild(highlightPopup);
2588
- }
2664
+ // Listen for text selection
2665
+ document.addEventListener('mouseup', (e) => {
2666
+ // Small delay to let selection finalize
2667
+ setTimeout(() => {
2668
+ const selData = getSelectionRects();
2669
+ if (selData && selData.relativeRects.length > 0) {
2670
+ const lastRect = selData.lastRect;
2671
+ showHighlightPopup(lastRect.right, lastRect.bottom, selData.pageDiv, selData.relativeRects);
2672
+ } else {
2673
+ removeHighlightPopup();
2674
+ }
2675
+ }, 10);
2676
+ });
2589
2677
 
2590
- // Listen for text selection
2591
- document.addEventListener('mouseup', (e) => {
2592
- // Small delay to let selection finalize
2593
- setTimeout(() => {
2594
- const selData = getSelectionRects();
2595
- if (selData && selData.relativeRects.length > 0) {
2596
- const lastRect = selData.lastRect;
2597
- showHighlightPopup(lastRect.right, lastRect.bottom, selData.pageDiv, selData.relativeRects);
2598
- } else {
2678
+ // Remove popup on click elsewhere
2679
+ document.addEventListener('mousedown', (e) => {
2680
+ if (highlightPopup && !highlightPopup.contains(e.target)) {
2599
2681
  removeHighlightPopup();
2600
2682
  }
2601
- }, 10);
2602
- });
2683
+ });
2603
2684
 
2604
- // Remove popup on click elsewhere
2605
- document.addEventListener('mousedown', (e) => {
2606
- if (highlightPopup && !highlightPopup.contains(e.target)) {
2607
- removeHighlightPopup();
2608
- }
2609
- });
2685
+ // Load text highlights when pages render
2686
+ eventBus.on('pagerendered', (evt) => {
2687
+ const pageDiv = pdfViewer.getPageView(evt.pageNumber - 1)?.div;
2688
+ if (pageDiv) {
2689
+ loadTextHighlights(evt.pageNumber, pageDiv);
2690
+ }
2691
+ });
2610
2692
 
2611
- // Load text highlights when pages render
2612
- eventBus.on('pagerendered', (evt) => {
2613
- const pageDiv = pdfViewer.getPageView(evt.pageNumber - 1)?.div;
2614
- if (pageDiv) {
2615
- loadTextHighlights(evt.pageNumber, pageDiv);
2616
- }
2617
- });
2618
-
2619
- // ==========================================
2620
- // SELECT/MOVE TOOL (Fixed + Touch Support)
2621
- // ==========================================
2622
- let selectedAnnotation = null;
2623
- let selectedSvg = null;
2624
- let selectedPageNum = null;
2625
- let copiedAnnotation = null;
2626
- let copiedPageNum = null;
2627
- let isDraggingAnnotation = false;
2628
- let annotationDragStartX = 0;
2629
- let annotationDragStartY = 0;
2630
-
2631
- // Create selection toolbar for touch devices
2632
- const selectionToolbar = document.createElement('div');
2633
- selectionToolbar.className = 'selection-toolbar';
2634
- selectionToolbar.innerHTML = `
2693
+ // ==========================================
2694
+ // SELECT/MOVE TOOL (Fixed + Touch Support)
2695
+ // ==========================================
2696
+ let selectedAnnotation = null;
2697
+ let selectedSvg = null;
2698
+ let selectedPageNum = null;
2699
+ let copiedAnnotation = null;
2700
+ let copiedPageNum = null;
2701
+ let isDraggingAnnotation = false;
2702
+ let annotationDragStartX = 0;
2703
+ let annotationDragStartY = 0;
2704
+
2705
+ // Create selection toolbar for touch devices
2706
+ const selectionToolbar = document.createElement('div');
2707
+ selectionToolbar.className = 'selection-toolbar';
2708
+ selectionToolbar.innerHTML = `
2635
2709
  <button data-action="copy" title="Kopyala (Ctrl+C)">
2636
2710
  <svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
2637
2711
  <span>Kopyala</span>
@@ -2645,355 +2719,355 @@
2645
2719
  <span>Sil</span>
2646
2720
  </button>
2647
2721
  `;
2648
- document.body.appendChild(selectionToolbar);
2649
-
2650
- // Selection toolbar event handlers
2651
- selectionToolbar.addEventListener('click', (e) => {
2652
- const btn = e.target.closest('button');
2653
- if (!btn) return;
2654
-
2655
- const action = btn.dataset.action;
2656
- if (action === 'copy') {
2657
- copySelectedAnnotation();
2658
- showToast('Kopyalandı!');
2659
- } else if (action === 'duplicate') {
2660
- copySelectedAnnotation();
2661
- pasteAnnotation();
2662
- showToast('Çoğaltıldı!');
2663
- } else if (action === 'delete') {
2664
- deleteSelectedAnnotation();
2665
- showToast('Silindi!');
2666
- }
2667
- });
2722
+ document.body.appendChild(selectionToolbar);
2723
+
2724
+ // Selection toolbar event handlers
2725
+ selectionToolbar.addEventListener('click', (e) => {
2726
+ const btn = e.target.closest('button');
2727
+ if (!btn) return;
2728
+
2729
+ const action = btn.dataset.action;
2730
+ if (action === 'copy') {
2731
+ copySelectedAnnotation();
2732
+ showToast('Kopyalandı!');
2733
+ } else if (action === 'duplicate') {
2734
+ copySelectedAnnotation();
2735
+ pasteAnnotation();
2736
+ showToast('Çoğaltıldı!');
2737
+ } else if (action === 'delete') {
2738
+ deleteSelectedAnnotation();
2739
+ showToast('Silindi!');
2740
+ }
2741
+ });
2668
2742
 
2669
- function showToast(message) {
2670
- const existingToast = document.querySelector('.toast-notification');
2671
- if (existingToast) existingToast.remove();
2743
+ function showToast(message) {
2744
+ const existingToast = document.querySelector('.toast-notification');
2745
+ if (existingToast) existingToast.remove();
2672
2746
 
2673
- const toast = document.createElement('div');
2674
- toast.className = 'toast-notification';
2675
- toast.textContent = message;
2676
- document.body.appendChild(toast);
2677
- setTimeout(() => toast.remove(), 2000);
2678
- }
2747
+ const toast = document.createElement('div');
2748
+ toast.className = 'toast-notification';
2749
+ toast.textContent = message;
2750
+ document.body.appendChild(toast);
2751
+ setTimeout(() => toast.remove(), 2000);
2752
+ }
2679
2753
 
2680
- function updateSelectionToolbar() {
2681
- if (selectedAnnotation && currentTool === 'select') {
2682
- selectionToolbar.classList.add('visible');
2683
- } else {
2684
- selectionToolbar.classList.remove('visible');
2754
+ function updateSelectionToolbar() {
2755
+ if (selectedAnnotation && currentTool === 'select') {
2756
+ selectionToolbar.classList.add('visible');
2757
+ } else {
2758
+ selectionToolbar.classList.remove('visible');
2759
+ }
2685
2760
  }
2686
- }
2687
2761
 
2688
- function clearAnnotationSelection() {
2689
- if (selectedAnnotation) {
2690
- selectedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
2762
+ function clearAnnotationSelection() {
2763
+ if (selectedAnnotation) {
2764
+ selectedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
2765
+ }
2766
+ selectedAnnotation = null;
2767
+ selectedSvg = null;
2768
+ selectedPageNum = null;
2769
+ isDraggingAnnotation = false;
2770
+ updateSelectionToolbar();
2691
2771
  }
2692
- selectedAnnotation = null;
2693
- selectedSvg = null;
2694
- selectedPageNum = null;
2695
- isDraggingAnnotation = false;
2696
- updateSelectionToolbar();
2697
- }
2698
2772
 
2699
- function selectAnnotation(element, svg, pageNum) {
2700
- clearAnnotationSelection();
2701
- selectedAnnotation = element;
2702
- selectedSvg = svg;
2703
- selectedPageNum = pageNum;
2704
- element.classList.add('annotation-selected', 'just-selected');
2773
+ function selectAnnotation(element, svg, pageNum) {
2774
+ clearAnnotationSelection();
2775
+ selectedAnnotation = element;
2776
+ selectedSvg = svg;
2777
+ selectedPageNum = pageNum;
2778
+ element.classList.add('annotation-selected', 'just-selected');
2705
2779
 
2706
- // Remove pulse animation after it completes
2707
- setTimeout(() => {
2708
- element.classList.remove('just-selected');
2709
- }, 600);
2780
+ // Remove pulse animation after it completes
2781
+ setTimeout(() => {
2782
+ element.classList.remove('just-selected');
2783
+ }, 600);
2710
2784
 
2711
- updateSelectionToolbar();
2712
- }
2785
+ updateSelectionToolbar();
2786
+ }
2713
2787
 
2714
- function deleteSelectedAnnotation() {
2715
- if (selectedAnnotation && selectedSvg) {
2716
- selectedAnnotation.remove();
2717
- saveAnnotations(selectedPageNum);
2718
- clearAnnotationSelection();
2788
+ function deleteSelectedAnnotation() {
2789
+ if (selectedAnnotation && selectedSvg) {
2790
+ selectedAnnotation.remove();
2791
+ saveAnnotations(selectedPageNum);
2792
+ clearAnnotationSelection();
2793
+ }
2719
2794
  }
2720
- }
2721
2795
 
2722
- function copySelectedAnnotation() {
2723
- if (selectedAnnotation) {
2724
- copiedAnnotation = selectedAnnotation.cloneNode(true);
2725
- copiedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
2726
- copiedPageNum = selectedPageNum;
2796
+ function copySelectedAnnotation() {
2797
+ if (selectedAnnotation) {
2798
+ copiedAnnotation = selectedAnnotation.cloneNode(true);
2799
+ copiedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
2800
+ copiedPageNum = selectedPageNum;
2801
+ }
2727
2802
  }
2728
- }
2729
2803
 
2730
- function pasteAnnotation() {
2731
- if (!copiedAnnotation || !pdfViewer) return;
2804
+ function pasteAnnotation() {
2805
+ if (!copiedAnnotation || !pdfViewer) return;
2732
2806
 
2733
- // Paste to current page
2734
- const currentPage = pdfViewer.currentPageNumber;
2735
- const pageView = pdfViewer.getPageView(currentPage - 1);
2736
- const svg = pageView?.div?.querySelector('.annotationLayer');
2807
+ // Paste to current page
2808
+ const currentPage = pdfViewer.currentPageNumber;
2809
+ const pageView = pdfViewer.getPageView(currentPage - 1);
2810
+ const svg = pageView?.div?.querySelector('.annotationLayer');
2737
2811
 
2738
- if (svg) {
2739
- const cloned = copiedAnnotation.cloneNode(true);
2740
- const offset = 30; // Offset amount for pasted elements
2812
+ if (svg) {
2813
+ const cloned = copiedAnnotation.cloneNode(true);
2814
+ const offset = 30; // Offset amount for pasted elements
2741
2815
 
2742
- // Offset pasted element slightly
2743
- if (cloned.tagName === 'path') {
2744
- // For paths, add/update transform translate
2745
- const currentTransform = cloned.getAttribute('transform') || '';
2746
- const match = currentTransform.match(/translate\(([^,]+),([^)]+)\)/);
2747
- let tx = offset, ty = offset;
2748
- if (match) {
2749
- tx = parseFloat(match[1]) + offset;
2750
- ty = parseFloat(match[2]) + offset;
2816
+ // Offset pasted element slightly
2817
+ if (cloned.tagName === 'path') {
2818
+ // For paths, add/update transform translate
2819
+ const currentTransform = cloned.getAttribute('transform') || '';
2820
+ const match = currentTransform.match(/translate\(([^,]+),([^)]+)\)/);
2821
+ let tx = offset, ty = offset;
2822
+ if (match) {
2823
+ tx = parseFloat(match[1]) + offset;
2824
+ ty = parseFloat(match[2]) + offset;
2825
+ }
2826
+ cloned.setAttribute('transform', `translate(${tx}, ${ty})`);
2827
+ } else if (cloned.tagName === 'rect') {
2828
+ cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
2829
+ cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
2830
+ } else if (cloned.tagName === 'ellipse') {
2831
+ cloned.setAttribute('cx', parseFloat(cloned.getAttribute('cx')) + offset);
2832
+ cloned.setAttribute('cy', parseFloat(cloned.getAttribute('cy')) + offset);
2833
+ } else if (cloned.tagName === 'line') {
2834
+ cloned.setAttribute('x1', parseFloat(cloned.getAttribute('x1')) + offset);
2835
+ cloned.setAttribute('y1', parseFloat(cloned.getAttribute('y1')) + offset);
2836
+ cloned.setAttribute('x2', parseFloat(cloned.getAttribute('x2')) + offset);
2837
+ cloned.setAttribute('y2', parseFloat(cloned.getAttribute('y2')) + offset);
2838
+ } else if (cloned.tagName === 'text') {
2839
+ cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
2840
+ cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
2751
2841
  }
2752
- cloned.setAttribute('transform', `translate(${tx}, ${ty})`);
2753
- } else if (cloned.tagName === 'rect') {
2754
- cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
2755
- cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
2756
- } else if (cloned.tagName === 'ellipse') {
2757
- cloned.setAttribute('cx', parseFloat(cloned.getAttribute('cx')) + offset);
2758
- cloned.setAttribute('cy', parseFloat(cloned.getAttribute('cy')) + offset);
2759
- } else if (cloned.tagName === 'line') {
2760
- cloned.setAttribute('x1', parseFloat(cloned.getAttribute('x1')) + offset);
2761
- cloned.setAttribute('y1', parseFloat(cloned.getAttribute('y1')) + offset);
2762
- cloned.setAttribute('x2', parseFloat(cloned.getAttribute('x2')) + offset);
2763
- cloned.setAttribute('y2', parseFloat(cloned.getAttribute('y2')) + offset);
2764
- } else if (cloned.tagName === 'text') {
2765
- cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
2766
- cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
2767
- }
2768
-
2769
- svg.appendChild(cloned);
2770
- saveAnnotations(currentPage);
2771
- selectAnnotation(cloned, svg, currentPage);
2772
- }
2773
- }
2774
2842
 
2775
- // Get coordinates from mouse or touch event
2776
- function getEventCoords(e) {
2777
- if (e.touches && e.touches.length > 0) {
2778
- return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
2843
+ svg.appendChild(cloned);
2844
+ saveAnnotations(currentPage);
2845
+ selectAnnotation(cloned, svg, currentPage);
2846
+ }
2779
2847
  }
2780
- if (e.changedTouches && e.changedTouches.length > 0) {
2781
- return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY };
2848
+
2849
+ // Get coordinates from mouse or touch event
2850
+ function getEventCoords(e) {
2851
+ if (e.touches && e.touches.length > 0) {
2852
+ return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
2853
+ }
2854
+ if (e.changedTouches && e.changedTouches.length > 0) {
2855
+ return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY };
2856
+ }
2857
+ return { clientX: e.clientX, clientY: e.clientY };
2782
2858
  }
2783
- return { clientX: e.clientX, clientY: e.clientY };
2784
- }
2785
2859
 
2786
- // Handle select tool events (both mouse and touch)
2787
- function handleSelectPointerDown(e, svg, pageNum) {
2788
- if (currentTool !== 'select') return false;
2860
+ // Handle select tool events (both mouse and touch)
2861
+ function handleSelectPointerDown(e, svg, pageNum) {
2862
+ if (currentTool !== 'select') return false;
2789
2863
 
2790
- const coords = getEventCoords(e);
2791
- const target = e.target;
2864
+ const coords = getEventCoords(e);
2865
+ const target = e.target;
2792
2866
 
2793
- if (target === svg || target.tagName === 'svg') {
2794
- // Clicked on empty area - deselect
2795
- clearAnnotationSelection();
2796
- return true;
2797
- }
2867
+ if (target === svg || target.tagName === 'svg') {
2868
+ // Clicked on empty area - deselect
2869
+ clearAnnotationSelection();
2870
+ return true;
2871
+ }
2798
2872
 
2799
- // Check if clicked on an annotation element
2800
- if (target.closest('.annotationLayer') && target !== svg) {
2801
- e.preventDefault();
2802
- e.stopPropagation();
2873
+ // Check if clicked on an annotation element
2874
+ if (target.closest('.annotationLayer') && target !== svg) {
2875
+ e.preventDefault();
2876
+ e.stopPropagation();
2803
2877
 
2804
- selectAnnotation(target, svg, pageNum);
2878
+ selectAnnotation(target, svg, pageNum);
2805
2879
 
2806
- // Start drag
2807
- const rect = svg.getBoundingClientRect();
2808
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2809
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2810
- const scaleX = viewBoxWidth / rect.width;
2811
- const scaleY = viewBoxHeight / rect.height;
2880
+ // Start drag
2881
+ const rect = svg.getBoundingClientRect();
2882
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2883
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2884
+ const scaleX = viewBoxWidth / rect.width;
2885
+ const scaleY = viewBoxHeight / rect.height;
2812
2886
 
2813
- isDraggingAnnotation = true;
2814
- annotationDragStartX = coords.clientX;
2815
- annotationDragStartY = coords.clientY;
2887
+ isDraggingAnnotation = true;
2888
+ annotationDragStartX = coords.clientX;
2889
+ annotationDragStartY = coords.clientY;
2816
2890
 
2817
- target.classList.add('annotation-dragging');
2891
+ target.classList.add('annotation-dragging');
2818
2892
 
2819
- function onMove(ev) {
2820
- if (!isDraggingAnnotation) return;
2821
- ev.preventDefault();
2893
+ function onMove(ev) {
2894
+ if (!isDraggingAnnotation) return;
2895
+ ev.preventDefault();
2822
2896
 
2823
- const moveCoords = getEventCoords(ev);
2824
- const dx = (moveCoords.clientX - annotationDragStartX) * scaleX;
2825
- const dy = (moveCoords.clientY - annotationDragStartY) * scaleY;
2897
+ const moveCoords = getEventCoords(ev);
2898
+ const dx = (moveCoords.clientX - annotationDragStartX) * scaleX;
2899
+ const dy = (moveCoords.clientY - annotationDragStartY) * scaleY;
2826
2900
 
2827
- // Move the element
2828
- moveAnnotation(target, dx, dy);
2901
+ // Move the element
2902
+ moveAnnotation(target, dx, dy);
2829
2903
 
2830
- // Update start position for next move (CRITICAL FIX)
2831
- annotationDragStartX = moveCoords.clientX;
2832
- annotationDragStartY = moveCoords.clientY;
2833
- }
2904
+ // Update start position for next move (CRITICAL FIX)
2905
+ annotationDragStartX = moveCoords.clientX;
2906
+ annotationDragStartY = moveCoords.clientY;
2907
+ }
2834
2908
 
2835
- function onEnd(ev) {
2836
- document.removeEventListener('mousemove', onMove);
2837
- document.removeEventListener('mouseup', onEnd);
2838
- document.removeEventListener('touchmove', onMove);
2839
- document.removeEventListener('touchend', onEnd);
2840
- document.removeEventListener('touchcancel', onEnd);
2909
+ function onEnd(ev) {
2910
+ document.removeEventListener('mousemove', onMove);
2911
+ document.removeEventListener('mouseup', onEnd);
2912
+ document.removeEventListener('touchmove', onMove);
2913
+ document.removeEventListener('touchend', onEnd);
2914
+ document.removeEventListener('touchcancel', onEnd);
2841
2915
 
2842
- target.classList.remove('annotation-dragging');
2843
- isDraggingAnnotation = false;
2844
- saveAnnotations(pageNum);
2916
+ target.classList.remove('annotation-dragging');
2917
+ isDraggingAnnotation = false;
2918
+ saveAnnotations(pageNum);
2919
+ }
2920
+
2921
+ document.addEventListener('mousemove', onMove, { passive: false });
2922
+ document.addEventListener('mouseup', onEnd);
2923
+ document.addEventListener('touchmove', onMove, { passive: false });
2924
+ document.addEventListener('touchend', onEnd);
2925
+ document.addEventListener('touchcancel', onEnd);
2926
+
2927
+ return true;
2845
2928
  }
2846
2929
 
2847
- document.addEventListener('mousemove', onMove, { passive: false });
2848
- document.addEventListener('mouseup', onEnd);
2849
- document.addEventListener('touchmove', onMove, { passive: false });
2850
- document.addEventListener('touchend', onEnd);
2851
- document.addEventListener('touchcancel', onEnd);
2930
+ return false;
2931
+ }
2852
2932
 
2853
- return true;
2933
+ // moveAnnotation - applies delta movement to an annotation element
2934
+ function moveAnnotation(element, dx, dy) {
2935
+ if (element.tagName === 'path') {
2936
+ // Transform path using translate
2937
+ const currentTransform = element.getAttribute('transform') || '';
2938
+ const match = currentTransform.match(/translate\(([^,]+),\s*([^)]+)\)/);
2939
+ let tx = 0, ty = 0;
2940
+ if (match) {
2941
+ tx = parseFloat(match[1]);
2942
+ ty = parseFloat(match[2]);
2943
+ }
2944
+ element.setAttribute('transform', `translate(${tx + dx}, ${ty + dy})`);
2945
+ } else if (element.tagName === 'rect') {
2946
+ element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
2947
+ element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
2948
+ } else if (element.tagName === 'ellipse') {
2949
+ element.setAttribute('cx', parseFloat(element.getAttribute('cx')) + dx);
2950
+ element.setAttribute('cy', parseFloat(element.getAttribute('cy')) + dy);
2951
+ } else if (element.tagName === 'line') {
2952
+ element.setAttribute('x1', parseFloat(element.getAttribute('x1')) + dx);
2953
+ element.setAttribute('y1', parseFloat(element.getAttribute('y1')) + dy);
2954
+ element.setAttribute('x2', parseFloat(element.getAttribute('x2')) + dx);
2955
+ element.setAttribute('y2', parseFloat(element.getAttribute('y2')) + dy);
2956
+ } else if (element.tagName === 'text') {
2957
+ element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
2958
+ element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
2959
+ }
2854
2960
  }
2855
2961
 
2856
- return false;
2857
- }
2858
-
2859
- // moveAnnotation - applies delta movement to an annotation element
2860
- function moveAnnotation(element, dx, dy) {
2861
- if (element.tagName === 'path') {
2862
- // Transform path using translate
2863
- const currentTransform = element.getAttribute('transform') || '';
2864
- const match = currentTransform.match(/translate\(([^,]+),\s*([^)]+)\)/);
2865
- let tx = 0, ty = 0;
2866
- if (match) {
2867
- tx = parseFloat(match[1]);
2868
- ty = parseFloat(match[2]);
2869
- }
2870
- element.setAttribute('transform', `translate(${tx + dx}, ${ty + dy})`);
2871
- } else if (element.tagName === 'rect') {
2872
- element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
2873
- element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
2874
- } else if (element.tagName === 'ellipse') {
2875
- element.setAttribute('cx', parseFloat(element.getAttribute('cx')) + dx);
2876
- element.setAttribute('cy', parseFloat(element.getAttribute('cy')) + dy);
2877
- } else if (element.tagName === 'line') {
2878
- element.setAttribute('x1', parseFloat(element.getAttribute('x1')) + dx);
2879
- element.setAttribute('y1', parseFloat(element.getAttribute('y1')) + dy);
2880
- element.setAttribute('x2', parseFloat(element.getAttribute('x2')) + dx);
2881
- element.setAttribute('y2', parseFloat(element.getAttribute('y2')) + dy);
2882
- } else if (element.tagName === 'text') {
2883
- element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
2884
- element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
2962
+ // Legacy function for backwards compatibility (used elsewhere)
2963
+ function handleSelectMouseDown(e, svg, pageNum) {
2964
+ return handleSelectPointerDown(e, svg, pageNum);
2885
2965
  }
2886
- }
2887
2966
 
2888
- // Legacy function for backwards compatibility (used elsewhere)
2889
- function handleSelectMouseDown(e, svg, pageNum) {
2890
- return handleSelectPointerDown(e, svg, pageNum);
2891
- }
2967
+ // ==========================================
2968
+ // KEYBOARD SHORTCUTS
2969
+ // ==========================================
2970
+ document.addEventListener('keydown', (e) => {
2971
+ // Ignore if typing in input
2972
+ if (e.target.tagName === 'INPUT' || e.target.contentEditable === 'true') return;
2892
2973
 
2893
- // ==========================================
2894
- // KEYBOARD SHORTCUTS
2895
- // ==========================================
2896
- document.addEventListener('keydown', (e) => {
2897
- // Ignore if typing in input
2898
- if (e.target.tagName === 'INPUT' || e.target.contentEditable === 'true') return;
2974
+ const key = e.key.toLowerCase();
2899
2975
 
2900
- const key = e.key.toLowerCase();
2976
+ // Tool shortcuts
2977
+ if (key === 'h') { setTool('highlight'); e.preventDefault(); }
2978
+ if (key === 'p') { setTool('pen'); e.preventDefault(); }
2979
+ if (key === 'e') { setTool('eraser'); e.preventDefault(); }
2980
+ if (key === 't') { setTool('text'); e.preventDefault(); }
2981
+ if (key === 'r') { setTool('shape'); e.preventDefault(); }
2982
+ if (key === 'v') { setTool('select'); e.preventDefault(); }
2901
2983
 
2902
- // Tool shortcuts
2903
- if (key === 'h') { setTool('highlight'); e.preventDefault(); }
2904
- if (key === 'p') { setTool('pen'); e.preventDefault(); }
2905
- if (key === 'e') { setTool('eraser'); e.preventDefault(); }
2906
- if (key === 't') { setTool('text'); e.preventDefault(); }
2907
- if (key === 'r') { setTool('shape'); e.preventDefault(); }
2908
- if (key === 'v') { setTool('select'); e.preventDefault(); }
2984
+ // Delete selected annotation
2985
+ if ((key === 'delete' || key === 'backspace') && selectedAnnotation) {
2986
+ deleteSelectedAnnotation();
2987
+ e.preventDefault();
2988
+ }
2909
2989
 
2910
- // Delete selected annotation
2911
- if ((key === 'delete' || key === 'backspace') && selectedAnnotation) {
2912
- deleteSelectedAnnotation();
2913
- e.preventDefault();
2914
- }
2990
+ // Copy/Paste annotations
2991
+ if ((e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
2992
+ copySelectedAnnotation();
2993
+ e.preventDefault();
2994
+ }
2995
+ if ((e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
2996
+ pasteAnnotation();
2997
+ e.preventDefault();
2998
+ }
2915
2999
 
2916
- // Copy/Paste annotations
2917
- if ((e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
2918
- copySelectedAnnotation();
2919
- e.preventDefault();
2920
- }
2921
- if ((e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
2922
- pasteAnnotation();
2923
- e.preventDefault();
2924
- }
3000
+ // Navigation
3001
+ if (key === 's') {
3002
+ document.getElementById('sidebarBtn').click();
3003
+ e.preventDefault();
3004
+ }
2925
3005
 
2926
- // Navigation
2927
- if (key === 's') {
2928
- document.getElementById('sidebarBtn').click();
2929
- e.preventDefault();
2930
- }
3006
+ // Arrow key navigation
3007
+ if (key === 'arrowleft' || key === 'arrowup') {
3008
+ if (pdfViewer && pdfViewer.currentPageNumber > 1) {
3009
+ pdfViewer.currentPageNumber--;
3010
+ }
3011
+ e.preventDefault();
3012
+ }
3013
+ if (key === 'arrowright' || key === 'arrowdown') {
3014
+ if (pdfViewer && pdfViewer.currentPageNumber < pdfViewer.pagesCount) {
3015
+ pdfViewer.currentPageNumber++;
3016
+ }
3017
+ e.preventDefault();
3018
+ }
2931
3019
 
2932
- // Arrow key navigation
2933
- if (key === 'arrowleft' || key === 'arrowup') {
2934
- if (pdfViewer && pdfViewer.currentPageNumber > 1) {
2935
- pdfViewer.currentPageNumber--;
3020
+ // Home/End
3021
+ if (key === 'home') {
3022
+ if (pdfViewer) pdfViewer.currentPageNumber = 1;
3023
+ e.preventDefault();
2936
3024
  }
2937
- e.preventDefault();
2938
- }
2939
- if (key === 'arrowright' || key === 'arrowdown') {
2940
- if (pdfViewer && pdfViewer.currentPageNumber < pdfViewer.pagesCount) {
2941
- pdfViewer.currentPageNumber++;
3025
+ if (key === 'end') {
3026
+ if (pdfViewer) pdfViewer.currentPageNumber = pdfViewer.pagesCount;
3027
+ e.preventDefault();
2942
3028
  }
2943
- e.preventDefault();
2944
- }
2945
3029
 
2946
- // Home/End
2947
- if (key === 'home') {
2948
- if (pdfViewer) pdfViewer.currentPageNumber = 1;
2949
- e.preventDefault();
2950
- }
2951
- if (key === 'end') {
2952
- if (pdfViewer) pdfViewer.currentPageNumber = pdfViewer.pagesCount;
2953
- e.preventDefault();
2954
- }
3030
+ // Zoom shortcuts - prevent browser zoom
3031
+ if ((e.ctrlKey || e.metaKey) && (key === '=' || key === '+' || e.code === 'Equal')) {
3032
+ e.preventDefault();
3033
+ e.stopPropagation();
3034
+ pdfViewer.currentScale += 0.25;
3035
+ return;
3036
+ }
3037
+ if ((e.ctrlKey || e.metaKey) && (key === '-' || e.code === 'Minus')) {
3038
+ e.preventDefault();
3039
+ e.stopPropagation();
3040
+ pdfViewer.currentScale -= 0.25;
3041
+ return;
3042
+ }
3043
+ if ((e.ctrlKey || e.metaKey) && (key === '0' || e.code === 'Digit0')) {
3044
+ e.preventDefault();
3045
+ e.stopPropagation();
3046
+ pdfViewer.currentScaleValue = 'page-width';
3047
+ return;
3048
+ }
2955
3049
 
2956
- // Zoom shortcuts - prevent browser zoom
2957
- if ((e.ctrlKey || e.metaKey) && (key === '=' || key === '+' || e.code === 'Equal')) {
2958
- e.preventDefault();
2959
- e.stopPropagation();
2960
- pdfViewer.currentScale += 0.25;
2961
- return;
2962
- }
2963
- if ((e.ctrlKey || e.metaKey) && (key === '-' || e.code === 'Minus')) {
2964
- e.preventDefault();
2965
- e.stopPropagation();
2966
- pdfViewer.currentScale -= 0.25;
2967
- return;
2968
- }
2969
- if ((e.ctrlKey || e.metaKey) && (key === '0' || e.code === 'Digit0')) {
2970
- e.preventDefault();
2971
- e.stopPropagation();
2972
- pdfViewer.currentScaleValue = 'page-width';
2973
- return;
2974
- }
3050
+ // Escape to deselect tool
3051
+ if (key === 'escape') {
3052
+ if (currentTool) {
3053
+ setTool(currentTool); // Toggle off
3054
+ }
3055
+ closeAllDropdowns();
3056
+ }
2975
3057
 
2976
- // Escape to deselect tool
2977
- if (key === 'escape') {
2978
- if (currentTool) {
2979
- setTool(currentTool); // Toggle off
3058
+ // Sepia mode
3059
+ if (key === 'm') {
3060
+ document.getElementById('sepiaBtn').click();
3061
+ e.preventDefault();
2980
3062
  }
2981
- closeAllDropdowns();
2982
- }
3063
+ });
2983
3064
 
2984
- // Sepia mode
2985
- if (key === 'm') {
2986
- document.getElementById('sepiaBtn').click();
2987
- e.preventDefault();
2988
- }
2989
- });
2990
-
2991
- // ==========================================
2992
- // CONTEXT MENU (Right-click)
2993
- // ==========================================
2994
- const contextMenu = document.createElement('div');
2995
- contextMenu.className = 'contextMenu';
2996
- contextMenu.innerHTML = `
3065
+ // ==========================================
3066
+ // CONTEXT MENU (Right-click)
3067
+ // ==========================================
3068
+ const contextMenu = document.createElement('div');
3069
+ contextMenu.className = 'contextMenu';
3070
+ contextMenu.innerHTML = `
2997
3071
  <div class="contextMenuItem" data-action="highlight">
2998
3072
  <svg viewBox="0 0 24 24"><path d="M3 21h18v-2H3v2zM5 16h14l-3-10H8l-3 10z"/></svg>
2999
3073
  Vurgula
@@ -3027,185 +3101,188 @@
3027
3101
  <span class="shortcutHint">M</span>
3028
3102
  </div>
3029
3103
  `;
3030
- document.body.appendChild(contextMenu);
3031
-
3032
- // Show context menu on right-click in viewer
3033
- container.addEventListener('contextmenu', (e) => {
3034
- e.preventDefault();
3035
- contextMenu.style.left = e.clientX + 'px';
3036
- contextMenu.style.top = e.clientY + 'px';
3037
- contextMenu.classList.add('visible');
3038
- });
3039
-
3040
- // Hide context menu on click
3041
- document.addEventListener('click', () => {
3042
- contextMenu.classList.remove('visible');
3043
- });
3044
-
3045
- // Context menu actions
3046
- contextMenu.addEventListener('click', (e) => {
3047
- const item = e.target.closest('.contextMenuItem');
3048
- if (!item) return;
3049
-
3050
- const action = item.dataset.action;
3051
- switch (action) {
3052
- case 'highlight': setTool('highlight'); break;
3053
- case 'pen': setTool('pen'); break;
3054
- case 'text': setTool('text'); break;
3055
- case 'zoomIn': pdfViewer.currentScale += 0.25; break;
3056
- case 'zoomOut': pdfViewer.currentScale -= 0.25; break;
3057
- case 'sepia': document.getElementById('sepiaBtn').click(); break;
3058
- }
3059
- contextMenu.classList.remove('visible');
3060
- });
3061
-
3062
- // ==========================================
3063
- // ERGONOMIC FEATURES
3064
- // ==========================================
3065
-
3066
- // Double-click on page for fullscreen
3067
- let lastClickTime = 0;
3068
- container.addEventListener('click', (e) => {
3069
- const now = Date.now();
3070
- if (now - lastClickTime < 300) {
3071
- // Double click detected
3072
- if (document.fullscreenElement) {
3073
- document.exitFullscreen();
3074
- } else {
3075
- container.requestFullscreen().catch(() => { });
3076
- }
3077
- }
3078
- lastClickTime = now;
3079
- });
3104
+ document.body.appendChild(contextMenu);
3080
3105
 
3081
- // Mouse wheel zoom with Ctrl
3082
- container.addEventListener('wheel', (e) => {
3083
- if (e.ctrlKey) {
3106
+ // Show context menu on right-click in viewer
3107
+ container.addEventListener('contextmenu', (e) => {
3084
3108
  e.preventDefault();
3085
- if (e.deltaY < 0) {
3086
- pdfViewer.currentScale += 0.1;
3087
- } else {
3088
- pdfViewer.currentScale -= 0.1;
3089
- }
3090
- }
3091
- }, { passive: false });
3092
-
3093
- console.log('PDF Viewer Ready');
3094
- console.log('Keyboard Shortcuts: H=Highlight, P=Pen, E=Eraser, T=Text, R=Shapes, S=Sidebar, M=ReadingMode, Arrows=Navigate');
3095
-
3096
- // ==========================================
3097
- // SECURITY FEATURES
3098
- // ==========================================
3109
+ contextMenu.style.left = e.clientX + 'px';
3110
+ contextMenu.style.top = e.clientY + 'px';
3111
+ contextMenu.classList.add('visible');
3112
+ });
3099
3113
 
3100
- (function initSecurityFeatures() {
3101
- console.log('[Security] Initializing protection features...');
3114
+ // Hide context menu on click
3115
+ document.addEventListener('click', () => {
3116
+ contextMenu.classList.remove('visible');
3117
+ });
3102
3118
 
3103
- // 1. Block dangerous keyboard shortcuts
3104
- document.addEventListener('keydown', function (e) {
3105
- // Ctrl+S (Save)
3106
- if (e.ctrlKey && e.key === 's') {
3107
- e.preventDefault();
3108
- console.log('[Security] Ctrl+S blocked');
3109
- return false;
3119
+ // Context menu actions
3120
+ contextMenu.addEventListener('click', (e) => {
3121
+ const item = e.target.closest('.contextMenuItem');
3122
+ if (!item) return;
3123
+
3124
+ const action = item.dataset.action;
3125
+ switch (action) {
3126
+ case 'highlight': setTool('highlight'); break;
3127
+ case 'pen': setTool('pen'); break;
3128
+ case 'text': setTool('text'); break;
3129
+ case 'zoomIn': pdfViewer.currentScale += 0.25; break;
3130
+ case 'zoomOut': pdfViewer.currentScale -= 0.25; break;
3131
+ case 'sepia': document.getElementById('sepiaBtn').click(); break;
3110
3132
  }
3111
- // Ctrl+P (Print)
3112
- if (e.ctrlKey && e.key === 'p') {
3113
- e.preventDefault();
3114
- console.log('[Security] Ctrl+P blocked');
3115
- return false;
3116
- }
3117
- // Ctrl+Shift+S (Save As)
3118
- if (e.ctrlKey && e.shiftKey && e.key === 'S') {
3119
- e.preventDefault();
3120
- return false;
3133
+ contextMenu.classList.remove('visible');
3134
+ });
3135
+
3136
+ // ==========================================
3137
+ // ERGONOMIC FEATURES
3138
+ // ==========================================
3139
+
3140
+ // Double-click on page for fullscreen
3141
+ let lastClickTime = 0;
3142
+ container.addEventListener('click', (e) => {
3143
+ const now = Date.now();
3144
+ if (now - lastClickTime < 300) {
3145
+ // Double click detected
3146
+ if (document.fullscreenElement) {
3147
+ document.exitFullscreen();
3148
+ } else {
3149
+ container.requestFullscreen().catch(() => { });
3150
+ }
3121
3151
  }
3122
- // F12 (DevTools)
3123
- if (e.key === 'F12') {
3152
+ lastClickTime = now;
3153
+ });
3154
+
3155
+ // Mouse wheel zoom with Ctrl
3156
+ container.addEventListener('wheel', (e) => {
3157
+ if (e.ctrlKey) {
3124
3158
  e.preventDefault();
3125
- console.log('[Security] F12 blocked');
3126
- return false;
3159
+ if (e.deltaY < 0) {
3160
+ pdfViewer.currentScale += 0.1;
3161
+ } else {
3162
+ pdfViewer.currentScale -= 0.1;
3163
+ }
3127
3164
  }
3128
- // Ctrl+Shift+I (DevTools)
3129
- if (e.ctrlKey && e.shiftKey && e.key === 'I') {
3165
+ }, { passive: false });
3166
+
3167
+ console.log('PDF Viewer Ready');
3168
+ console.log('Keyboard Shortcuts: H=Highlight, P=Pen, E=Eraser, T=Text, R=Shapes, S=Sidebar, M=ReadingMode, Arrows=Navigate');
3169
+
3170
+ // ==========================================
3171
+ // SECURITY FEATURES
3172
+ // ==========================================
3173
+
3174
+ (function initSecurityFeatures() {
3175
+ console.log('[Security] Initializing protection features...');
3176
+
3177
+ // 1. Block dangerous keyboard shortcuts
3178
+ document.addEventListener('keydown', function (e) {
3179
+ // Ctrl+S (Save)
3180
+ if (e.ctrlKey && e.key === 's') {
3181
+ e.preventDefault();
3182
+ console.log('[Security] Ctrl+S blocked');
3183
+ return false;
3184
+ }
3185
+ // Ctrl+P (Print)
3186
+ if (e.ctrlKey && e.key === 'p') {
3187
+ e.preventDefault();
3188
+ console.log('[Security] Ctrl+P blocked');
3189
+ return false;
3190
+ }
3191
+ // Ctrl+Shift+S (Save As)
3192
+ if (e.ctrlKey && e.shiftKey && e.key === 'S') {
3193
+ e.preventDefault();
3194
+ return false;
3195
+ }
3196
+ // F12 (DevTools)
3197
+ if (e.key === 'F12') {
3198
+ e.preventDefault();
3199
+ console.log('[Security] F12 blocked');
3200
+ return false;
3201
+ }
3202
+ // Ctrl+Shift+I (DevTools)
3203
+ if (e.ctrlKey && e.shiftKey && e.key === 'I') {
3204
+ e.preventDefault();
3205
+ return false;
3206
+ }
3207
+ // Ctrl+Shift+J (Console)
3208
+ if (e.ctrlKey && e.shiftKey && e.key === 'J') {
3209
+ e.preventDefault();
3210
+ return false;
3211
+ }
3212
+ // Ctrl+U (View Source)
3213
+ if (e.ctrlKey && e.key === 'u') {
3214
+ e.preventDefault();
3215
+ return false;
3216
+ }
3217
+ // Ctrl+Shift+C (Inspect Element)
3218
+ if (e.ctrlKey && e.shiftKey && e.key === 'C') {
3219
+ e.preventDefault();
3220
+ return false;
3221
+ }
3222
+ }, true);
3223
+
3224
+ // 2. Block context menu (right-click) - EVERYWHERE
3225
+ document.addEventListener('contextmenu', function (e) {
3130
3226
  e.preventDefault();
3227
+ e.stopPropagation();
3131
3228
  return false;
3132
- }
3133
- // Ctrl+Shift+J (Console)
3134
- if (e.ctrlKey && e.shiftKey && e.key === 'J') {
3229
+ }, true);
3230
+
3231
+ // 3. Block copy/cut/paste
3232
+ document.addEventListener('copy', function (e) {
3135
3233
  e.preventDefault();
3234
+ console.log('[Security] Copy blocked');
3136
3235
  return false;
3137
- }
3138
- // Ctrl+U (View Source)
3139
- if (e.ctrlKey && e.key === 'u') {
3236
+ }, true);
3237
+
3238
+ document.addEventListener('cut', function (e) {
3140
3239
  e.preventDefault();
3141
3240
  return false;
3142
- }
3143
- // Ctrl+Shift+C (Inspect Element)
3144
- if (e.ctrlKey && e.shiftKey && e.key === 'C') {
3241
+ }, true);
3242
+
3243
+ // 4. Block drag events (prevent dragging content out)
3244
+ document.addEventListener('dragstart', function (e) {
3145
3245
  e.preventDefault();
3146
3246
  return false;
3147
- }
3148
- }, true);
3149
-
3150
- // 2. Block context menu (right-click) - EVERYWHERE
3151
- document.addEventListener('contextmenu', function (e) {
3152
- e.preventDefault();
3153
- e.stopPropagation();
3154
- return false;
3155
- }, true);
3247
+ }, true);
3156
3248
 
3157
- // 3. Block copy/cut/paste
3158
- document.addEventListener('copy', function (e) {
3159
- e.preventDefault();
3160
- console.log('[Security] Copy blocked');
3161
- return false;
3162
- }, true);
3163
-
3164
- document.addEventListener('cut', function (e) {
3165
- e.preventDefault();
3166
- return false;
3167
- }, true);
3168
-
3169
- // 4. Block drag events (prevent dragging content out)
3170
- document.addEventListener('dragstart', function (e) {
3171
- e.preventDefault();
3172
- return false;
3173
- }, true);
3249
+ // 5. Block Print via window.print override
3250
+ window.print = function () {
3251
+ console.log('[Security] Print function blocked');
3252
+ alert('Yazdırma bu belgede engellenmiştir.');
3253
+ return false;
3254
+ };
3174
3255
 
3175
- // 5. Block Print via window.print override
3176
- window.print = function () {
3177
- console.log('[Security] Print function blocked');
3178
- alert('Yazdırma bu belgede engellenmiştir.');
3179
- return false;
3180
- };
3256
+ // 6. Disable beforeprint event
3257
+ window.addEventListener('beforeprint', function (e) {
3258
+ e.preventDefault();
3259
+ document.body.style.display = 'none';
3260
+ });
3181
3261
 
3182
- // 6. Disable beforeprint event
3183
- window.addEventListener('beforeprint', function (e) {
3184
- e.preventDefault();
3185
- document.body.style.display = 'none';
3186
- });
3262
+ window.addEventListener('afterprint', function () {
3263
+ document.body.style.display = '';
3264
+ });
3187
3265
 
3188
- window.addEventListener('afterprint', function () {
3189
- document.body.style.display = '';
3190
- });
3266
+ // 7. Block screenshot keyboard shortcuts
3267
+ document.addEventListener('keyup', function (e) {
3268
+ // PrintScreen key
3269
+ if (e.key === 'PrintScreen') {
3270
+ navigator.clipboard.writeText('');
3271
+ console.log('[Security] PrintScreen clipboard cleared');
3272
+ }
3273
+ }, true);
3191
3274
 
3192
- // 7. Block screenshot keyboard shortcuts
3193
- document.addEventListener('keyup', function (e) {
3194
- // PrintScreen key
3195
- if (e.key === 'PrintScreen') {
3196
- navigator.clipboard.writeText('');
3197
- console.log('[Security] PrintScreen clipboard cleared');
3198
- }
3199
- }, true);
3275
+ // 8. Visibility change detection (tab switching for screenshots)
3276
+ document.addEventListener('visibilitychange', function () {
3277
+ if (document.hidden) {
3278
+ console.log('[Security] Tab hidden');
3279
+ }
3280
+ });
3200
3281
 
3201
- // 8. Visibility change detection (tab switching for screenshots)
3202
- document.addEventListener('visibilitychange', function () {
3203
- if (document.hidden) {
3204
- console.log('[Security] Tab hidden');
3205
- }
3206
- });
3282
+ console.log('[Security] All protection features initialized');
3283
+ })();
3207
3284
 
3208
- console.log('[Security] All protection features initialized');
3285
+ // End of main IIFE - pdfDoc, pdfViewer not accessible from console
3209
3286
  })();
3210
3287
  </script>
3211
3288
  </body>