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