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