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