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