nodebb-plugin-pdf-secure 1.2.4 → 1.2.5

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