nodebb-plugin-pdf-secure 1.2.9 → 1.2.11

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.
@@ -7,14 +7,1521 @@
7
7
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
8
8
  <title>PDF Viewer</title>
9
9
 
10
- <!-- PDF.js (must load before viewer-app.js) -->
10
+ <!-- PDF.js -->
11
11
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
12
- <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.js"></script>
13
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css">
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.js"></script>
14
+
15
+ <style>
16
+ /* Microsoft Edge / Dark Theme */
17
+ :root {
18
+ --bg-primary: #1f1f1f;
19
+ --bg-secondary: #2d2d2d;
20
+ --bg-tertiary: #3d3d3d;
21
+ --text-primary: #ffffff;
22
+ --text-secondary: #a0a0a0;
23
+ --accent: #0078d4;
24
+ --accent-hover: #1a86d9;
25
+ --border-color: #404040;
26
+ --toolbar-height: 48px;
27
+ --sidebar-width: 200px;
28
+ --toolbar-height-mobile: 44px;
29
+ --bottom-bar-height: 52px;
30
+ --safe-area-top: env(safe-area-inset-top, 0px);
31
+ --safe-area-bottom: env(safe-area-inset-bottom, 0px);
32
+ --safe-area-left: env(safe-area-inset-left, 0px);
33
+ --safe-area-right: env(safe-area-inset-right, 0px);
34
+ }
35
+
36
+ * {
37
+ margin: 0;
38
+ padding: 0;
39
+ box-sizing: border-box;
40
+ }
41
+
42
+ html,
43
+ body {
44
+ height: 100%;
45
+ background: var(--bg-primary);
46
+ font-family: "Segoe UI", system-ui, sans-serif;
47
+ font-size: 14px;
48
+ overflow: hidden;
49
+ color: var(--text-primary);
50
+ /* Security: prevent text selection globally */
51
+ -webkit-user-select: none;
52
+ -moz-user-select: none;
53
+ -ms-user-select: none;
54
+ user-select: none;
55
+ }
56
+
57
+ /* Print Protection - hide everything when printing */
58
+ @media print {
59
+
60
+ html,
61
+ body,
62
+ #viewerContainer,
63
+ #viewer,
64
+ .pdfViewer,
65
+ .page {
66
+ display: none !important;
67
+ visibility: hidden !important;
68
+ }
69
+
70
+ body::before {
71
+ content: 'Bu içeriğin yazdırılması engellenmiştir.' !important;
72
+ display: block !important;
73
+ font-size: 24px;
74
+ padding: 50px;
75
+ text-align: center;
76
+ color: #666;
77
+ }
78
+ }
79
+
80
+ /* Loading Spinner Animation */
81
+ @keyframes spin {
82
+ from {
83
+ transform: rotate(0deg);
84
+ }
85
+
86
+ to {
87
+ transform: rotate(360deg);
88
+ }
89
+ }
90
+
91
+ .spin {
92
+ animation: spin 1s linear infinite;
93
+ }
94
+
95
+ .dropzone svg.spin {
96
+ fill: var(--accent);
97
+ }
98
+
99
+ /* Toolbar - Edge Style */
100
+ #toolbar {
101
+ position: fixed;
102
+ top: 0;
103
+ left: 0;
104
+ right: 0;
105
+ height: var(--toolbar-height);
106
+ background: var(--bg-secondary);
107
+ border-bottom: 1px solid var(--border-color);
108
+ display: flex;
109
+ align-items: center;
110
+ padding: 0 12px;
111
+ gap: 4px;
112
+ z-index: 100;
113
+ }
114
+
115
+ .toolbarGroup {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 2px;
119
+ }
120
+
121
+ .toolbarBtn {
122
+ width: 36px;
123
+ height: 36px;
124
+ border: none;
125
+ background: transparent;
126
+ color: var(--text-primary);
127
+ border-radius: 4px;
128
+ cursor: pointer;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ transition: background 0.1s;
133
+ }
134
+
135
+ .toolbarBtn:hover {
136
+ background: var(--bg-tertiary);
137
+ }
138
+
139
+ .toolbarBtn.active {
140
+ background: var(--accent);
141
+ }
142
+
143
+ .toolbarBtn svg {
144
+ width: 18px;
145
+ height: 18px;
146
+ fill: currentColor;
147
+ }
148
+
149
+ .separator {
150
+ width: 1px;
151
+ height: 24px;
152
+ background: var(--border-color);
153
+ margin: 0 8px;
154
+ }
155
+
156
+ /* Enhanced Tooltips */
157
+ .toolbarBtn {
158
+ position: relative;
159
+ }
160
+
161
+ .toolbarBtn::after {
162
+ content: attr(data-tooltip);
163
+ position: absolute;
164
+ top: 100%;
165
+ left: 50%;
166
+ transform: translateX(-50%);
167
+ background: #1a1a1a;
168
+ color: #fff;
169
+ padding: 6px 10px;
170
+ border-radius: 6px;
171
+ font-size: 12px;
172
+ white-space: nowrap;
173
+ opacity: 0;
174
+ visibility: hidden;
175
+ transition: opacity 0.2s, visibility 0.2s;
176
+ z-index: 1000;
177
+ margin-top: 8px;
178
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
179
+ pointer-events: none;
180
+ }
181
+
182
+ .toolbarBtn::before {
183
+ content: '';
184
+ position: absolute;
185
+ top: 100%;
186
+ left: 50%;
187
+ transform: translateX(-50%);
188
+ border: 6px solid transparent;
189
+ border-bottom-color: #1a1a1a;
190
+ opacity: 0;
191
+ visibility: hidden;
192
+ transition: opacity 0.2s, visibility 0.2s;
193
+ z-index: 1001;
194
+ margin-top: -4px;
195
+ }
196
+
197
+ .toolbarBtn:hover::after,
198
+ .toolbarBtn:hover::before {
199
+ opacity: 1;
200
+ visibility: visible;
201
+ }
202
+
203
+ .toolbarBtn .shortcut {
204
+ display: inline;
205
+ opacity: 0.6;
206
+ margin-left: 8px;
207
+ padding: 2px 5px;
208
+ background: rgba(255, 255, 255, 0.15);
209
+ border-radius: 3px;
210
+ font-size: 10px;
211
+ }
212
+
213
+ /* Context Menu */
214
+ .contextMenu {
215
+ position: fixed;
216
+ background: #2d2d2d;
217
+ border: 1px solid var(--border-color);
218
+ border-radius: 8px;
219
+ padding: 6px 0;
220
+ min-width: 180px;
221
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
222
+ z-index: 2000;
223
+ display: none;
224
+ }
225
+
226
+ .contextMenu.visible {
227
+ display: block;
228
+ }
229
+
230
+ .contextMenuItem {
231
+ padding: 10px 16px;
232
+ cursor: pointer;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 12px;
236
+ color: var(--text-primary);
237
+ font-size: 13px;
238
+ transition: background 0.1s;
239
+ }
240
+
241
+ .contextMenuItem:hover {
242
+ background: var(--bg-tertiary);
243
+ }
244
+
245
+ .contextMenuItem svg {
246
+ width: 16px;
247
+ height: 16px;
248
+ fill: currentColor;
249
+ opacity: 0.8;
250
+ }
251
+
252
+ .contextMenuItem .shortcutHint {
253
+ margin-left: auto;
254
+ opacity: 0.5;
255
+ font-size: 11px;
256
+ }
257
+
258
+ .contextMenuDivider {
259
+ height: 1px;
260
+ background: var(--border-color);
261
+ margin: 6px 0;
262
+ }
263
+
264
+ /* Tool Dropdown Panel - Microsoft Edge Style */
265
+ .toolDropdown {
266
+ position: absolute;
267
+ top: calc(var(--toolbar-height) + 4px);
268
+ background: #2d2d2d;
269
+ border-radius: 8px;
270
+ padding: 16px;
271
+ min-width: 240px;
272
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
273
+ display: none;
274
+ z-index: 200;
275
+ }
276
+
277
+ .toolDropdown.visible {
278
+ display: block;
279
+ }
280
+
281
+ .dropdownSection {
282
+ margin-bottom: 16px;
283
+ }
284
+
285
+ .dropdownSection:last-child {
286
+ margin-bottom: 0;
287
+ }
288
+
289
+ .dropdownLabel {
290
+ font-size: 13px;
291
+ font-weight: 600;
292
+ color: var(--text-primary);
293
+ margin-bottom: 12px;
294
+ display: flex;
295
+ align-items: center;
296
+ gap: 6px;
297
+ }
298
+
299
+ .dropdownLabel svg {
300
+ width: 14px;
301
+ height: 14px;
302
+ fill: var(--text-secondary);
303
+ }
304
+
305
+ /* Color Grid */
306
+ .colorGrid {
307
+ display: grid;
308
+ grid-template-columns: repeat(6, 1fr);
309
+ gap: 8px;
310
+ }
311
+
312
+ .colorDot {
313
+ width: 28px;
314
+ height: 28px;
315
+ border-radius: 50%;
316
+ border: 2px solid transparent;
317
+ cursor: pointer;
318
+ transition: transform 0.1s, border-color 0.1s;
319
+ }
320
+
321
+ .colorDot:hover {
322
+ transform: scale(1.1);
323
+ }
324
+
325
+ .colorDot.active {
326
+ border-color: var(--text-primary);
327
+ }
328
+
329
+ /* Stroke Preview Wave */
330
+ .strokePreview {
331
+ height: 50px;
332
+ background: #1f1f1f;
333
+ border-radius: 8px;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ margin-bottom: 16px;
338
+ overflow: hidden;
339
+ }
340
+
341
+ .strokePreview svg {
342
+ width: 100%;
343
+ height: 100%;
344
+ }
345
+
346
+ /* Thickness Slider */
347
+ .thicknessSlider {
348
+ width: 100%;
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 8px;
352
+ }
353
+
354
+ .thicknessSlider input[type="range"] {
355
+ -webkit-appearance: none;
356
+ appearance: none;
357
+ width: 100%;
358
+ height: 4px;
359
+ background: #555;
360
+ border-radius: 2px;
361
+ outline: none;
362
+ }
363
+
364
+ .thicknessSlider input[type="range"]::-webkit-slider-thumb {
365
+ -webkit-appearance: none;
366
+ appearance: none;
367
+ width: 16px;
368
+ height: 16px;
369
+ background: #fff;
370
+ border-radius: 50%;
371
+ cursor: pointer;
372
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
373
+ }
374
+
375
+ .thicknessSlider input[type="range"]::-moz-range-thumb {
376
+ width: 16px;
377
+ height: 16px;
378
+ background: #fff;
379
+ border-radius: 50%;
380
+ cursor: pointer;
381
+ border: none;
382
+ }
383
+
384
+ .thicknessLabels {
385
+ display: flex;
386
+ justify-content: space-between;
387
+ font-size: 12px;
388
+ color: var(--text-secondary);
389
+ }
390
+
391
+ /* Tool button with dropdown arrow */
392
+ .toolbarBtnWithDropdown {
393
+ position: relative;
394
+ display: flex;
395
+ align-items: center;
396
+ }
397
+
398
+ .toolbarBtnWithDropdown .toolbarBtn {
399
+ border-radius: 4px 0 0 4px;
400
+ }
401
+
402
+ .dropdownArrow {
403
+ width: 20px;
404
+ height: 36px;
405
+ border: none;
406
+ background: transparent;
407
+ color: var(--text-primary);
408
+ border-radius: 0 4px 4px 0;
409
+ cursor: pointer;
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ }
414
+
415
+ .dropdownArrow:hover {
416
+ background: var(--bg-tertiary);
417
+ }
418
+
419
+ .toolbarBtnWithDropdown.active .toolbarBtn,
420
+ .toolbarBtnWithDropdown.active .dropdownArrow {
421
+ background: var(--accent);
422
+ }
423
+
424
+ .dropdownArrow svg {
425
+ width: 12px;
426
+ height: 12px;
427
+ fill: currentColor;
428
+ }
429
+
430
+ /* Overflow Menu Items */
431
+ .overflowItem {
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 12px;
435
+ padding: 10px 16px;
436
+ background: none;
437
+ border: none;
438
+ color: var(--text-primary);
439
+ cursor: pointer;
440
+ width: 100%;
441
+ border-radius: 6px;
442
+ font-size: 14px;
443
+ white-space: nowrap;
444
+ }
445
+
446
+ .overflowItem:hover {
447
+ background: var(--bg-tertiary);
448
+ }
449
+
450
+ .overflowItem svg {
451
+ width: 20px;
452
+ height: 20px;
453
+ fill: currentColor;
454
+ flex-shrink: 0;
455
+ }
456
+
457
+ .overflowItem.active {
458
+ color: var(--accent);
459
+ }
460
+
461
+ .overflowDivider {
462
+ height: 1px;
463
+ background: var(--border-color);
464
+ margin: 6px 0;
465
+ }
466
+
467
+ /* Overflow: visible on all screens, originals hidden */
468
+ #overflowWrapper {
469
+ display: flex;
470
+ }
471
+
472
+ .overflowSep {
473
+ display: block;
474
+ }
475
+
476
+ /* Hide rotate, sepia and their separators (children 3-8 of view group) */
477
+ .toolbarGroup:nth-child(5)> :nth-child(3),
478
+ .toolbarGroup:nth-child(5)> :nth-child(4),
479
+ .toolbarGroup:nth-child(5)> :nth-child(5),
480
+ .toolbarGroup:nth-child(5)> :nth-child(6),
481
+ .toolbarGroup:nth-child(5)> :nth-child(7),
482
+ .toolbarGroup:nth-child(5)> :nth-child(8) {
483
+ display: none !important;
484
+ }
485
+
486
+ /* Shape Grid */
487
+ .shapeGrid {
488
+ display: grid;
489
+ grid-template-columns: repeat(4, 1fr);
490
+ gap: 8px;
491
+ }
492
+
493
+ .shapeBtn {
494
+ width: 48px;
495
+ height: 48px;
496
+ background: #1f1f1f;
497
+ border: 2px solid transparent;
498
+ border-radius: 8px;
499
+ cursor: pointer;
500
+ display: flex;
501
+ align-items: center;
502
+ justify-content: center;
503
+ color: var(--text-primary);
504
+ transition: border-color 0.1s, background 0.1s;
505
+ }
506
+
507
+ .shapeBtn:hover {
508
+ background: #3d3d3d;
509
+ }
510
+
511
+ .shapeBtn.active {
512
+ border-color: var(--accent);
513
+ background: #3d3d3d;
514
+ }
515
+
516
+ .shapeBtn svg {
517
+ width: 28px;
518
+ height: 28px;
519
+ }
520
+
521
+ /* Page Info */
522
+ .pageInfo {
523
+ display: flex;
524
+ align-items: center;
525
+ gap: 8px;
526
+ margin-left: auto;
527
+ }
528
+
529
+ #pageInput {
530
+ width: 40px;
531
+ height: 28px;
532
+ background: var(--bg-tertiary);
533
+ border: 1px solid var(--border-color);
534
+ border-radius: 4px;
535
+ color: var(--text-primary);
536
+ text-align: center;
537
+ font-size: 13px;
538
+ }
539
+
540
+ #pageCount {
541
+ color: var(--text-secondary);
542
+ font-size: 13px;
543
+ }
544
+
545
+ /* Sidebar - Thumbnails */
546
+ #sidebar {
547
+ position: fixed;
548
+ top: var(--toolbar-height);
549
+ left: 0;
550
+ bottom: 0;
551
+ width: var(--sidebar-width);
552
+ background: var(--bg-secondary);
553
+ border-right: 1px solid var(--border-color);
554
+ overflow-y: auto;
555
+ display: none;
556
+ z-index: 50;
557
+ }
558
+
559
+ #sidebar.open {
560
+ display: block;
561
+ }
562
+
563
+ .sidebarHeader {
564
+ padding: 12px 16px;
565
+ font-size: 13px;
566
+ font-weight: 600;
567
+ border-bottom: 1px solid var(--border-color);
568
+ display: flex;
569
+ justify-content: space-between;
570
+ align-items: center;
571
+ }
572
+
573
+ .closeBtn {
574
+ background: none;
575
+ border: none;
576
+ color: var(--text-primary);
577
+ cursor: pointer;
578
+ font-size: 18px;
579
+ }
580
+
581
+ #thumbnailContainer {
582
+ padding: 12px;
583
+ display: flex;
584
+ flex-direction: column;
585
+ gap: 12px;
586
+ }
587
+
588
+ .thumbnail {
589
+ background: var(--bg-tertiary);
590
+ border: 2px solid transparent;
591
+ border-radius: 4px;
592
+ cursor: pointer;
593
+ padding: 4px;
594
+ transition: border-color 0.15s;
595
+ }
596
+
597
+ .thumbnail:hover {
598
+ border-color: var(--accent);
599
+ }
600
+
601
+ .thumbnail.active {
602
+ border-color: var(--accent);
603
+ }
604
+
605
+ .thumbnail canvas {
606
+ width: 100%;
607
+ display: block;
608
+ }
609
+
610
+ .thumbnailNum {
611
+ text-align: center;
612
+ font-size: 11px;
613
+ color: var(--text-secondary);
614
+ margin-top: 4px;
615
+ }
616
+
617
+ /* Viewer Container */
618
+ #viewerContainer {
619
+ position: fixed;
620
+ top: var(--toolbar-height);
621
+ left: 0;
622
+ right: 0;
623
+ bottom: 0;
624
+ overflow: auto;
625
+ background: #525659;
626
+ z-index: 1;
627
+ }
628
+
629
+ #viewerContainer.withSidebar {
630
+ left: var(--sidebar-width);
631
+ }
632
+
633
+ .pdfViewer .page {
634
+ margin: 8px auto;
635
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
636
+ position: relative;
637
+ transition: filter 0.3s ease;
638
+ }
639
+
640
+ /* Sepia Reading Mode */
641
+ .pdfViewer.sepia .page canvas {
642
+ filter: sepia(40%) brightness(0.95) contrast(0.9);
643
+ }
644
+
645
+ .pdfViewer.sepia .page {
646
+ background: #f4ecd8 !important;
647
+ }
648
+
649
+ #viewerContainer.sepia {
650
+ background: #d4c9a8;
651
+ }
652
+
653
+ /* Annotation Layer */
654
+ .annotationLayer {
655
+ position: absolute;
656
+ top: 0;
657
+ left: 0;
658
+ right: 0;
659
+ bottom: 0;
660
+ pointer-events: none;
661
+ z-index: 10;
662
+ }
663
+
664
+ .annotationLayer.active {
665
+ pointer-events: auto;
666
+ cursor: crosshair;
667
+ }
668
+
669
+ .annotationLayer path {
670
+ fill: none;
671
+ stroke-linecap: round;
672
+ stroke-linejoin: round;
673
+ }
674
+
675
+ /* Select tool cursor */
676
+ .annotationLayer.select-mode {
677
+ cursor: default;
678
+ }
679
+
680
+ .annotationLayer.select-mode path,
681
+ .annotationLayer.select-mode rect,
682
+ .annotationLayer.select-mode ellipse,
683
+ .annotationLayer.select-mode line,
684
+ .annotationLayer.select-mode text {
685
+ cursor: grab;
686
+ pointer-events: all;
687
+ transition: transform 0.1s ease, opacity 0.1s ease;
688
+ }
689
+
690
+ /* Invisible hit area for easier selection */
691
+ .annotationLayer.select-mode path,
692
+ .annotationLayer.select-mode line {
693
+ stroke-linecap: round;
694
+ }
695
+
696
+ .annotationLayer.select-mode path:hover,
697
+ .annotationLayer.select-mode rect:hover,
698
+ .annotationLayer.select-mode ellipse:hover,
699
+ .annotationLayer.select-mode line:hover,
700
+ .annotationLayer.select-mode text:hover {
701
+ opacity: 0.8;
702
+ cursor: grab;
703
+ }
704
+
705
+ /* Selected annotation element - use filter for SVG compatibility */
706
+ .annotation-selected {
707
+ filter: drop-shadow(0 0 4px #0078d4) drop-shadow(0 0 8px rgba(0, 120, 212, 0.6)) !important;
708
+ opacity: 1 !important;
709
+ }
710
+
711
+ /* Marquee selection rectangle */
712
+ .annotationLayer .marquee-rect {
713
+ fill: rgba(0, 120, 212, 0.1) !important;
714
+ stroke: #0078d4 !important;
715
+ stroke-width: 1 !important;
716
+ stroke-dasharray: 4 2 !important;
717
+ pointer-events: none !important;
718
+ cursor: default !important;
719
+ opacity: 1 !important;
720
+ filter: none !important;
721
+ transition: none !important;
722
+ }
723
+
724
+ /* Multi-selected annotations */
725
+ .annotation-multi-selected {
726
+ filter: drop-shadow(0 0 3px #0078d4) drop-shadow(0 0 6px rgba(0, 120, 212, 0.4)) !important;
727
+ opacity: 0.9 !important;
728
+ }
729
+
730
+ /* Touch feedback */
731
+ .annotation-dragging {
732
+ opacity: 0.6;
733
+ cursor: grabbing !important;
734
+ filter: drop-shadow(0 4px 12px rgba(0, 120, 212, 0.5));
735
+ }
736
+
737
+ /* Tablet/Touch optimizations for select mode */
738
+ @media (pointer: coarse),
739
+ (max-width: 1024px) {
740
+
741
+ .annotationLayer.select-mode path,
742
+ .annotationLayer.select-mode rect,
743
+ .annotationLayer.select-mode ellipse,
744
+ .annotationLayer.select-mode line,
745
+ .annotationLayer.select-mode text {
746
+ /* Ensure touch-friendly interaction */
747
+ cursor: pointer;
748
+ }
749
+
750
+ /* Bigger toolbar buttons for touch */
751
+ .toolbarBtn {
752
+ width: 44px;
753
+ height: 44px;
754
+ min-width: 44px;
755
+ }
756
+ }
757
+
758
+ /* Touch selection ring animation */
759
+ @keyframes selectionPulse {
760
+ 0% {
761
+ outline-color: rgba(0, 120, 212, 1);
762
+ }
763
+
764
+ 50% {
765
+ outline-color: rgba(0, 120, 212, 0.5);
766
+ }
767
+
768
+ 100% {
769
+ outline-color: rgba(0, 120, 212, 1);
770
+ }
771
+ }
772
+
773
+ .annotation-selected.just-selected {
774
+ animation: selectionPulse 0.6s ease-out;
775
+ }
776
+
777
+ /* Move handle for touch devices */
778
+ .annotation-move-handle {
779
+ position: absolute;
780
+ width: 36px;
781
+ height: 36px;
782
+ background: rgba(0, 120, 212, 0.9);
783
+ border-radius: 50%;
784
+ display: none;
785
+ align-items: center;
786
+ justify-content: center;
787
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
788
+ cursor: grab;
789
+ z-index: 100;
790
+ touch-action: none;
791
+ }
792
+
793
+ .annotation-move-handle svg {
794
+ width: 20px;
795
+ height: 20px;
796
+ fill: white;
797
+ }
798
+
799
+ @media (pointer: coarse) {
800
+ .annotation-move-handle {
801
+ display: flex;
802
+ }
803
+ }
804
+
805
+ /* Ghost element while dragging */
806
+ .annotation-ghost {
807
+ opacity: 0.3;
808
+ pointer-events: none;
809
+ }
810
+
811
+ /* Selection toolbar for touch - action buttons */
812
+ .selection-toolbar {
813
+ position: fixed;
814
+ bottom: 24px;
815
+ left: 50%;
816
+ transform: translateX(-50%) translateY(100px);
817
+ background: linear-gradient(135deg, #363636 0%, #2d2d2d 100%);
818
+ border: 1px solid rgba(255, 255, 255, 0.1);
819
+ border-radius: 16px;
820
+ padding: 12px 16px;
821
+ display: flex;
822
+ align-items: center;
823
+ gap: 12px;
824
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05);
825
+ z-index: 2000;
826
+ opacity: 0;
827
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
828
+ pointer-events: none;
829
+ }
830
+
831
+ .selection-toolbar.visible {
832
+ transform: translateX(-50%) translateY(0);
833
+ opacity: 1;
834
+ pointer-events: auto;
835
+ }
836
+
837
+ .selection-toolbar::before {
838
+ content: 'Seçili Öğe';
839
+ position: absolute;
840
+ top: -28px;
841
+ left: 50%;
842
+ transform: translateX(-50%);
843
+ font-size: 11px;
844
+ color: rgba(255, 255, 255, 0.6);
845
+ white-space: nowrap;
846
+ text-transform: uppercase;
847
+ letter-spacing: 0.5px;
848
+ }
849
+
850
+ .selection-toolbar button {
851
+ width: 52px;
852
+ height: 52px;
853
+ border: none;
854
+ background: rgba(255, 255, 255, 0.08);
855
+ color: white;
856
+ border-radius: 12px;
857
+ cursor: pointer;
858
+ display: flex;
859
+ flex-direction: column;
860
+ align-items: center;
861
+ justify-content: center;
862
+ gap: 4px;
863
+ transition: all 0.15s ease;
864
+ position: relative;
865
+ }
866
+
867
+ .selection-toolbar button:hover {
868
+ background: rgba(255, 255, 255, 0.15);
869
+ transform: translateY(-2px);
870
+ }
871
+
872
+ .selection-toolbar button:active {
873
+ transform: translateY(0);
874
+ background: rgba(255, 255, 255, 0.2);
875
+ }
876
+
877
+ .selection-toolbar button.delete {
878
+ background: rgba(196, 43, 28, 0.8);
879
+ }
880
+
881
+ .selection-toolbar button.delete:hover {
882
+ background: #e03e2f;
883
+ transform: translateY(-2px);
884
+ }
885
+
886
+ .selection-toolbar button svg {
887
+ width: 22px;
888
+ height: 22px;
889
+ fill: currentColor;
890
+ }
891
+
892
+ .selection-toolbar button span {
893
+ font-size: 9px;
894
+ opacity: 0.8;
895
+ text-transform: uppercase;
896
+ letter-spacing: 0.3px;
897
+ }
898
+
899
+ /* Toast notification for copy/paste */
900
+ .toast-notification {
901
+ position: fixed;
902
+ bottom: 80px;
903
+ left: 50%;
904
+ transform: translateX(-50%);
905
+ background: #323232;
906
+ color: white;
907
+ padding: 12px 24px;
908
+ border-radius: 8px;
909
+ font-size: 14px;
910
+ z-index: 3000;
911
+ animation: toastIn 0.3s ease, toastOut 0.3s ease 1.7s forwards;
912
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
913
+ }
914
+
915
+ @keyframes toastIn {
916
+ from {
917
+ opacity: 0;
918
+ transform: translateX(-50%) translateY(20px);
919
+ }
920
+
921
+ to {
922
+ opacity: 1;
923
+ transform: translateX(-50%) translateY(0);
924
+ }
925
+ }
926
+
927
+ @keyframes toastOut {
928
+ from {
929
+ opacity: 1;
930
+ transform: translateX(-50%) translateY(0);
931
+ }
932
+
933
+ to {
934
+ opacity: 0;
935
+ transform: translateX(-50%) translateY(-20px);
936
+ }
937
+ }
938
+
939
+ /* Text Selection Highlight */
940
+ .textHighlight {
941
+ position: absolute;
942
+ pointer-events: none;
943
+ border-radius: 2px;
944
+ }
945
+
946
+ /* Selection Popup Button */
947
+ .highlightPopup {
948
+ position: absolute;
949
+ background: var(--bg-secondary);
950
+ border: 1px solid var(--border-color);
951
+ border-radius: 8px;
952
+ padding: 6px;
953
+ display: flex;
954
+ gap: 4px;
955
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
956
+ z-index: 500;
957
+ }
958
+
959
+ .highlightPopup button {
960
+ width: 28px;
961
+ height: 28px;
962
+ border: none;
963
+ border-radius: 50%;
964
+ cursor: pointer;
965
+ transition: transform 0.1s;
966
+ }
967
+
968
+ .highlightPopup button:hover {
969
+ transform: scale(1.15);
970
+ }
971
+
972
+ /* Upload Overlay */
973
+ #uploadOverlay {
974
+ position: fixed;
975
+ top: var(--toolbar-height);
976
+ left: 0;
977
+ right: 0;
978
+ bottom: 0;
979
+ background: var(--bg-primary);
980
+ display: flex;
981
+ align-items: center;
982
+ justify-content: center;
983
+ z-index: 40;
984
+ }
14
985
 
15
- <!-- Viewer styles -->
16
- <link rel="stylesheet" href="viewer.css">
986
+ .dropzone {
987
+ width: 400px;
988
+ padding: 60px 40px;
989
+ background: var(--bg-secondary);
990
+ border: 2px dashed var(--border-color);
991
+ border-radius: 12px;
992
+ text-align: center;
993
+ cursor: pointer;
994
+ transition: all 0.2s;
995
+ }
17
996
 
997
+ .dropzone:hover {
998
+ border-color: var(--accent);
999
+ background: var(--bg-tertiary);
1000
+ }
1001
+
1002
+ .dropzone svg {
1003
+ width: 64px;
1004
+ height: 64px;
1005
+ fill: var(--text-secondary);
1006
+ margin-bottom: 16px;
1007
+ }
1008
+
1009
+ .dropzone h2 {
1010
+ font-size: 18px;
1011
+ font-weight: 500;
1012
+ margin-bottom: 8px;
1013
+ }
1014
+
1015
+ .dropzone p {
1016
+ color: var(--text-secondary);
1017
+ font-size: 13px;
1018
+ }
1019
+
1020
+ /* Inline Text Editor */
1021
+ .textEditorOverlay {
1022
+ position: fixed;
1023
+ top: 0;
1024
+ left: 0;
1025
+ right: 0;
1026
+ bottom: 0;
1027
+ z-index: 1000;
1028
+ }
1029
+
1030
+ .textEditorBox {
1031
+ position: absolute;
1032
+ background: white;
1033
+ border: 2px solid var(--accent);
1034
+ border-radius: 4px;
1035
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
1036
+ min-width: 200px;
1037
+ max-width: 400px;
1038
+ }
1039
+
1040
+ .textEditorInput {
1041
+ padding: 12px 16px;
1042
+ font-size: 14px;
1043
+ font-family: 'Segoe UI', system-ui, sans-serif;
1044
+ color: #333;
1045
+ outline: none;
1046
+ min-height: 40px;
1047
+ word-wrap: break-word;
1048
+ }
1049
+
1050
+ .textEditorInput:empty:before {
1051
+ content: 'Buraya yazmaya başla...';
1052
+ color: #999;
1053
+ }
1054
+
1055
+ .textEditorToolbar {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ gap: 6px;
1059
+ padding: 8px 12px;
1060
+ border-top: 1px solid #e0e0e0;
1061
+ background: #f5f5f5;
1062
+ flex-wrap: wrap;
1063
+ }
1064
+
1065
+ .textEditorColors {
1066
+ display: flex;
1067
+ align-items: center;
1068
+ gap: 4px;
1069
+ }
1070
+
1071
+ .textEditorSizeGroup {
1072
+ display: flex;
1073
+ align-items: center;
1074
+ gap: 2px;
1075
+ margin-left: 4px;
1076
+ }
1077
+
1078
+ .textEditorSizeLabel {
1079
+ font-size: 12px;
1080
+ font-weight: 600;
1081
+ color: #333;
1082
+ min-width: 24px;
1083
+ text-align: center;
1084
+ user-select: none;
1085
+ }
1086
+
1087
+ .textEditorBtn {
1088
+ width: 28px;
1089
+ height: 28px;
1090
+ border: none;
1091
+ background: transparent;
1092
+ border-radius: 4px;
1093
+ cursor: pointer;
1094
+ display: flex;
1095
+ align-items: center;
1096
+ justify-content: center;
1097
+ color: #333;
1098
+ font-size: 12px;
1099
+ font-weight: 600;
1100
+ }
1101
+
1102
+ .textEditorBtn:hover {
1103
+ background: #e0e0e0;
1104
+ }
1105
+
1106
+ .textEditorBtn.delete {
1107
+ color: #d32f2f;
1108
+ margin-left: auto;
1109
+ }
1110
+
1111
+ .textEditorColorDot {
1112
+ width: 18px;
1113
+ height: 18px;
1114
+ border-radius: 50%;
1115
+ border: 2px solid transparent;
1116
+ cursor: pointer;
1117
+ flex-shrink: 0;
1118
+ }
1119
+
1120
+ .textEditorColorDot:hover {
1121
+ transform: scale(1.15);
1122
+ }
1123
+
1124
+ .textEditorColorDot.active {
1125
+ border-color: var(--accent);
1126
+ }
1127
+
1128
+ /* Draggable text annotations */
1129
+ .annotationLayer svg text {
1130
+ cursor: move;
1131
+ user-select: none;
1132
+ }
1133
+
1134
+ .annotationLayer svg text.dragging {
1135
+ opacity: 0.7;
1136
+ }
1137
+
1138
+ .hidden {
1139
+ display: none !important;
1140
+ }
1141
+
1142
+ /* Scrollbar */
1143
+ ::-webkit-scrollbar {
1144
+ width: 8px;
1145
+ height: 8px;
1146
+ }
1147
+
1148
+ ::-webkit-scrollbar-track {
1149
+ background: var(--bg-secondary);
1150
+ }
1151
+
1152
+ ::-webkit-scrollbar-thumb {
1153
+ background: var(--bg-tertiary);
1154
+ border-radius: 4px;
1155
+ }
1156
+
1157
+ ::-webkit-scrollbar-thumb:hover {
1158
+ background: #555;
1159
+ }
1160
+
1161
+ /* ==========================================
1162
+ BOTTOM TOOLBAR (Mobile Only)
1163
+ ========================================== */
1164
+ #bottomToolbar {
1165
+ display: none;
1166
+ position: fixed;
1167
+ bottom: 0;
1168
+ left: 0;
1169
+ right: 0;
1170
+ height: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1171
+ background: var(--bg-secondary);
1172
+ border-top: 1px solid var(--border-color);
1173
+ z-index: 100;
1174
+ padding: 0 8px;
1175
+ padding-bottom: var(--safe-area-bottom);
1176
+ }
1177
+
1178
+ .bottomToolbarInner {
1179
+ display: flex;
1180
+ align-items: center;
1181
+ gap: 2px;
1182
+ height: var(--bottom-bar-height);
1183
+ overflow-x: auto;
1184
+ overflow-y: hidden;
1185
+ -webkit-overflow-scrolling: touch;
1186
+ scrollbar-width: none;
1187
+ -ms-overflow-style: none;
1188
+ }
1189
+
1190
+ .bottomToolbarInner::-webkit-scrollbar {
1191
+ display: none;
1192
+ }
1193
+
1194
+ /* Dropdown backdrop overlay */
1195
+ #dropdownBackdrop {
1196
+ display: none;
1197
+ position: fixed;
1198
+ top: 0;
1199
+ left: 0;
1200
+ right: 0;
1201
+ bottom: 0;
1202
+ background: rgba(0, 0, 0, 0.5);
1203
+ z-index: 250;
1204
+ }
1205
+
1206
+ #dropdownBackdrop.visible {
1207
+ display: block;
1208
+ }
1209
+
1210
+ /* Bottom sheet drag handle */
1211
+ .bottomSheetHandle {
1212
+ width: 40px;
1213
+ height: 4px;
1214
+ background: rgba(255, 255, 255, 0.3);
1215
+ border-radius: 2px;
1216
+ margin: 8px auto 4px;
1217
+ }
1218
+
1219
+ /* ==========================================
1220
+ MOBILE BREAKPOINT (max-width: 599px)
1221
+ ========================================== */
1222
+ @media (max-width: 599px) {
1223
+
1224
+ /* Top toolbar - compact mobile layout */
1225
+ #toolbar {
1226
+ height: calc(var(--toolbar-height-mobile) + var(--safe-area-top));
1227
+ padding-top: var(--safe-area-top);
1228
+ padding-left: calc(8px + var(--safe-area-left));
1229
+ padding-right: calc(8px + var(--safe-area-right));
1230
+ gap: 2px;
1231
+ }
1232
+
1233
+ /* Hide annotation tools from top bar on mobile (they go to bottom bar) */
1234
+ #toolbar>.toolbarGroup:nth-child(3) {
1235
+ display: none;
1236
+ }
1237
+
1238
+ /* Hide separators adjacent to hidden group */
1239
+ #toolbar>.separator:nth-child(2),
1240
+ #toolbar>.separator:nth-child(4) {
1241
+ display: none;
1242
+ }
1243
+
1244
+ /* Show bottom toolbar */
1245
+ #bottomToolbar {
1246
+ display: block;
1247
+ }
1248
+
1249
+ /* Viewer container adjusted for mobile toolbars */
1250
+ #viewerContainer {
1251
+ top: calc(var(--toolbar-height-mobile) + var(--safe-area-top));
1252
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1253
+ }
1254
+
1255
+ /* Upload overlay adjusted */
1256
+ #uploadOverlay {
1257
+ top: calc(var(--toolbar-height-mobile) + var(--safe-area-top));
1258
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1259
+ }
1260
+
1261
+ /* Sidebar - full width overlay on mobile */
1262
+ #sidebar {
1263
+ width: 100%;
1264
+ z-index: 150;
1265
+ top: calc(var(--toolbar-height-mobile) + var(--safe-area-top));
1266
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1267
+ }
1268
+
1269
+ #viewerContainer.withSidebar {
1270
+ left: 0;
1271
+ }
1272
+
1273
+ /* Dropdowns become bottom sheets on mobile */
1274
+ .toolDropdown {
1275
+ position: fixed !important;
1276
+ bottom: 0 !important;
1277
+ left: 0 !important;
1278
+ right: 0 !important;
1279
+ top: auto !important;
1280
+ border-radius: 16px 16px 0 0;
1281
+ padding: 8px 16px calc(16px + var(--safe-area-bottom));
1282
+ max-height: 60vh;
1283
+ overflow-y: auto;
1284
+ z-index: 300;
1285
+ transform: translateY(100%);
1286
+ transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
1287
+ display: block !important;
1288
+ min-width: unset;
1289
+ box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
1290
+ }
1291
+
1292
+ .toolDropdown.visible {
1293
+ transform: translateY(0);
1294
+ }
1295
+
1296
+ /* Responsive dropzone */
1297
+ .dropzone {
1298
+ width: 90%;
1299
+ padding: 40px 20px;
1300
+ }
1301
+
1302
+ .dropzone svg {
1303
+ width: 48px;
1304
+ height: 48px;
1305
+ }
1306
+
1307
+ .dropzone h2 {
1308
+ font-size: 16px;
1309
+ }
1310
+
1311
+ /* Text editor bounds */
1312
+ .textEditorBox {
1313
+ max-width: calc(100vw - 32px);
1314
+ max-height: calc(100vh - 120px);
1315
+ }
1316
+
1317
+ /* Selection toolbar above bottom bar */
1318
+ .selection-toolbar {
1319
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom) + 12px);
1320
+ }
1321
+
1322
+ /* Toast above bottom bar */
1323
+ .toast-notification {
1324
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom) + 16px);
1325
+ }
1326
+
1327
+ /* Page info compact */
1328
+ .pageInfo {
1329
+ margin-left: auto;
1330
+ gap: 4px;
1331
+ }
1332
+
1333
+ #pageInput {
1334
+ width: 32px;
1335
+ font-size: 12px;
1336
+ }
1337
+
1338
+ #pageCount {
1339
+ font-size: 12px;
1340
+ }
1341
+
1342
+ /* Hide tooltips on mobile (no hover) */
1343
+ .toolbarBtn::after,
1344
+ .toolbarBtn::before {
1345
+ display: none;
1346
+ }
1347
+ }
1348
+
1349
+ /* ==========================================
1350
+ TABLET BREAKPOINT (600px - 1024px)
1351
+ ========================================== */
1352
+
1353
+ /* --- Tablet shared (both orientations) --- */
1354
+ @media (min-width: 600px) and (max-width: 1024px) {
1355
+
1356
+ /* Scrollable toolbar */
1357
+ #toolbar {
1358
+ overflow-x: auto;
1359
+ overflow-y: hidden;
1360
+ scrollbar-width: none;
1361
+ -ms-overflow-style: none;
1362
+ }
1363
+
1364
+ #toolbar::-webkit-scrollbar {
1365
+ display: none;
1366
+ }
1367
+
1368
+ /* Hide tooltips on touch tablets */
1369
+ .toolbarBtn::after,
1370
+ .toolbarBtn::before {
1371
+ display: none;
1372
+ }
1373
+
1374
+ /* Safe area insets for modern tablets */
1375
+ #toolbar {
1376
+ padding-top: var(--safe-area-top);
1377
+ padding-left: calc(12px + var(--safe-area-left));
1378
+ padding-right: calc(12px + var(--safe-area-right));
1379
+ }
1380
+
1381
+ /* Text editor bounds check */
1382
+ .textEditorBox {
1383
+ max-width: calc(100vw - 48px);
1384
+ max-height: calc(100vh - 140px);
1385
+ }
1386
+
1387
+ /* Dropzone slightly smaller */
1388
+ .dropzone {
1389
+ width: 70%;
1390
+ }
1391
+ }
1392
+
1393
+ /* --- Tablet PORTRAIT --- */
1394
+ @media (min-width: 600px) and (max-width: 1024px) and (orientation: portrait) {
1395
+
1396
+ /* Top toolbar compact — height includes safe area (like mobile) */
1397
+ #toolbar {
1398
+ height: calc(var(--toolbar-height) + var(--safe-area-top));
1399
+ gap: 2px;
1400
+ }
1401
+
1402
+ /* Hide annotation tools group from top bar (CSS hides, JS moves) */
1403
+ #toolbar>.toolbarGroup:nth-child(3) {
1404
+ display: none;
1405
+ }
1406
+
1407
+ #toolbar>.separator:nth-child(2),
1408
+ #toolbar>.separator:nth-child(4) {
1409
+ display: none;
1410
+ }
1411
+
1412
+ /* Show bottom toolbar */
1413
+ #bottomToolbar {
1414
+ display: block;
1415
+ }
1416
+
1417
+ /* Viewer container adjusted for both toolbars */
1418
+ #viewerContainer {
1419
+ top: calc(var(--toolbar-height) + var(--safe-area-top));
1420
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1421
+ }
1422
+
1423
+ #uploadOverlay {
1424
+ top: calc(var(--toolbar-height) + var(--safe-area-top));
1425
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1426
+ }
1427
+
1428
+ /* Sidebar as overlay (don't push content) */
1429
+ #sidebar {
1430
+ width: 280px;
1431
+ z-index: 150;
1432
+ top: calc(var(--toolbar-height) + var(--safe-area-top));
1433
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom));
1434
+ }
1435
+
1436
+ #viewerContainer.withSidebar {
1437
+ left: 0;
1438
+ }
1439
+
1440
+ /* Dropdowns become bottom sheets */
1441
+ .toolDropdown {
1442
+ position: fixed !important;
1443
+ bottom: 0 !important;
1444
+ left: 0 !important;
1445
+ right: 0 !important;
1446
+ top: auto !important;
1447
+ border-radius: 16px 16px 0 0;
1448
+ padding: 12px 20px calc(16px + var(--safe-area-bottom));
1449
+ max-height: 55vh;
1450
+ overflow-y: auto;
1451
+ z-index: 300;
1452
+ transform: translateY(100%);
1453
+ transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
1454
+ display: block !important;
1455
+ min-width: unset;
1456
+ box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
1457
+ }
1458
+
1459
+ .toolDropdown.visible {
1460
+ transform: translateY(0);
1461
+ }
1462
+
1463
+ /* Selection toolbar & toast above bottom bar */
1464
+ .selection-toolbar {
1465
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom) + 12px);
1466
+ }
1467
+
1468
+ .toast-notification {
1469
+ bottom: calc(var(--bottom-bar-height) + var(--safe-area-bottom) + 16px);
1470
+ }
1471
+ }
1472
+
1473
+ /* --- Tablet LANDSCAPE --- */
1474
+ @media (min-width: 600px) and (max-width: 1024px) and (orientation: landscape) {
1475
+
1476
+ /* Toolbar stays single row, compact gaps */
1477
+ #toolbar {
1478
+ gap: 2px;
1479
+ }
1480
+
1481
+ .separator {
1482
+ margin: 0 4px;
1483
+ }
1484
+
1485
+ /* Sidebar narrower to save space */
1486
+ #sidebar {
1487
+ width: 180px;
1488
+ }
1489
+
1490
+ #viewerContainer.withSidebar {
1491
+ left: 180px;
1492
+ }
1493
+
1494
+ /* Dropdowns get wider min-width */
1495
+ .toolDropdown {
1496
+ min-width: 280px;
1497
+ }
1498
+ }
1499
+
1500
+ /* ==========================================
1501
+ TOUCH-FRIENDLY SIZES (pointer: coarse)
1502
+ ========================================== */
1503
+ @media (pointer: coarse) {
1504
+ .thicknessSlider input[type="range"]::-webkit-slider-thumb {
1505
+ width: 24px;
1506
+ height: 24px;
1507
+ }
1508
+
1509
+ .thicknessSlider input[type="range"]::-moz-range-thumb {
1510
+ width: 24px;
1511
+ height: 24px;
1512
+ }
1513
+
1514
+ .colorDot {
1515
+ width: 36px;
1516
+ height: 36px;
1517
+ }
1518
+
1519
+ .shapeBtn {
1520
+ width: 56px;
1521
+ height: 56px;
1522
+ }
1523
+ }
1524
+ </style>
18
1525
  </head>
19
1526
 
20
1527
  <body>
@@ -376,7 +1883,2745 @@
376
1883
  <div id="viewer" class="pdfViewer"></div>
377
1884
  </div>
378
1885
 
379
- <script defer src="viewer-app.js"></script>
1886
+ <script>
1887
+ // IIFE to prevent global access to pdfDoc, pdfViewer
1888
+ (function () {
1889
+ 'use strict';
1890
+
1891
+ // ============================================
1892
+ // CANVAS EXPORT PROTECTION
1893
+ // Block toDataURL/toBlob for PDF render canvas only
1894
+ // Allows: thumbnails, annotations, other canvases
1895
+ // ============================================
1896
+ const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
1897
+ const originalToBlob = HTMLCanvasElement.prototype.toBlob;
1898
+
1899
+ HTMLCanvasElement.prototype.toDataURL = function () {
1900
+ // Block only main PDF page canvases (inside .page elements in #viewerContainer)
1901
+ if (this.closest && this.closest('.page') && this.closest('#viewerContainer')) {
1902
+ console.warn('[Security] Canvas toDataURL blocked for PDF page');
1903
+ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; // 1x1 transparent
1904
+ }
1905
+ return originalToDataURL.apply(this, arguments);
1906
+ };
1907
+
1908
+ HTMLCanvasElement.prototype.toBlob = function (callback) {
1909
+ // Block only main PDF page canvases
1910
+ if (this.closest && this.closest('.page') && this.closest('#viewerContainer')) {
1911
+ console.warn('[Security] Canvas toBlob blocked for PDF page');
1912
+ // Return empty blob
1913
+ if (callback) callback(new Blob([], { type: 'image/png' }));
1914
+ return;
1915
+ }
1916
+ return originalToBlob.apply(this, arguments);
1917
+ };
1918
+
1919
+ pdfjsLib.GlobalWorkerOptions.workerSrc = '';
1920
+
1921
+ // State - now private, not accessible from console
1922
+ let pdfDoc = null;
1923
+ let pdfViewer = null;
1924
+ let annotationMode = false;
1925
+ let currentTool = null; // null, 'pen', 'highlight', 'eraser'
1926
+ let currentColor = '#e81224';
1927
+ let currentWidth = 2;
1928
+ let isDrawing = false;
1929
+ let currentPath = null;
1930
+ let currentDrawingPage = null;
1931
+
1932
+ // RAF throttle for smooth drawing performance
1933
+ let pathSegments = []; // Buffer path segments
1934
+ let drawRAF = null; // requestAnimationFrame ID
1935
+
1936
+ // Annotation persistence - stores SVG innerHTML per page
1937
+ const annotationsStore = new Map();
1938
+ const annotationRotations = new Map(); // tracks rotation when annotations were saved
1939
+
1940
+ // AbortControllers for annotation layer event listeners (cleanup on re-inject)
1941
+ const annotationAbortControllers = new Map(); // pageNum -> AbortController
1942
+
1943
+ // Undo/Redo history stacks - per page
1944
+ const undoStacks = new Map(); // pageNum -> [svgInnerHTML, ...]
1945
+ const redoStacks = new Map(); // pageNum -> [svgInnerHTML, ...]
1946
+ const MAX_HISTORY = 30;
1947
+
1948
+ // Store base dimensions (scale=1.0) for each page - ensures consistent coordinates
1949
+ const pageBaseDimensions = new Map();
1950
+
1951
+ // Current SVG reference for drawing
1952
+ let currentSvg = null;
1953
+
1954
+ // Elements
1955
+ const container = document.getElementById('viewerContainer');
1956
+ const uploadOverlay = document.getElementById('uploadOverlay');
1957
+ const fileInput = document.getElementById('fileInput');
1958
+ const sidebar = document.getElementById('sidebar');
1959
+ const thumbnailContainer = document.getElementById('thumbnailContainer');
1960
+
1961
+ // Initialize PDFViewer
1962
+ const eventBus = new pdfjsViewer.EventBus();
1963
+ const linkService = new pdfjsViewer.PDFLinkService({ eventBus });
1964
+
1965
+ pdfViewer = new pdfjsViewer.PDFViewer({
1966
+ container: container,
1967
+ eventBus: eventBus,
1968
+ linkService: linkService,
1969
+ removePageBorders: true,
1970
+ textLayerMode: 2
1971
+ });
1972
+ linkService.setViewer(pdfViewer);
1973
+
1974
+ // Track first page render for queue system
1975
+ let firstPageRendered = false;
1976
+ eventBus.on('pagerendered', function (evt) {
1977
+ if (!firstPageRendered && evt.pageNumber === 1) {
1978
+ firstPageRendered = true;
1979
+ // Notify parent that PDF is fully rendered (for queue system)
1980
+ if (window.parent && window.parent !== window) {
1981
+ const config = window.PDF_SECURE_CONFIG || {};
1982
+ window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, window.location.origin);
1983
+ console.log('[PDF-Secure] First page rendered, notifying parent');
1984
+ }
1985
+ }
1986
+ });
1987
+
1988
+ // File Handling
1989
+ document.getElementById('dropzone').onclick = () => fileInput.click();
1990
+
1991
+ fileInput.onchange = async (e) => {
1992
+ const file = e.target.files[0];
1993
+ if (file) await loadPDF(file);
1994
+ };
1995
+
1996
+ uploadOverlay.ondragover = (e) => e.preventDefault();
1997
+ uploadOverlay.ondrop = async (e) => {
1998
+ e.preventDefault();
1999
+ const file = e.dataTransfer.files[0];
2000
+ if (file?.type === 'application/pdf') await loadPDF(file);
2001
+ };
2002
+
2003
+ async function loadPDF(file) {
2004
+ uploadOverlay.classList.add('hidden');
2005
+
2006
+ const data = await file.arrayBuffer();
2007
+ pdfDoc = await pdfjsLib.getDocument({ data }).promise;
2008
+
2009
+ pdfViewer.setDocument(pdfDoc);
2010
+ linkService.setDocument(pdfDoc);
2011
+
2012
+ ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
2013
+ document.getElementById(id).disabled = false;
2014
+ });
2015
+
2016
+ // Thumbnails will be generated on-demand when sidebar opens
2017
+ }
2018
+
2019
+ // Load PDF from ArrayBuffer (for secure nonce-based loading)
2020
+ async function loadPDFFromBuffer(arrayBuffer) {
2021
+ uploadOverlay.classList.add('hidden');
2022
+
2023
+ pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
2024
+
2025
+ pdfViewer.setDocument(pdfDoc);
2026
+ linkService.setDocument(pdfDoc);
2027
+
2028
+ ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
2029
+ document.getElementById(id).disabled = false;
2030
+ });
2031
+
2032
+ // Thumbnails will be generated on-demand when sidebar opens
2033
+ }
2034
+
2035
+ // Partial XOR decoder - must match backend encoding
2036
+ function partialXorDecode(encodedData, keyBase64) {
2037
+ const key = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
2038
+ const data = new Uint8Array(encodedData);
2039
+ const keyLen = key.length;
2040
+
2041
+ // Decrypt first 10KB fully
2042
+ const fullDecryptLen = Math.min(10240, data.length);
2043
+ for (let i = 0; i < fullDecryptLen; i++) {
2044
+ data[i] = data[i] ^ key[i % keyLen];
2045
+ }
2046
+
2047
+ // Decrypt every 50th byte after that
2048
+ for (let i = fullDecryptLen; i < data.length; i += 50) {
2049
+ data[i] = data[i] ^ key[i % keyLen];
2050
+ }
2051
+
2052
+ return data.buffer;
2053
+ }
2054
+
2055
+ // Auto-load PDF if config is present (injected by NodeBB plugin)
2056
+ async function autoLoadSecurePDF() {
2057
+ if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
2058
+ console.log('[PDF-Secure] No config found, showing file picker');
2059
+ return;
2060
+ }
2061
+
2062
+ const config = window.PDF_SECURE_CONFIG;
2063
+ console.log('[PDF-Secure] Auto-loading:', config.filename);
2064
+
2065
+ // Show loading state
2066
+ const dropzone = document.getElementById('dropzone');
2067
+ if (dropzone) {
2068
+ dropzone.innerHTML = `
2069
+ <svg viewBox="0 0 24 24" class="spin">
2070
+ <path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z" />
2071
+ </svg>
2072
+ <h2>PDF Yükleniyor...</h2>
2073
+ <p>${config.filename}</p>
2074
+ `;
2075
+ }
2076
+
2077
+ try {
2078
+ // ============================================
2079
+ // SPA CACHE - Check if parent has cached buffer
2080
+ // ============================================
2081
+ let pdfBuffer = null;
2082
+
2083
+ if (window.parent && window.parent !== window) {
2084
+ // Request cached buffer from parent
2085
+ const cachePromise = new Promise((resolve) => {
2086
+ const handler = (event) => {
2087
+ if (event.data && event.data.type === 'pdf-secure-cache-response' && event.data.filename === config.filename) {
2088
+ window.removeEventListener('message', handler);
2089
+ resolve(event.data.buffer);
2090
+ }
2091
+ };
2092
+ window.addEventListener('message', handler);
2093
+
2094
+ // Timeout after 100ms
2095
+ setTimeout(() => {
2096
+ window.removeEventListener('message', handler);
2097
+ resolve(null);
2098
+ }, 100);
2099
+
2100
+ window.parent.postMessage({ type: 'pdf-secure-cache-request', filename: config.filename }, window.location.origin);
2101
+ });
2102
+
2103
+ pdfBuffer = await cachePromise;
2104
+ if (pdfBuffer) {
2105
+ console.log('[PDF-Secure] Using cached buffer');
2106
+ }
2107
+ }
2108
+
2109
+ // If no cache, fetch from server
2110
+ if (!pdfBuffer) {
2111
+ // Nonce and key are embedded in HTML config (not fetched from API)
2112
+ const nonce = config.nonce;
2113
+ const xorKey = config.dk;
2114
+
2115
+ // Fetch encrypted PDF binary
2116
+ const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
2117
+ const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
2118
+
2119
+ if (!pdfRes.ok) {
2120
+ throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
2121
+ }
2122
+
2123
+ const encodedBuffer = await pdfRes.arrayBuffer();
2124
+ console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
2125
+
2126
+ // Decode XOR encrypted data
2127
+ if (xorKey) {
2128
+ console.log('[PDF-Secure] Decoding XOR encrypted data...');
2129
+ pdfBuffer = partialXorDecode(encodedBuffer, xorKey);
2130
+ } else {
2131
+ pdfBuffer = encodedBuffer;
2132
+ }
2133
+
2134
+ // Send buffer to parent for caching
2135
+ if (window.parent && window.parent !== window) {
2136
+ // Clone buffer for parent (we keep original)
2137
+ const bufferCopy = pdfBuffer.slice(0);
2138
+ window.parent.postMessage({
2139
+ type: 'pdf-secure-buffer',
2140
+ filename: config.filename,
2141
+ buffer: bufferCopy
2142
+ }, window.location.origin, [bufferCopy]); // Transferable
2143
+ }
2144
+ }
2145
+
2146
+ console.log('[PDF-Secure] PDF decoded successfully');
2147
+
2148
+ // Step 4: Load into viewer
2149
+ await loadPDFFromBuffer(pdfBuffer);
2150
+
2151
+ // Step 5: Moved to pagerendered event for proper timing
2152
+
2153
+ // Step 6: Security - clear references to prevent extraction
2154
+ pdfBuffer = null;
2155
+
2156
+ // Security: Delete config containing sensitive data (nonce, key)
2157
+ delete window.PDF_SECURE_CONFIG;
2158
+
2159
+ // Security: Remove PDF.js globals to prevent console manipulation
2160
+ delete window.pdfjsLib;
2161
+ delete window.pdfjsViewer;
2162
+
2163
+ // Security: Block dangerous PDF.js methods
2164
+ if (pdfDoc) {
2165
+ pdfDoc.getData = function () {
2166
+ console.warn('[Security] getData() is blocked');
2167
+ return Promise.reject(new Error('Access denied'));
2168
+ };
2169
+ pdfDoc.saveDocument = function () {
2170
+ console.warn('[Security] saveDocument() is blocked');
2171
+ return Promise.reject(new Error('Access denied'));
2172
+ };
2173
+ }
2174
+
2175
+ console.log('[PDF-Secure] PDF fully loaded and ready');
2176
+
2177
+ } catch (err) {
2178
+ console.error('[PDF-Secure] Auto-load error:', err);
2179
+
2180
+ // Notify parent of error (prevents 60s queue hang)
2181
+ if (window.parent && window.parent !== window) {
2182
+ const config = window.PDF_SECURE_CONFIG || {};
2183
+ window.parent.postMessage({
2184
+ type: 'pdf-secure-ready',
2185
+ filename: config.filename,
2186
+ error: err.message
2187
+ }, window.location.origin);
2188
+ }
2189
+
2190
+ if (dropzone) {
2191
+ dropzone.innerHTML = `
2192
+ <svg viewBox="0 0 24 24" style="fill: #e81224;">
2193
+ <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"/>
2194
+ </svg>
2195
+ <h2>Hata</h2>
2196
+ <p>${err.message}</p>
2197
+ `;
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ // Run auto-load on page ready
2203
+ autoLoadSecurePDF();
2204
+
2205
+ // Generate Thumbnails (deferred - only when sidebar opens)
2206
+ let thumbnailsGenerated = false;
2207
+ async function generateThumbnails() {
2208
+ if (thumbnailsGenerated) return;
2209
+ thumbnailsGenerated = true;
2210
+ thumbnailContainer.innerHTML = '';
2211
+
2212
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
2213
+ const page = await pdfDoc.getPage(i);
2214
+ const viewport = page.getViewport({ scale: 0.2 });
2215
+
2216
+ const canvas = document.createElement('canvas');
2217
+ canvas.width = viewport.width;
2218
+ canvas.height = viewport.height;
2219
+
2220
+ await page.render({
2221
+ canvasContext: canvas.getContext('2d'),
2222
+ viewport: viewport
2223
+ }).promise;
2224
+
2225
+ const thumb = document.createElement('div');
2226
+ thumb.className = 'thumbnail' + (i === 1 ? ' active' : '');
2227
+ thumb.dataset.page = i;
2228
+ thumb.innerHTML = `<div class="thumbnailNum">${i}</div>`;
2229
+ thumb.insertBefore(canvas, thumb.firstChild);
2230
+
2231
+ thumb.onclick = () => {
2232
+ pdfViewer.currentPageNumber = i;
2233
+ document.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'));
2234
+ thumb.classList.add('active');
2235
+ };
2236
+
2237
+ thumbnailContainer.appendChild(thumb);
2238
+ }
2239
+ }
2240
+
2241
+ // Events
2242
+ eventBus.on('pagesinit', () => {
2243
+ pdfViewer.currentScaleValue = 'page-width';
2244
+ document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
2245
+ });
2246
+
2247
+ eventBus.on('pagechanging', (evt) => {
2248
+ document.getElementById('pageInput').value = evt.pageNumber;
2249
+ // Update active thumbnail
2250
+ document.querySelectorAll('.thumbnail').forEach(t => {
2251
+ t.classList.toggle('active', parseInt(t.dataset.page) === evt.pageNumber);
2252
+ });
2253
+ // Update undo/redo buttons for new page
2254
+ updateUndoRedoButtons();
2255
+
2256
+ // Bug fix: Clear selection on page change (stale SVG reference)
2257
+ clearAnnotationSelection();
2258
+
2259
+ // Bug fix: Reset drawing state on page change
2260
+ if (isDrawing && currentDrawingPage) {
2261
+ saveAnnotations(currentDrawingPage);
2262
+ }
2263
+ isDrawing = false;
2264
+ currentPath = null;
2265
+ currentSvg = null;
2266
+ currentDrawingPage = null;
2267
+ });
2268
+
2269
+ eventBus.on('pagerendered', (evt) => {
2270
+ if (annotationMode) injectAnnotationLayer(evt.pageNumber);
2271
+
2272
+ // Rotation is handled natively by PDF.js via pagesRotation
2273
+ });
2274
+
2275
+ // Page Navigation
2276
+ document.getElementById('pageInput').onchange = (e) => {
2277
+ const num = parseInt(e.target.value);
2278
+ if (num >= 1 && num <= pdfViewer.pagesCount) {
2279
+ pdfViewer.currentPageNumber = num;
2280
+ }
2281
+ };
2282
+
2283
+ // Zoom
2284
+ document.getElementById('zoomIn').onclick = () => pdfViewer.currentScale += 0.25;
2285
+ document.getElementById('zoomOut').onclick = () => pdfViewer.currentScale -= 0.25;
2286
+
2287
+ // Sidebar toggle (deferred thumbnail generation)
2288
+ const sidebarEl = document.getElementById('sidebar');
2289
+ const sidebarBtnEl = document.getElementById('sidebarBtn');
2290
+ const closeSidebarBtn = document.getElementById('closeSidebar');
2291
+
2292
+ sidebarBtnEl.onclick = () => {
2293
+ const isOpening = !sidebarEl.classList.contains('open');
2294
+ sidebarEl.classList.toggle('open');
2295
+ sidebarBtnEl.classList.toggle('active');
2296
+ container.classList.toggle('withSidebar', sidebarEl.classList.contains('open'));
2297
+
2298
+ // Generate thumbnails on first open (deferred loading)
2299
+ if (isOpening && pdfDoc) {
2300
+ generateThumbnails();
2301
+ }
2302
+ };
2303
+
2304
+ closeSidebarBtn.onclick = () => {
2305
+ sidebarEl.classList.remove('open');
2306
+ sidebarBtnEl.classList.remove('active');
2307
+ container.classList.remove('withSidebar');
2308
+ };
2309
+
2310
+ // Sepia Reading Mode
2311
+ let sepiaMode = false;
2312
+ document.getElementById('sepiaBtn').onclick = () => {
2313
+ sepiaMode = !sepiaMode;
2314
+ document.getElementById('viewer').classList.toggle('sepia', sepiaMode);
2315
+ container.classList.toggle('sepia', sepiaMode);
2316
+ document.getElementById('sepiaBtn').classList.toggle('active', sepiaMode);
2317
+ };
2318
+
2319
+ // Page Rotation — uses PDF.js native rotation (re-renders at correct size & quality)
2320
+ function rotatePage(delta) {
2321
+ const current = pdfViewer.pagesRotation || 0;
2322
+ // Clear cached dimensions so they get recalculated with new rotation
2323
+ pageBaseDimensions.clear();
2324
+ pdfViewer.pagesRotation = (current + delta + 360) % 360;
2325
+ }
2326
+
2327
+ document.getElementById('rotateLeft').onclick = () => rotatePage(-90);
2328
+ document.getElementById('rotateRight').onclick = () => rotatePage(90);
2329
+
2330
+
2331
+
2332
+
2333
+ // Tool settings - separate for each tool
2334
+ let highlightColor = '#fff100';
2335
+ let highlightWidth = 4;
2336
+ let drawColor = '#e81224';
2337
+ let drawWidth = 2;
2338
+ let shapeColor = '#e81224';
2339
+ let shapeWidth = 2;
2340
+ let currentShape = 'rectangle'; // rectangle, circle, line, arrow
2341
+
2342
+ // Dropdown Panel Logic
2343
+ const highlightDropdown = document.getElementById('highlightDropdown');
2344
+ const drawDropdown = document.getElementById('drawDropdown');
2345
+ const shapesDropdown = document.getElementById('shapesDropdown');
2346
+ const highlightWrapper = document.getElementById('highlightWrapper');
2347
+ const drawWrapper = document.getElementById('drawWrapper');
2348
+ const shapesWrapper = document.getElementById('shapesWrapper');
2349
+
2350
+ const dropdownBackdrop = document.getElementById('dropdownBackdrop');
2351
+ const overflowDropdown = document.getElementById('overflowDropdown');
2352
+
2353
+ function closeAllDropdowns() {
2354
+ highlightDropdown.classList.remove('visible');
2355
+ drawDropdown.classList.remove('visible');
2356
+ shapesDropdown.classList.remove('visible');
2357
+ overflowDropdown.classList.remove('visible');
2358
+ dropdownBackdrop.classList.remove('visible');
2359
+ }
2360
+
2361
+ function toggleDropdown(dropdown, e) {
2362
+ e.stopPropagation();
2363
+ const isVisible = dropdown.classList.contains('visible');
2364
+ closeAllDropdowns();
2365
+ if (!isVisible) {
2366
+ const useBottomSheet = isMobile() || isTabletPortrait();
2367
+ // Add drag handle for mobile/tablet portrait bottom sheets
2368
+ if (useBottomSheet && !dropdown.querySelector('.bottomSheetHandle')) {
2369
+ const handle = document.createElement('div');
2370
+ handle.className = 'bottomSheetHandle';
2371
+ dropdown.insertBefore(handle, dropdown.firstChild);
2372
+ }
2373
+ dropdown.classList.add('visible');
2374
+ // Show backdrop on mobile/tablet portrait
2375
+ if (useBottomSheet) {
2376
+ dropdownBackdrop.classList.add('visible');
2377
+ }
2378
+ }
2379
+ }
2380
+
2381
+ // Backdrop click closes dropdowns
2382
+ dropdownBackdrop.addEventListener('click', () => {
2383
+ closeAllDropdowns();
2384
+ });
2385
+
2386
+ // Arrow buttons toggle dropdowns
2387
+ document.getElementById('highlightArrow').onclick = (e) => toggleDropdown(highlightDropdown, e);
2388
+ document.getElementById('drawArrow').onclick = (e) => toggleDropdown(drawDropdown, e);
2389
+ document.getElementById('shapesArrow').onclick = (e) => toggleDropdown(shapesDropdown, e);
2390
+
2391
+ // Overflow menu toggle
2392
+ document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
2393
+ overflowDropdown.onclick = (e) => e.stopPropagation();
2394
+
2395
+ // Overflow menu actions
2396
+ document.getElementById('overflowRotateLeft').onclick = () => {
2397
+ rotatePage(-90);
2398
+ closeAllDropdowns();
2399
+ };
2400
+ document.getElementById('overflowRotateRight').onclick = () => {
2401
+ rotatePage(90);
2402
+ closeAllDropdowns();
2403
+ };
2404
+ document.getElementById('overflowSepia').onclick = () => {
2405
+ document.getElementById('sepiaBtn').click();
2406
+ document.getElementById('overflowSepia').classList.toggle('active',
2407
+ document.getElementById('sepiaBtn').classList.contains('active'));
2408
+ closeAllDropdowns();
2409
+ };
2410
+
2411
+ // Close dropdowns when clicking outside
2412
+ document.addEventListener('click', (e) => {
2413
+ if (!e.target.closest('.toolDropdown') && !e.target.closest('.dropdownArrow')) {
2414
+ closeAllDropdowns();
2415
+ }
2416
+ });
2417
+
2418
+ // Prevent dropdown from closing when clicking inside
2419
+ highlightDropdown.onclick = (e) => e.stopPropagation();
2420
+ drawDropdown.onclick = (e) => e.stopPropagation();
2421
+ shapesDropdown.onclick = (e) => e.stopPropagation();
2422
+
2423
+ // Drawing Tools - Toggle Behavior
2424
+ async function setTool(tool) {
2425
+ // If same tool clicked again, deactivate
2426
+ if (currentTool === tool) {
2427
+ currentTool = null;
2428
+ annotationMode = false;
2429
+ document.querySelectorAll('.annotationLayer').forEach(el => el.classList.remove('active'));
2430
+ } else {
2431
+ currentTool = tool;
2432
+ annotationMode = true;
2433
+
2434
+ // Set color and width based on tool
2435
+ if (tool === 'highlight') {
2436
+ currentColor = highlightColor;
2437
+ currentWidth = highlightWidth;
2438
+ } else if (tool === 'pen') {
2439
+ currentColor = drawColor;
2440
+ currentWidth = drawWidth;
2441
+ } else if (tool === 'shape') {
2442
+ currentColor = shapeColor;
2443
+ currentWidth = shapeWidth;
2444
+ }
2445
+
2446
+ // Performance: Just toggle active class instead of re-injecting layers
2447
+ // This avoids expensive DOM recreation on every tool change
2448
+ document.querySelectorAll('.annotationLayer').forEach(layer => {
2449
+ layer.classList.toggle('active', annotationMode);
2450
+ });
2451
+ }
2452
+
2453
+ // Update button states
2454
+ highlightWrapper.classList.toggle('active', currentTool === 'highlight');
2455
+ drawWrapper.classList.toggle('active', currentTool === 'pen');
2456
+ shapesWrapper.classList.toggle('active', currentTool === 'shape');
2457
+ document.getElementById('eraserBtn').classList.toggle('active', currentTool === 'eraser');
2458
+ document.getElementById('textBtn').classList.toggle('active', currentTool === 'text');
2459
+ document.getElementById('selectBtn').classList.toggle('active', currentTool === 'select');
2460
+
2461
+ // Toggle select-mode class on annotation layers
2462
+ document.querySelectorAll('.annotationLayer').forEach(layer => {
2463
+ layer.classList.toggle('select-mode', currentTool === 'select');
2464
+ });
2465
+
2466
+ // Clear selection when switching tools
2467
+ if (currentTool !== 'select') {
2468
+ clearAnnotationSelection();
2469
+ }
2470
+ }
2471
+
2472
+ document.getElementById('drawBtn').onclick = () => setTool('pen');
2473
+ document.getElementById('highlightBtn').onclick = () => setTool('highlight');
2474
+ document.getElementById('shapesBtn').onclick = () => setTool('shape');
2475
+ document.getElementById('eraserBtn').onclick = () => setTool('eraser');
2476
+ document.getElementById('textBtn').onclick = () => setTool('text');
2477
+ document.getElementById('selectBtn').onclick = () => setTool('select');
2478
+
2479
+ // Undo / Redo / Clear All
2480
+ document.getElementById('undoBtn').onclick = () => performUndo();
2481
+ document.getElementById('redoBtn').onclick = () => performRedo();
2482
+ document.getElementById('clearAllBtn').onclick = () => performClearAll();
2483
+
2484
+ // Highlighter Colors
2485
+ document.querySelectorAll('#highlightColors .colorDot').forEach(dot => {
2486
+ dot.onclick = (e) => {
2487
+ e.stopPropagation();
2488
+ document.querySelectorAll('#highlightColors .colorDot').forEach(d => d.classList.remove('active'));
2489
+ dot.classList.add('active');
2490
+ highlightColor = dot.dataset.color;
2491
+ if (currentTool === 'highlight') currentColor = highlightColor;
2492
+ // Update preview
2493
+ document.getElementById('highlightWave').setAttribute('stroke', highlightColor);
2494
+ };
2495
+ });
2496
+
2497
+ // Pen Colors
2498
+ document.querySelectorAll('#drawColors .colorDot').forEach(dot => {
2499
+ dot.onclick = (e) => {
2500
+ e.stopPropagation();
2501
+ document.querySelectorAll('#drawColors .colorDot').forEach(d => d.classList.remove('active'));
2502
+ dot.classList.add('active');
2503
+ drawColor = dot.dataset.color;
2504
+ if (currentTool === 'pen') currentColor = drawColor;
2505
+ // Update preview
2506
+ document.getElementById('drawWave').setAttribute('stroke', drawColor);
2507
+ };
2508
+ });
2509
+
2510
+ // Highlighter Thickness Slider
2511
+ document.getElementById('highlightThickness').oninput = (e) => {
2512
+ highlightWidth = parseInt(e.target.value);
2513
+ if (currentTool === 'highlight') currentWidth = highlightWidth;
2514
+ // Update preview - highlighter uses width * 2 for display
2515
+ document.getElementById('highlightWave').setAttribute('stroke-width', highlightWidth * 2);
2516
+ };
2517
+
2518
+ // Pen Thickness Slider
2519
+ document.getElementById('drawThickness').oninput = (e) => {
2520
+ drawWidth = parseInt(e.target.value);
2521
+ if (currentTool === 'pen') currentWidth = drawWidth;
2522
+ // Update preview
2523
+ document.getElementById('drawWave').setAttribute('stroke-width', drawWidth);
2524
+ };
2525
+
2526
+ // Shape Selection
2527
+ document.querySelectorAll('.shapeBtn').forEach(btn => {
2528
+ btn.onclick = (e) => {
2529
+ e.stopPropagation();
2530
+ document.querySelectorAll('.shapeBtn').forEach(b => b.classList.remove('active'));
2531
+ btn.classList.add('active');
2532
+ currentShape = btn.dataset.shape;
2533
+ };
2534
+ });
2535
+
2536
+ // Shape Colors
2537
+ document.querySelectorAll('#shapeColors .colorDot').forEach(dot => {
2538
+ dot.onclick = (e) => {
2539
+ e.stopPropagation();
2540
+ document.querySelectorAll('#shapeColors .colorDot').forEach(d => d.classList.remove('active'));
2541
+ dot.classList.add('active');
2542
+ shapeColor = dot.dataset.color;
2543
+ if (currentTool === 'shape') currentColor = shapeColor;
2544
+ };
2545
+ });
2546
+
2547
+ // Shape Thickness Slider
2548
+ document.getElementById('shapeThickness').oninput = (e) => {
2549
+ shapeWidth = parseInt(e.target.value);
2550
+ if (currentTool === 'shape') currentWidth = shapeWidth;
2551
+ };
2552
+
2553
+ // Annotation Layer with Persistence
2554
+ async function injectAnnotationLayer(pageNum) {
2555
+ const pageView = pdfViewer.getPageView(pageNum - 1);
2556
+ if (!pageView?.div) return;
2557
+
2558
+ // Remove old SVG and abort its event listeners
2559
+ const oldSvg = pageView.div.querySelector('.annotationLayer');
2560
+ if (oldSvg) oldSvg.remove();
2561
+ const oldController = annotationAbortControllers.get(pageNum);
2562
+ if (oldController) oldController.abort();
2563
+
2564
+ // Get or calculate base dimensions (scale=1.0, current rotation)
2565
+ const currentRotation = pdfViewer.pagesRotation || 0;
2566
+ let baseDims = pageBaseDimensions.get(pageNum);
2567
+ if (!baseDims) {
2568
+ const page = await pdfDoc.getPage(pageNum);
2569
+ const baseViewport = page.getViewport({ scale: 1.0, rotation: currentRotation });
2570
+ baseDims = { width: baseViewport.width, height: baseViewport.height };
2571
+ pageBaseDimensions.set(pageNum, baseDims);
2572
+ }
2573
+
2574
+ // Create fresh SVG with viewBox matching rotated dimensions
2575
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2576
+ svg.setAttribute('class', 'annotationLayer');
2577
+ svg.setAttribute('viewBox', `0 0 ${baseDims.width} ${baseDims.height}`);
2578
+ svg.setAttribute('preserveAspectRatio', 'none');
2579
+ svg.style.width = '100%';
2580
+ svg.style.height = '100%';
2581
+ svg.dataset.page = pageNum;
2582
+ svg.dataset.viewboxWidth = baseDims.width;
2583
+ svg.dataset.viewboxHeight = baseDims.height;
2584
+
2585
+ pageView.div.appendChild(svg);
2586
+
2587
+
2588
+
2589
+ // Restore saved annotations for this page (with rotation transform if needed)
2590
+ if (annotationsStore.has(pageNum)) {
2591
+ const savedRot = annotationRotations.get(pageNum) || 0;
2592
+ const curRot = pdfViewer.pagesRotation || 0;
2593
+ const delta = (curRot - savedRot + 360) % 360;
2594
+
2595
+ if (delta === 0) {
2596
+ svg.innerHTML = annotationsStore.get(pageNum);
2597
+ } else {
2598
+ // Get unrotated page dimensions for transform calculation
2599
+ const page = await pdfDoc.getPage(pageNum);
2600
+ const unrotVP = page.getViewport({ scale: 1.0 });
2601
+ const W = unrotVP.width, H = unrotVP.height;
2602
+
2603
+ // Old viewBox dimensions (at saved rotation)
2604
+ let oW, oH;
2605
+ if (savedRot === 90 || savedRot === 270) { oW = H; oH = W; }
2606
+ else { oW = W; oH = H; }
2607
+
2608
+ let transform;
2609
+ if (delta === 90) transform = `translate(${oH},0) rotate(90)`;
2610
+ else if (delta === 180) transform = `translate(${oW},${oH}) rotate(180)`;
2611
+ else if (delta === 270) transform = `translate(0,${oW}) rotate(270)`;
2612
+
2613
+ svg.innerHTML = `<g transform="${transform}">${annotationsStore.get(pageNum)}</g>`;
2614
+
2615
+ // Update stored annotations & rotation to current
2616
+ annotationsStore.set(pageNum, svg.innerHTML);
2617
+ annotationRotations.set(pageNum, curRot);
2618
+
2619
+ // Note: No need to wrap stack entries anymore
2620
+ // Rotation is now stored per-entry, transforms applied on restore
2621
+ }
2622
+ }
2623
+
2624
+ // Bug fix: Use AbortController for cleanup when page re-renders
2625
+ const controller = new AbortController();
2626
+ const signal = controller.signal;
2627
+ annotationAbortControllers.set(pageNum, controller);
2628
+
2629
+ svg.addEventListener('mousedown', (e) => startDraw(e, pageNum), { signal });
2630
+ svg.addEventListener('mousemove', draw, { signal });
2631
+ svg.addEventListener('mouseup', () => stopDraw(pageNum), { signal });
2632
+ svg.addEventListener('mouseleave', () => stopDraw(pageNum), { signal });
2633
+
2634
+ // Touch support for tablets
2635
+ svg.addEventListener('touchstart', (e) => {
2636
+ // Prevent default to avoid scroll while drawing/selecting
2637
+ if (currentTool) e.preventDefault();
2638
+ startDraw(e, pageNum);
2639
+ }, { passive: false, signal });
2640
+ svg.addEventListener('touchmove', (e) => {
2641
+ if (currentTool) e.preventDefault();
2642
+ draw(e);
2643
+ }, { passive: false, signal });
2644
+ svg.addEventListener('touchend', () => stopDraw(pageNum), { signal });
2645
+ svg.addEventListener('touchcancel', () => stopDraw(pageNum), { signal });
2646
+
2647
+ svg.classList.toggle('active', annotationMode);
2648
+ }
2649
+
2650
+ // Strip transient classes, styles, and elements from SVG before saving
2651
+ function getCleanSvgInnerHTML(svg) {
2652
+ // Performance: Work on a cloned node to avoid modifying live DOM
2653
+ const clone = svg.cloneNode(true);
2654
+
2655
+ // Remove marquee rect if present
2656
+ const marquee = clone.querySelector('.marquee-rect');
2657
+ if (marquee) marquee.remove();
2658
+
2659
+ // Strip transient classes and inline styles from annotation elements
2660
+ const transientClasses = ['annotation-selected', 'annotation-multi-selected', 'annotation-dragging', 'just-selected'];
2661
+ clone.querySelectorAll('path, rect, ellipse, line, text').forEach(el => {
2662
+ transientClasses.forEach(cls => el.classList.remove(cls));
2663
+ // Remove inline cursor style added by multi-drag
2664
+ if (el.style.cursor) el.style.cursor = '';
2665
+ // Clean up empty style attribute
2666
+ if (el.getAttribute('style') === '') el.removeAttribute('style');
2667
+ // Clean up empty class attribute
2668
+ if (el.getAttribute('class') === '') el.removeAttribute('class');
2669
+ });
2670
+
2671
+ return clone.innerHTML.trim();
2672
+ }
2673
+
2674
+ // Helper: Apply rotation transform from savedRot to curRot
2675
+ // Uses clone-based flatten approach - updates each element's transform individually
2676
+ // This prevents nested <g> accumulation entirely
2677
+ function applyRotationTransform(html, savedRot, curRot, pageNum) {
2678
+ if (!html || !html.trim()) return html;
2679
+
2680
+ const delta = (curRot - savedRot + 360) % 360;
2681
+ if (delta === 0) return html;
2682
+
2683
+ // Calculate transform based on page dimensions
2684
+ const baseDims = pageBaseDimensions.get(pageNum);
2685
+ if (!baseDims) return html; // Fallback if no dims available
2686
+
2687
+ const W = baseDims.width, H = baseDims.height;
2688
+
2689
+ // Old viewBox dimensions (at saved rotation)
2690
+ let oW, oH;
2691
+ if (savedRot === 90 || savedRot === 270) { oW = H; oH = W; }
2692
+ else { oW = W; oH = H; }
2693
+
2694
+ let rotationTransform;
2695
+ if (delta === 90) rotationTransform = `translate(${oH},0) rotate(90)`;
2696
+ else if (delta === 180) rotationTransform = `translate(${oW},${oH}) rotate(180)`;
2697
+ else if (delta === 270) rotationTransform = `translate(0,${oW}) rotate(270)`;
2698
+ else return html;
2699
+
2700
+ // Clone-based flatten: Apply transform to each top-level element individually
2701
+ const tempContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2702
+ tempContainer.innerHTML = html;
2703
+
2704
+ // Process each top-level child element
2705
+ Array.from(tempContainer.children).forEach(child => {
2706
+ const existingTransform = child.getAttribute('transform') || '';
2707
+ // Prepend rotation transform (rotation first, then existing)
2708
+ const newTransform = existingTransform
2709
+ ? `${rotationTransform} ${existingTransform}`
2710
+ : rotationTransform;
2711
+ child.setAttribute('transform', newTransform);
2712
+ });
2713
+
2714
+ return tempContainer.innerHTML;
2715
+ }
2716
+
2717
+ // Save annotations for a page (with undo history)
2718
+ function saveAnnotations(pageNum) {
2719
+ const pageView = pdfViewer.getPageView(pageNum - 1);
2720
+ const svg = pageView?.div?.querySelector('.annotationLayer');
2721
+ if (!svg) return;
2722
+
2723
+ // Push previous state to undo stack before saving new state
2724
+ const previousState = annotationsStore.get(pageNum) || '';
2725
+ const newState = getCleanSvgInnerHTML(svg);
2726
+
2727
+ // Only push to history if state actually changed
2728
+ if (previousState !== newState) {
2729
+ if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
2730
+ const stack = undoStacks.get(pageNum);
2731
+ // Store as {html, rotation} object to avoid nested <g> wrap accumulation
2732
+ stack.push({ html: previousState, rotation: annotationRotations.get(pageNum) || 0 });
2733
+ if (stack.length > MAX_HISTORY) stack.shift();
2734
+
2735
+ // Clear redo stack on new action
2736
+ redoStacks.delete(pageNum);
2737
+ }
2738
+
2739
+ if (newState) {
2740
+ annotationsStore.set(pageNum, newState);
2741
+ annotationRotations.set(pageNum, pdfViewer.pagesRotation || 0);
2742
+ } else {
2743
+ annotationsStore.delete(pageNum);
2744
+ annotationRotations.delete(pageNum);
2745
+ }
2746
+
2747
+ updateUndoRedoButtons();
2748
+ }
2749
+
2750
+ function updateUndoRedoButtons() {
2751
+ const pageNum = pdfViewer ? pdfViewer.currentPageNumber : 0;
2752
+ const undoBtn = document.getElementById('undoBtn');
2753
+ const redoBtn = document.getElementById('redoBtn');
2754
+ const undoStack = undoStacks.get(pageNum);
2755
+ const redoStack = redoStacks.get(pageNum);
2756
+ undoBtn.disabled = !undoStack || undoStack.length === 0;
2757
+ redoBtn.disabled = !redoStack || redoStack.length === 0;
2758
+ }
2759
+
2760
+ function performUndo() {
2761
+ const pageNum = pdfViewer.currentPageNumber;
2762
+ const stack = undoStacks.get(pageNum);
2763
+ if (!stack || stack.length === 0) return;
2764
+
2765
+ const pageView = pdfViewer.getPageView(pageNum - 1);
2766
+ const svg = pageView?.div?.querySelector('.annotationLayer');
2767
+ if (!svg) return;
2768
+
2769
+ // Save current state to redo stack with rotation
2770
+ if (!redoStacks.has(pageNum)) redoStacks.set(pageNum, []);
2771
+ const redoStack = redoStacks.get(pageNum);
2772
+ redoStack.push({ html: getCleanSvgInnerHTML(svg), rotation: pdfViewer.pagesRotation || 0 });
2773
+ if (redoStack.length > MAX_HISTORY) redoStack.shift();
2774
+
2775
+ // Restore previous state with rotation transform if needed
2776
+ const entry = stack.pop();
2777
+ const previousHtml = typeof entry === 'object' ? entry.html : entry;
2778
+ const savedRot = typeof entry === 'object' ? entry.rotation : (annotationRotations.get(pageNum) || 0);
2779
+ const curRot = pdfViewer.pagesRotation || 0;
2780
+
2781
+ svg.innerHTML = applyRotationTransform(previousHtml, savedRot, curRot, pageNum);
2782
+
2783
+ // Update store
2784
+ if (previousHtml.trim()) {
2785
+ annotationsStore.set(pageNum, svg.innerHTML);
2786
+ annotationRotations.set(pageNum, curRot);
2787
+ } else {
2788
+ annotationsStore.delete(pageNum);
2789
+ annotationRotations.delete(pageNum);
2790
+ }
2791
+
2792
+ clearAnnotationSelection();
2793
+ updateUndoRedoButtons();
2794
+ }
2795
+
2796
+ function performRedo() {
2797
+ const pageNum = pdfViewer.currentPageNumber;
2798
+ const stack = redoStacks.get(pageNum);
2799
+ if (!stack || stack.length === 0) return;
2800
+
2801
+ const pageView = pdfViewer.getPageView(pageNum - 1);
2802
+ const svg = pageView?.div?.querySelector('.annotationLayer');
2803
+ if (!svg) return;
2804
+
2805
+ // Save current state to undo stack with rotation
2806
+ if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
2807
+ const undoStack = undoStacks.get(pageNum);
2808
+ undoStack.push({ html: getCleanSvgInnerHTML(svg), rotation: pdfViewer.pagesRotation || 0 });
2809
+ if (undoStack.length > MAX_HISTORY) undoStack.shift();
2810
+
2811
+ // Restore redo state with rotation transform if needed
2812
+ const entry = stack.pop();
2813
+ const redoHtml = typeof entry === 'object' ? entry.html : entry;
2814
+ const savedRot = typeof entry === 'object' ? entry.rotation : (annotationRotations.get(pageNum) || 0);
2815
+ const curRot = pdfViewer.pagesRotation || 0;
2816
+
2817
+ svg.innerHTML = applyRotationTransform(redoHtml, savedRot, curRot, pageNum);
2818
+
2819
+ // Update store
2820
+ if (redoHtml.trim()) {
2821
+ annotationsStore.set(pageNum, svg.innerHTML);
2822
+ annotationRotations.set(pageNum, curRot);
2823
+ } else {
2824
+ annotationsStore.delete(pageNum);
2825
+ annotationRotations.delete(pageNum);
2826
+ }
2827
+
2828
+ clearAnnotationSelection();
2829
+ updateUndoRedoButtons();
2830
+ }
2831
+
2832
+ function performClearAll() {
2833
+ const pageNum = pdfViewer.currentPageNumber;
2834
+ const pageView = pdfViewer.getPageView(pageNum - 1);
2835
+ const svg = pageView?.div?.querySelector('.annotationLayer');
2836
+ if (!svg || !svg.innerHTML.trim()) return;
2837
+
2838
+ // Save current state to undo stack with rotation
2839
+ if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
2840
+ const stack = undoStacks.get(pageNum);
2841
+ stack.push({ html: svg.innerHTML, rotation: pdfViewer.pagesRotation || 0 });
2842
+ if (stack.length > MAX_HISTORY) stack.shift();
2843
+
2844
+ // Clear redo stack
2845
+ redoStacks.delete(pageNum);
2846
+
2847
+ // Clear all annotations
2848
+ svg.innerHTML = '';
2849
+ annotationsStore.delete(pageNum);
2850
+ annotationRotations.delete(pageNum);
2851
+
2852
+ clearAnnotationSelection();
2853
+ updateUndoRedoButtons();
2854
+ }
2855
+
2856
+ function startDraw(e, pageNum) {
2857
+ if (!annotationMode || !currentTool) return;
2858
+
2859
+ e.preventDefault(); // Prevent text selection
2860
+
2861
+ const svg = e.currentTarget;
2862
+ if (!svg || !svg.dataset.viewboxWidth) return; // Defensive check
2863
+
2864
+ // Handle select tool separately
2865
+ if (currentTool === 'select') {
2866
+ if (handleSelectMouseDown(e, svg, pageNum)) {
2867
+ return; // Select tool handled the event
2868
+ }
2869
+ }
2870
+
2871
+ isDrawing = true;
2872
+ currentDrawingPage = pageNum;
2873
+ currentSvg = svg; // Store reference
2874
+
2875
+ // Convert screen coords to viewBox coords (rotation-aware)
2876
+ const coords = getEventCoords(e);
2877
+ const vb = screenToViewBox(svg, coords.clientX, coords.clientY);
2878
+ const x = vb.x;
2879
+ const y = vb.y;
2880
+ const scaleX = vb.scaleX;
2881
+ const scaleY = vb.scaleY;
2882
+
2883
+ if (currentTool === 'eraser') {
2884
+ eraseAt(svg, x, y, scaleX);
2885
+ // Performance: Don't save here - isDrawing is true, stopDraw will save on mouseup
2886
+ return;
2887
+ }
2888
+
2889
+ // Text tool - create/edit/drag text
2890
+ if (currentTool === 'text') {
2891
+ // Check if clicked on existing text element
2892
+ const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
2893
+ const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
2894
+
2895
+ if (existingText) {
2896
+ // Start dragging (double-click will edit via separate handler)
2897
+ startTextDrag(e, existingText, svg, scaleX, pageNum);
2898
+ } else {
2899
+ // Create new text
2900
+ showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
2901
+ }
2902
+ return;
2903
+ }
2904
+
2905
+ // Shape tool - create shapes
2906
+ if (currentTool === 'shape') {
2907
+ isDrawing = true;
2908
+ // Store start position for shape drawing
2909
+ svg.dataset.shapeStartX = x;
2910
+ svg.dataset.shapeStartY = y;
2911
+ svg.dataset.shapeScaleX = scaleX;
2912
+ svg.dataset.shapeScaleY = scaleY;
2913
+
2914
+ let shapeEl;
2915
+ if (currentShape === 'rectangle') {
2916
+ shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
2917
+ shapeEl.setAttribute('x', x);
2918
+ shapeEl.setAttribute('y', y);
2919
+ shapeEl.setAttribute('width', 0);
2920
+ shapeEl.setAttribute('height', 0);
2921
+ } else if (currentShape === 'circle') {
2922
+ shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
2923
+ shapeEl.setAttribute('cx', x);
2924
+ shapeEl.setAttribute('cy', y);
2925
+ shapeEl.setAttribute('rx', 0);
2926
+ shapeEl.setAttribute('ry', 0);
2927
+ } else if (currentShape === 'line' || currentShape === 'arrow') {
2928
+ shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'line');
2929
+ shapeEl.setAttribute('x1', x);
2930
+ shapeEl.setAttribute('y1', y);
2931
+ shapeEl.setAttribute('x2', x);
2932
+ shapeEl.setAttribute('y2', y);
2933
+ }
2934
+
2935
+ shapeEl.setAttribute('stroke', currentColor);
2936
+ shapeEl.setAttribute('stroke-width', String(currentWidth * scaleX));
2937
+ shapeEl.setAttribute('fill', 'none');
2938
+ shapeEl.classList.add('current-shape');
2939
+ svg.appendChild(shapeEl);
2940
+ return;
2941
+ }
2942
+
2943
+ currentPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2944
+ currentPath.setAttribute('stroke', currentColor);
2945
+ currentPath.setAttribute('fill', 'none');
2946
+
2947
+ if (currentTool === 'highlight') {
2948
+ // Highlighter uses stroke size * 5 for thicker strokes
2949
+ currentPath.setAttribute('stroke-width', String(currentWidth * 5 * scaleX));
2950
+ currentPath.setAttribute('stroke-opacity', '0.35');
2951
+ } else {
2952
+ currentPath.setAttribute('stroke-width', String(currentWidth * scaleX));
2953
+ currentPath.setAttribute('stroke-opacity', '1');
2954
+ }
2955
+
2956
+ currentPath.setAttribute('d', `M${x.toFixed(2)},${y.toFixed(2)}`);
2957
+ svg.appendChild(currentPath);
2958
+ }
2959
+
2960
+ function draw(e) {
2961
+ if (!isDrawing || !currentSvg) return;
2962
+
2963
+ // Bug fix: Check if SVG is still in DOM (prevents stale reference)
2964
+ if (!currentSvg.isConnected) {
2965
+ isDrawing = false;
2966
+ currentPath = null;
2967
+ currentSvg = null;
2968
+ currentDrawingPage = null;
2969
+ return;
2970
+ }
2971
+
2972
+ e.preventDefault(); // Prevent text selection
2973
+
2974
+ const svg = currentSvg; // Use stored reference
2975
+ if (!svg || !svg.dataset.viewboxWidth) return;
2976
+
2977
+ // Convert screen coords to viewBox coords (rotation-aware)
2978
+ const coords = getEventCoords(e);
2979
+ const vb = screenToViewBox(svg, coords.clientX, coords.clientY);
2980
+ const x = vb.x;
2981
+ const y = vb.y;
2982
+ const scaleX = vb.scaleX;
2983
+
2984
+ if (currentTool === 'eraser') {
2985
+ eraseAt(svg, x, y, scaleX);
2986
+ // Performance: Don't save on every mousemove - let mouseup handle it
2987
+ return;
2988
+ }
2989
+
2990
+ // Shape tool - update shape size
2991
+ if (currentTool === 'shape') {
2992
+ const shapeEl = svg.querySelector('.current-shape');
2993
+ if (!shapeEl) return;
2994
+
2995
+ const startX = parseFloat(svg.dataset.shapeStartX);
2996
+ const startY = parseFloat(svg.dataset.shapeStartY);
2997
+
2998
+ if (currentShape === 'rectangle') {
2999
+ const width = Math.abs(x - startX);
3000
+ const height = Math.abs(y - startY);
3001
+ shapeEl.setAttribute('x', Math.min(x, startX));
3002
+ shapeEl.setAttribute('y', Math.min(y, startY));
3003
+ shapeEl.setAttribute('width', width);
3004
+ shapeEl.setAttribute('height', height);
3005
+ } else if (currentShape === 'circle') {
3006
+ const rx = Math.abs(x - startX) / 2;
3007
+ const ry = Math.abs(y - startY) / 2;
3008
+ shapeEl.setAttribute('cx', (startX + x) / 2);
3009
+ shapeEl.setAttribute('cy', (startY + y) / 2);
3010
+ shapeEl.setAttribute('rx', rx);
3011
+ shapeEl.setAttribute('ry', ry);
3012
+ } else if (currentShape === 'line' || currentShape === 'arrow' || currentShape === 'callout') {
3013
+ shapeEl.setAttribute('x2', x);
3014
+ shapeEl.setAttribute('y2', y);
3015
+ }
3016
+ return;
3017
+ }
3018
+
3019
+ // RAF throttle: buffer segments and flush in animation frame
3020
+ if (currentPath) {
3021
+ pathSegments.push(`L${x.toFixed(2)},${y.toFixed(2)}`);
3022
+
3023
+ if (!drawRAF) {
3024
+ drawRAF = requestAnimationFrame(() => {
3025
+ if (currentPath && pathSegments.length > 0) {
3026
+ currentPath.setAttribute('d', currentPath.getAttribute('d') + ' ' + pathSegments.join(' '));
3027
+ pathSegments = [];
3028
+ }
3029
+ drawRAF = null;
3030
+ });
3031
+ }
3032
+ }
3033
+ }
3034
+
3035
+ function stopDraw(pageNum) {
3036
+ // Flush any pending path segments before stopping
3037
+ if (drawRAF) {
3038
+ cancelAnimationFrame(drawRAF);
3039
+ drawRAF = null;
3040
+ }
3041
+ if (currentPath && pathSegments.length > 0) {
3042
+ currentPath.setAttribute('d', currentPath.getAttribute('d') + ' ' + pathSegments.join(' '));
3043
+ pathSegments = [];
3044
+ }
3045
+
3046
+ // Handle arrow marker
3047
+ if (currentTool === 'shape' && currentShape === 'arrow' && currentSvg) {
3048
+ const shapeEl = currentSvg.querySelector('.current-shape');
3049
+ if (shapeEl && shapeEl.tagName === 'line') {
3050
+ // Create arrow head as a group
3051
+ const x1 = parseFloat(shapeEl.getAttribute('x1'));
3052
+ const y1 = parseFloat(shapeEl.getAttribute('y1'));
3053
+ const x2 = parseFloat(shapeEl.getAttribute('x2'));
3054
+ const y2 = parseFloat(shapeEl.getAttribute('y2'));
3055
+
3056
+ // Calculate arrow head
3057
+ const angle = Math.atan2(y2 - y1, x2 - x1);
3058
+ const headLength = 15 * parseFloat(currentSvg.dataset.shapeScaleX || 1);
3059
+
3060
+ const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3061
+ const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
3062
+ const p1y = y2 - headLength * Math.sin(angle - Math.PI / 6);
3063
+ const p2x = x2 - headLength * Math.cos(angle + Math.PI / 6);
3064
+ const p2y = y2 - headLength * Math.sin(angle + Math.PI / 6);
3065
+
3066
+ arrowHead.setAttribute('d', `M${x2},${y2} L${p1x},${p1y} M${x2},${y2} L${p2x},${p2y}`);
3067
+ arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
3068
+ arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
3069
+ arrowHead.setAttribute('fill', 'none');
3070
+ currentSvg.appendChild(arrowHead);
3071
+ }
3072
+ }
3073
+
3074
+ // Handle callout - arrow with text at the start, pointing to end
3075
+ // UX: Click where you want text box, drag to point at something
3076
+ if (currentTool === 'shape' && currentShape === 'callout' && currentSvg) {
3077
+ const shapeEl = currentSvg.querySelector('.current-shape');
3078
+ if (shapeEl && shapeEl.tagName === 'line') {
3079
+ const x1 = parseFloat(shapeEl.getAttribute('x1')); // Start - where text box goes
3080
+ const y1 = parseFloat(shapeEl.getAttribute('y1'));
3081
+ const x2 = parseFloat(shapeEl.getAttribute('x2')); // End - where arrow points
3082
+ const y2 = parseFloat(shapeEl.getAttribute('y2'));
3083
+
3084
+ // Only create callout if line has been drawn (not just a click)
3085
+ if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) {
3086
+ const scaleX = parseFloat(currentSvg.dataset.shapeScaleX || 1);
3087
+
3088
+ // Arrow head points TO the end (x2,y2) - where user wants to point at
3089
+ const angle = Math.atan2(y2 - y1, x2 - x1);
3090
+ const headLength = 12 * scaleX;
3091
+
3092
+ const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3093
+ const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
3094
+ const p1y = y2 - headLength * Math.sin(angle - Math.PI / 6);
3095
+ const p2x = x2 - headLength * Math.cos(angle + Math.PI / 6);
3096
+ const p2y = y2 - headLength * Math.sin(angle + Math.PI / 6);
3097
+
3098
+ arrowHead.setAttribute('d', `M${x2},${y2} L${p1x},${p1y} M${x2},${y2} L${p2x},${p2y}`);
3099
+ arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
3100
+ arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
3101
+ arrowHead.setAttribute('fill', 'none');
3102
+ arrowHead.classList.add('callout-arrow');
3103
+ currentSvg.appendChild(arrowHead);
3104
+
3105
+ // Store references for text editor
3106
+ const svg = currentSvg;
3107
+ const currentPageNum = currentDrawingPage;
3108
+ const arrowColor = shapeEl.getAttribute('stroke');
3109
+
3110
+ // Calculate screen position for text editor at START of arrow (x1,y1)
3111
+ // This is where the user clicked first - where they want the text
3112
+ const rect = svg.getBoundingClientRect();
3113
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
3114
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
3115
+ const screenX = rect.left + (x1 / viewBoxWidth) * rect.width;
3116
+ const screenY = rect.top + (y1 / viewBoxHeight) * rect.height;
3117
+
3118
+ // Remove the current-shape class before showing editor
3119
+ shapeEl.classList.remove('current-shape');
3120
+
3121
+ // Save first, then open text editor
3122
+ saveAnnotations(currentPageNum);
3123
+
3124
+ // Open text editor at the START of the arrow (where user clicked)
3125
+ setTimeout(() => {
3126
+ showTextEditor(screenX, screenY, svg, x1, y1, scaleX, currentPageNum, null, arrowColor);
3127
+ }, 50);
3128
+
3129
+ // Reset state
3130
+ isDrawing = false;
3131
+ currentPath = null;
3132
+ currentSvg = null;
3133
+ currentDrawingPage = null;
3134
+ return; // Exit early, text editor will handle the rest
3135
+ }
3136
+ }
3137
+ }
3138
+
3139
+ // Remove the current-shape class
3140
+ if (currentSvg) {
3141
+ const shapeEl = currentSvg.querySelector('.current-shape');
3142
+ if (shapeEl) shapeEl.classList.remove('current-shape');
3143
+ }
3144
+
3145
+ if (isDrawing && currentDrawingPage) {
3146
+ saveAnnotations(currentDrawingPage);
3147
+ }
3148
+ isDrawing = false;
3149
+ currentPath = null;
3150
+ currentSvg = null;
3151
+ currentDrawingPage = null;
3152
+ }
3153
+
3154
+ // Text Drag-and-Drop
3155
+ let draggedText = null;
3156
+ let dragStartX = 0;
3157
+ let dragStartY = 0;
3158
+ let textOriginalX = 0;
3159
+ let textOriginalY = 0;
3160
+ let hasDragged = false;
3161
+
3162
+ function startTextDrag(e, textEl, svg, scaleX, pageNum) {
3163
+ e.preventDefault();
3164
+ e.stopPropagation();
3165
+
3166
+ draggedText = textEl;
3167
+ textEl.classList.add('dragging');
3168
+ hasDragged = false;
3169
+
3170
+ dragStartX = e.clientX;
3171
+ dragStartY = e.clientY;
3172
+ textOriginalX = parseFloat(textEl.getAttribute('x'));
3173
+ textOriginalY = parseFloat(textEl.getAttribute('y'));
3174
+
3175
+ function onMouseMove(ev) {
3176
+ const dxScreen = ev.clientX - dragStartX;
3177
+ const dyScreen = ev.clientY - dragStartY;
3178
+ // Convert screen delta to viewBox delta (rotation-aware)
3179
+ const vbDelta = screenDeltaToViewBox(svg, dxScreen, dyScreen);
3180
+
3181
+ if (Math.abs(vbDelta.dx) > 2 || Math.abs(vbDelta.dy) > 2) {
3182
+ hasDragged = true;
3183
+ }
3184
+
3185
+ textEl.setAttribute('x', (textOriginalX + vbDelta.dx).toFixed(2));
3186
+ textEl.setAttribute('y', (textOriginalY + vbDelta.dy).toFixed(2));
3187
+ }
3188
+
3189
+ function onMouseUp(ev) {
3190
+ document.removeEventListener('mousemove', onMouseMove);
3191
+ document.removeEventListener('mouseup', onMouseUp);
3192
+ textEl.classList.remove('dragging');
3193
+
3194
+ if (hasDragged) {
3195
+ // Moved - save position
3196
+ saveAnnotations(pageNum);
3197
+ } else {
3198
+ // Not moved - short click = edit
3199
+ const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
3200
+ const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
3201
+ const svgX = parseFloat(textEl.getAttribute('x'));
3202
+ const svgY = parseFloat(textEl.getAttribute('y'));
3203
+ // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
3204
+ showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
3205
+ }
3206
+
3207
+ draggedText = null;
3208
+ }
3209
+
3210
+ document.addEventListener('mousemove', onMouseMove);
3211
+ document.addEventListener('mouseup', onMouseUp);
3212
+ }
3213
+
3214
+ // Inline Text Editor
3215
+ let textFontSize = 20;
3216
+
3217
+ function showTextEditor(screenX, screenY, svg, svgX, svgY, scale, pageNum, existingTextEl = null, overrideColor = null) {
3218
+ // Remove existing editor if any
3219
+ const existingOverlay = document.querySelector('.textEditorOverlay');
3220
+ if (existingOverlay) existingOverlay.remove();
3221
+
3222
+ // Use override color (for callout) or current color
3223
+ let textColor = overrideColor || currentColor;
3224
+
3225
+ // If editing existing text, get its properties
3226
+ let editingText = null;
3227
+ if (existingTextEl && typeof existingTextEl === 'object' && existingTextEl.textContent !== undefined) {
3228
+ editingText = existingTextEl.textContent;
3229
+ textFontSize = parseFloat(existingTextEl.getAttribute('font-size')) / scale || 20;
3230
+ // Use existing text's color
3231
+ textColor = existingTextEl.getAttribute('fill') || textColor;
3232
+ }
3233
+
3234
+ // Create overlay
3235
+ const overlay = document.createElement('div');
3236
+ overlay.className = 'textEditorOverlay';
3237
+
3238
+ // Create editor box
3239
+ const box = document.createElement('div');
3240
+ box.className = 'textEditorBox';
3241
+ box.style.left = screenX + 'px';
3242
+ box.style.top = screenY + 'px';
3243
+
3244
+ // Input area
3245
+ const input = document.createElement('div');
3246
+ input.className = 'textEditorInput';
3247
+ input.contentEditable = true;
3248
+ input.style.color = textColor;
3249
+ input.style.fontSize = textFontSize + 'px';
3250
+ if (editingText) {
3251
+ input.textContent = editingText;
3252
+ }
3253
+
3254
+ // Toolbar
3255
+ const toolbar = document.createElement('div');
3256
+ toolbar.className = 'textEditorToolbar';
3257
+
3258
+ // Color palette
3259
+ const colorsDiv = document.createElement('div');
3260
+ colorsDiv.className = 'textEditorColors';
3261
+ const textEditorColors = ['#000000', '#e81224', '#0078d4', '#16c60c', '#fff100', '#886ce4', '#ff8c00', '#ffffff'];
3262
+ let activeColor = textColor;
3263
+
3264
+ textEditorColors.forEach(c => {
3265
+ const dot = document.createElement('div');
3266
+ dot.className = 'textEditorColorDot' + (c === activeColor ? ' active' : '');
3267
+ dot.style.background = c;
3268
+ if (c === '#ffffff') dot.style.border = '2px solid #ccc';
3269
+ dot.onclick = (e) => {
3270
+ e.stopPropagation();
3271
+ activeColor = c;
3272
+ input.style.color = c;
3273
+ colorsDiv.querySelectorAll('.textEditorColorDot').forEach(d => d.classList.remove('active'));
3274
+ dot.classList.add('active');
3275
+ };
3276
+ colorsDiv.appendChild(dot);
3277
+ });
3278
+
3279
+ // Font size group: A⁻ [size] A⁺
3280
+ const sizeGroup = document.createElement('div');
3281
+ sizeGroup.className = 'textEditorSizeGroup';
3282
+
3283
+ const sizeLabel = document.createElement('span');
3284
+ sizeLabel.className = 'textEditorSizeLabel';
3285
+ sizeLabel.textContent = textFontSize;
3286
+
3287
+ const decreaseBtn = document.createElement('button');
3288
+ decreaseBtn.className = 'textEditorBtn';
3289
+ decreaseBtn.innerHTML = 'A<sup>-</sup>';
3290
+ decreaseBtn.onclick = (e) => {
3291
+ e.stopPropagation();
3292
+ if (textFontSize > 10) {
3293
+ textFontSize -= 2;
3294
+ input.style.fontSize = textFontSize + 'px';
3295
+ sizeLabel.textContent = textFontSize;
3296
+ }
3297
+ };
3298
+
3299
+ const increaseBtn = document.createElement('button');
3300
+ increaseBtn.className = 'textEditorBtn';
3301
+ increaseBtn.innerHTML = 'A<sup>+</sup>';
3302
+ increaseBtn.onclick = (e) => {
3303
+ e.stopPropagation();
3304
+ if (textFontSize < 60) {
3305
+ textFontSize += 2;
3306
+ input.style.fontSize = textFontSize + 'px';
3307
+ sizeLabel.textContent = textFontSize;
3308
+ }
3309
+ };
3310
+
3311
+ sizeGroup.appendChild(decreaseBtn);
3312
+ sizeGroup.appendChild(sizeLabel);
3313
+ sizeGroup.appendChild(increaseBtn);
3314
+
3315
+ // Delete button
3316
+ const deleteBtn = document.createElement('button');
3317
+ deleteBtn.className = 'textEditorBtn delete';
3318
+ deleteBtn.innerHTML = '🗑️';
3319
+ deleteBtn.onclick = (e) => {
3320
+ e.stopPropagation();
3321
+ if (existingTextEl) {
3322
+ existingTextEl.remove();
3323
+ saveAnnotations(pageNum);
3324
+ }
3325
+ overlay.remove();
3326
+ };
3327
+
3328
+ toolbar.appendChild(colorsDiv);
3329
+ toolbar.appendChild(sizeGroup);
3330
+ toolbar.appendChild(deleteBtn);
3331
+
3332
+ box.appendChild(input);
3333
+ box.appendChild(toolbar);
3334
+ overlay.appendChild(box);
3335
+ document.body.appendChild(overlay);
3336
+
3337
+ // Focus input and select all if editing
3338
+ setTimeout(() => {
3339
+ input.focus();
3340
+ if (editingText) {
3341
+ const range = document.createRange();
3342
+ range.selectNodeContents(input);
3343
+ const sel = window.getSelection();
3344
+ sel.removeAllRanges();
3345
+ sel.addRange(range);
3346
+ }
3347
+ }, 50);
3348
+
3349
+ // Confirm on click outside or Enter
3350
+ function confirmText() {
3351
+ const text = input.textContent.trim();
3352
+ if (text) {
3353
+ if (existingTextEl) {
3354
+ // Update existing text element
3355
+ existingTextEl.textContent = text;
3356
+ existingTextEl.setAttribute('fill', activeColor);
3357
+ existingTextEl.setAttribute('font-size', String(textFontSize * scale));
3358
+ } else {
3359
+ // Create new text element
3360
+ const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3361
+ textEl.setAttribute('x', svgX.toFixed(2));
3362
+ textEl.setAttribute('y', svgY.toFixed(2));
3363
+ textEl.setAttribute('fill', activeColor);
3364
+ textEl.setAttribute('font-size', String(textFontSize * scale));
3365
+ textEl.setAttribute('font-family', 'Segoe UI, Arial, sans-serif');
3366
+ textEl.textContent = text;
3367
+ svg.appendChild(textEl);
3368
+ }
3369
+ saveAnnotations(pageNum);
3370
+ } else if (existingTextEl) {
3371
+ // Empty text = delete existing
3372
+ existingTextEl.remove();
3373
+ saveAnnotations(pageNum);
3374
+ }
3375
+ overlay.remove();
3376
+ }
3377
+
3378
+ overlay.addEventListener('click', (e) => {
3379
+ if (e.target === overlay) confirmText();
3380
+ });
3381
+
3382
+ input.addEventListener('keydown', (e) => {
3383
+ if (e.key === 'Enter' && !e.shiftKey) {
3384
+ e.preventDefault();
3385
+ confirmText();
3386
+ }
3387
+ if (e.key === 'Escape') {
3388
+ overlay.remove();
3389
+ }
3390
+ });
3391
+ }
3392
+
3393
+ function eraseAt(svg, x, y, scale = 1) {
3394
+ const hitRadius = 15 * scale; // Scale hit radius with viewBox
3395
+ // Erase paths, text, and shape elements (rect, ellipse, line)
3396
+ svg.querySelectorAll('path, text, rect, ellipse, line').forEach(el => {
3397
+ const bbox = el.getBBox();
3398
+ if (x >= bbox.x - hitRadius && x <= bbox.x + bbox.width + hitRadius &&
3399
+ y >= bbox.y - hitRadius && y <= bbox.y + bbox.height + hitRadius) {
3400
+ el.remove();
3401
+ }
3402
+ });
3403
+
3404
+ // Also erase text highlights (in separate container)
3405
+ const pageDiv = svg.closest('.page');
3406
+ if (pageDiv) {
3407
+ const highlightContainer = pageDiv.querySelector('.textHighlightContainer');
3408
+ if (highlightContainer) {
3409
+ const pageRect = pageDiv.getBoundingClientRect();
3410
+ const vbW = parseFloat(svg.dataset.viewboxWidth);
3411
+ const vbH = parseFloat(svg.dataset.viewboxHeight);
3412
+ // Convert viewBox coords to percentage (independent of rotation)
3413
+ const screenXPercent = (x / vbW) * 100;
3414
+ const screenYPercent = (y / vbH) * 100;
3415
+
3416
+ highlightContainer.querySelectorAll('.textHighlight').forEach(el => {
3417
+ const left = parseFloat(el.style.left); // Already in %
3418
+ const top = parseFloat(el.style.top);
3419
+ const width = parseFloat(el.style.width);
3420
+ const height = parseFloat(el.style.height);
3421
+
3422
+ if (screenXPercent >= left - 2 && screenXPercent <= left + width + 2 &&
3423
+ screenYPercent >= top - 2 && screenYPercent <= top + height + 2) {
3424
+ el.remove();
3425
+ // Save changes
3426
+ const pageNum = parseInt(pageDiv.dataset.pageNumber);
3427
+ saveTextHighlights(pageNum, pageDiv);
3428
+ }
3429
+ });
3430
+ }
3431
+ }
3432
+ }
3433
+
3434
+ // ==========================================
3435
+ // TEXT SELECTION HIGHLIGHTING (Adobe/Edge style)
3436
+ // ==========================================
3437
+ let highlightPopup = null;
3438
+
3439
+ function removeHighlightPopup() {
3440
+ if (highlightPopup) {
3441
+ highlightPopup.remove();
3442
+ highlightPopup = null;
3443
+ }
3444
+ }
3445
+
3446
+ function getSelectionRects() {
3447
+ const selection = window.getSelection();
3448
+ if (!selection || selection.isCollapsed || !selection.rangeCount) return null;
3449
+
3450
+ const range = selection.getRangeAt(0);
3451
+ const rects = range.getClientRects();
3452
+ if (rects.length === 0) return null;
3453
+
3454
+ // Find which page the selection is in
3455
+ const startNode = range.startContainer.parentElement;
3456
+ const textLayer = startNode?.closest('.textLayer');
3457
+ if (!textLayer) return null;
3458
+
3459
+ const pageDiv = textLayer.closest('.page');
3460
+ if (!pageDiv) return null;
3461
+
3462
+ const pageNum = parseInt(pageDiv.dataset.pageNumber);
3463
+ const pageRect = pageDiv.getBoundingClientRect();
3464
+
3465
+ // Convert rects to page-relative coordinates
3466
+ const relativeRects = [];
3467
+ for (let i = 0; i < rects.length; i++) {
3468
+ const rect = rects[i];
3469
+ relativeRects.push({
3470
+ x: rect.left - pageRect.left,
3471
+ y: rect.top - pageRect.top,
3472
+ width: rect.width,
3473
+ height: rect.height
3474
+ });
3475
+ }
3476
+
3477
+ return { pageNum, pageDiv, relativeRects, lastRect: rects[rects.length - 1] };
3478
+ }
3479
+
3480
+ function createTextHighlights(pageDiv, rects, color) {
3481
+ // Find or create highlight container
3482
+ let highlightContainer = pageDiv.querySelector('.textHighlightContainer');
3483
+ if (!highlightContainer) {
3484
+ highlightContainer = document.createElement('div');
3485
+ highlightContainer.className = 'textHighlightContainer';
3486
+ highlightContainer.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
3487
+ pageDiv.insertBefore(highlightContainer, pageDiv.firstChild);
3488
+ }
3489
+
3490
+ // Get page dimensions for percentage calculation
3491
+ const pageRect = pageDiv.getBoundingClientRect();
3492
+ const pageWidth = pageRect.width;
3493
+ const pageHeight = pageRect.height;
3494
+
3495
+ // Add highlight rectangles with percentage positioning
3496
+ rects.forEach(rect => {
3497
+ const div = document.createElement('div');
3498
+ div.className = 'textHighlight';
3499
+
3500
+ // Convert to percentages for zoom-independent positioning
3501
+ const leftPercent = (rect.x / pageWidth) * 100;
3502
+ const topPercent = (rect.y / pageHeight) * 100;
3503
+ const widthPercent = (rect.width / pageWidth) * 100;
3504
+ const heightPercent = (rect.height / pageHeight) * 100;
3505
+
3506
+ div.style.cssText = `
3507
+ left: ${leftPercent}%;
3508
+ top: ${topPercent}%;
3509
+ width: ${widthPercent}%;
3510
+ height: ${heightPercent}%;
3511
+ background: ${color};
3512
+ opacity: 0.35;
3513
+ `;
3514
+ highlightContainer.appendChild(div);
3515
+ });
3516
+
3517
+ // Save to annotations store
3518
+ const pageNum = parseInt(pageDiv.dataset.pageNumber);
3519
+ saveTextHighlights(pageNum, pageDiv);
3520
+ }
3521
+
3522
+ function saveTextHighlights(pageNum, pageDiv) {
3523
+ const container = pageDiv.querySelector('.textHighlightContainer');
3524
+ if (container) {
3525
+ const key = `textHighlight_${pageNum}`;
3526
+ localStorage.setItem(key, container.innerHTML);
3527
+ }
3528
+ }
3529
+
3530
+ function loadTextHighlights(pageNum, pageDiv) {
3531
+ const key = `textHighlight_${pageNum}`;
3532
+ const saved = localStorage.getItem(key);
3533
+ if (saved) {
3534
+ let container = pageDiv.querySelector('.textHighlightContainer');
3535
+ if (!container) {
3536
+ container = document.createElement('div');
3537
+ container.className = 'textHighlightContainer';
3538
+ container.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
3539
+ pageDiv.insertBefore(container, pageDiv.firstChild);
3540
+ }
3541
+ container.innerHTML = saved;
3542
+ }
3543
+ }
3544
+
3545
+ function showHighlightPopup(x, y, pageDiv, rects) {
3546
+ removeHighlightPopup();
3547
+
3548
+ highlightPopup = document.createElement('div');
3549
+ highlightPopup.className = 'highlightPopup';
3550
+ highlightPopup.style.left = x + 'px';
3551
+ highlightPopup.style.top = (y + 10) + 'px';
3552
+
3553
+ const colors = ['#fff100', '#16c60c', '#00b7c3', '#0078d4', '#886ce4', '#e81224'];
3554
+ colors.forEach(color => {
3555
+ const btn = document.createElement('button');
3556
+ btn.style.background = color;
3557
+ btn.title = 'Vurgula';
3558
+ btn.onclick = (e) => {
3559
+ e.stopPropagation();
3560
+ createTextHighlights(pageDiv, rects, color);
3561
+ window.getSelection().removeAllRanges();
3562
+ removeHighlightPopup();
3563
+ };
3564
+ highlightPopup.appendChild(btn);
3565
+ });
3566
+
3567
+ document.body.appendChild(highlightPopup);
3568
+ }
3569
+
3570
+ // Listen for text selection
3571
+ document.addEventListener('mouseup', (e) => {
3572
+ // Small delay to let selection finalize
3573
+ setTimeout(() => {
3574
+ const selData = getSelectionRects();
3575
+ if (selData && selData.relativeRects.length > 0) {
3576
+ const lastRect = selData.lastRect;
3577
+ showHighlightPopup(lastRect.right, lastRect.bottom, selData.pageDiv, selData.relativeRects);
3578
+ } else {
3579
+ removeHighlightPopup();
3580
+ }
3581
+ }, 10);
3582
+ });
3583
+
3584
+ // Remove popup on click elsewhere
3585
+ document.addEventListener('mousedown', (e) => {
3586
+ if (highlightPopup && !highlightPopup.contains(e.target)) {
3587
+ removeHighlightPopup();
3588
+ }
3589
+ });
3590
+
3591
+ // Load text highlights when pages render
3592
+ eventBus.on('pagerendered', (evt) => {
3593
+ const pageDiv = pdfViewer.getPageView(evt.pageNumber - 1)?.div;
3594
+ if (pageDiv) {
3595
+ loadTextHighlights(evt.pageNumber, pageDiv);
3596
+ }
3597
+ });
3598
+
3599
+ // ==========================================
3600
+ // SELECT/MOVE TOOL (Fixed + Touch Support)
3601
+ // ==========================================
3602
+ let selectedAnnotation = null;
3603
+ let selectedSvg = null;
3604
+ let selectedPageNum = null;
3605
+ let copiedAnnotation = null;
3606
+ let copiedPageNum = null;
3607
+ let isDraggingAnnotation = false;
3608
+ let annotationDragStartX = 0;
3609
+ let annotationDragStartY = 0;
3610
+
3611
+ // Marquee selection state
3612
+ let marqueeActive = false;
3613
+ let marqueeStartX = 0, marqueeStartY = 0;
3614
+ let marqueeRect = null;
3615
+ let marqueeSvg = null;
3616
+ let marqueePageNum = null;
3617
+ let multiSelectedAnnotations = [];
3618
+
3619
+ // Create selection toolbar for touch devices
3620
+ const selectionToolbar = document.createElement('div');
3621
+ selectionToolbar.className = 'selection-toolbar';
3622
+ selectionToolbar.innerHTML = `
3623
+ <button data-action="copy" title="Kopyala (Ctrl+C)">
3624
+ <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>
3625
+ <span>Kopyala</span>
3626
+ </button>
3627
+ <button data-action="duplicate" title="Çoğalt">
3628
+ <svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
3629
+ <span>Çoğalt</span>
3630
+ </button>
3631
+ <button data-action="delete" class="delete" title="Sil (Del)">
3632
+ <svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
3633
+ <span>Sil</span>
3634
+ </button>
3635
+ `;
3636
+ document.body.appendChild(selectionToolbar);
3637
+
3638
+ // Selection toolbar event handlers
3639
+ selectionToolbar.addEventListener('click', (e) => {
3640
+ const btn = e.target.closest('button');
3641
+ if (!btn) return;
3642
+
3643
+ const action = btn.dataset.action;
3644
+ if (action === 'copy') {
3645
+ copySelectedAnnotation();
3646
+ showToast('Kopyalandı!');
3647
+ } else if (action === 'duplicate') {
3648
+ copySelectedAnnotation();
3649
+ pasteAnnotation();
3650
+ showToast('Çoğaltıldı!');
3651
+ } else if (action === 'delete') {
3652
+ deleteSelectedAnnotation();
3653
+ showToast('Silindi!');
3654
+ }
3655
+ });
3656
+
3657
+ function showToast(message) {
3658
+ const existingToast = document.querySelector('.toast-notification');
3659
+ if (existingToast) existingToast.remove();
3660
+
3661
+ const toast = document.createElement('div');
3662
+ toast.className = 'toast-notification';
3663
+ toast.textContent = message;
3664
+ document.body.appendChild(toast);
3665
+ setTimeout(() => toast.remove(), 2000);
3666
+ }
3667
+
3668
+ function updateSelectionToolbar() {
3669
+ if (selectedAnnotation && currentTool === 'select') {
3670
+ selectionToolbar.classList.add('visible');
3671
+ } else {
3672
+ selectionToolbar.classList.remove('visible');
3673
+ }
3674
+ }
3675
+
3676
+ function clearMultiSelection() {
3677
+ if (multiDragHandler) {
3678
+ multiSelectedAnnotations.forEach(el => {
3679
+ el.removeEventListener('mousedown', multiDragHandler);
3680
+ el.removeEventListener('touchstart', multiDragHandler);
3681
+ });
3682
+ multiDragHandler = null;
3683
+ }
3684
+ multiSelectedAnnotations.forEach(el => {
3685
+ el.classList.remove('annotation-multi-selected');
3686
+ el.style.cursor = '';
3687
+ });
3688
+ multiSelectedAnnotations = [];
3689
+ }
3690
+
3691
+ function clearAnnotationSelection() {
3692
+ if (selectedAnnotation) {
3693
+ selectedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
3694
+ }
3695
+ selectedAnnotation = null;
3696
+ selectedSvg = null;
3697
+ selectedPageNum = null;
3698
+ isDraggingAnnotation = false;
3699
+ clearMultiSelection();
3700
+ updateSelectionToolbar();
3701
+ }
3702
+
3703
+ function selectAnnotation(element, svg, pageNum) {
3704
+ clearAnnotationSelection();
3705
+ selectedAnnotation = element;
3706
+ selectedSvg = svg;
3707
+ selectedPageNum = pageNum;
3708
+ element.classList.add('annotation-selected', 'just-selected');
3709
+
3710
+ // Remove pulse animation after it completes
3711
+ setTimeout(() => {
3712
+ element.classList.remove('just-selected');
3713
+ }, 600);
3714
+
3715
+ updateSelectionToolbar();
3716
+ }
3717
+
3718
+ function deleteSelectedAnnotation() {
3719
+ if (multiSelectedAnnotations.length > 0 && marqueeSvg) {
3720
+ // Delete all multi-selected annotations
3721
+ const pageNum = marqueePageNum;
3722
+ multiSelectedAnnotations.forEach(el => el.remove());
3723
+ clearMultiSelection();
3724
+ if (marqueeSvg && marqueeSvg.isConnected) saveAnnotations(pageNum);
3725
+ marqueeSvg = null;
3726
+ marqueePageNum = null;
3727
+ } else if (selectedAnnotation && selectedSvg) {
3728
+ selectedAnnotation.remove();
3729
+ saveAnnotations(selectedPageNum);
3730
+ clearAnnotationSelection();
3731
+ }
3732
+ }
3733
+
3734
+ function copySelectedAnnotation() {
3735
+ if (selectedAnnotation) {
3736
+ copiedAnnotation = selectedAnnotation.cloneNode(true);
3737
+ copiedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
3738
+ copiedPageNum = selectedPageNum;
3739
+ }
3740
+ }
3741
+
3742
+ function pasteAnnotation() {
3743
+ if (!copiedAnnotation || !pdfViewer) return;
3744
+
3745
+ // Paste to current page
3746
+ const currentPage = pdfViewer.currentPageNumber;
3747
+ const pageView = pdfViewer.getPageView(currentPage - 1);
3748
+ const svg = pageView?.div?.querySelector('.annotationLayer');
3749
+
3750
+ if (svg) {
3751
+ const cloned = copiedAnnotation.cloneNode(true);
3752
+ const offset = 30; // Offset amount for pasted elements
3753
+
3754
+ // Offset pasted element slightly
3755
+ if (cloned.tagName === 'path') {
3756
+ // For paths, add/update transform translate
3757
+ const currentTransform = cloned.getAttribute('transform') || '';
3758
+ const match = currentTransform.match(/translate\(([^,]+),([^)]+)\)/);
3759
+ let tx = offset, ty = offset;
3760
+ if (match) {
3761
+ tx = parseFloat(match[1]) + offset;
3762
+ ty = parseFloat(match[2]) + offset;
3763
+ }
3764
+ cloned.setAttribute('transform', `translate(${tx}, ${ty})`);
3765
+ } else if (cloned.tagName === 'rect') {
3766
+ cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
3767
+ cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
3768
+ } else if (cloned.tagName === 'ellipse') {
3769
+ cloned.setAttribute('cx', parseFloat(cloned.getAttribute('cx')) + offset);
3770
+ cloned.setAttribute('cy', parseFloat(cloned.getAttribute('cy')) + offset);
3771
+ } else if (cloned.tagName === 'line') {
3772
+ cloned.setAttribute('x1', parseFloat(cloned.getAttribute('x1')) + offset);
3773
+ cloned.setAttribute('y1', parseFloat(cloned.getAttribute('y1')) + offset);
3774
+ cloned.setAttribute('x2', parseFloat(cloned.getAttribute('x2')) + offset);
3775
+ cloned.setAttribute('y2', parseFloat(cloned.getAttribute('y2')) + offset);
3776
+ } else if (cloned.tagName === 'text') {
3777
+ cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
3778
+ cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
3779
+ }
3780
+
3781
+ svg.appendChild(cloned);
3782
+ saveAnnotations(currentPage);
3783
+ selectAnnotation(cloned, svg, currentPage);
3784
+ }
3785
+ }
3786
+
3787
+ // Get coordinates from mouse or touch event
3788
+ function getEventCoords(e) {
3789
+ if (e.touches && e.touches.length > 0) {
3790
+ return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
3791
+ }
3792
+ if (e.changedTouches && e.changedTouches.length > 0) {
3793
+ return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY };
3794
+ }
3795
+ return { clientX: e.clientX, clientY: e.clientY };
3796
+ }
3797
+
3798
+ // Convert screen coordinates to viewBox coordinates, accounting for CSS rotation
3799
+ function screenToViewBox(svg, clientX, clientY) {
3800
+ const rect = svg.getBoundingClientRect();
3801
+ const vbW = parseFloat(svg.dataset.viewboxWidth);
3802
+ const vbH = parseFloat(svg.dataset.viewboxHeight);
3803
+
3804
+ // Offset from center in screen pixels
3805
+ const cx = rect.left + rect.width / 2;
3806
+ const cy = rect.top + rect.height / 2;
3807
+ const udx = clientX - cx;
3808
+ const udy = clientY - cy;
3809
+
3810
+ // Element dimensions (no CSS rotation — PDF.js handles rotation natively)
3811
+ let elemW, elemH;
3812
+ {
3813
+ elemW = rect.width;
3814
+ elemH = rect.height;
3815
+ }
3816
+
3817
+ // Map to viewBox: center-relative to 0,0-relative
3818
+ const x = (udx + elemW / 2) * (vbW / elemW);
3819
+ const y = (udy + elemH / 2) * (vbH / elemH);
3820
+
3821
+ const scaleX = vbW / elemW;
3822
+ const scaleY = vbH / elemH;
3823
+
3824
+ return { x, y, scaleX, scaleY };
3825
+ }
3826
+
3827
+ // Convert screen delta (dx,dy pixels) to viewBox delta
3828
+ function screenDeltaToViewBox(svg, dxScreen, dyScreen) {
3829
+ const rect = svg.getBoundingClientRect();
3830
+ const vbW = parseFloat(svg.dataset.viewboxWidth);
3831
+ const vbH = parseFloat(svg.dataset.viewboxHeight);
3832
+
3833
+ return {
3834
+ dx: dxScreen * (vbW / rect.width),
3835
+ dy: dyScreen * (vbH / rect.height)
3836
+ };
3837
+ }
3838
+
3839
+ // Handle select tool events (both mouse and touch)
3840
+ function handleSelectPointerDown(e, svg, pageNum) {
3841
+ if (currentTool !== 'select') return false;
3842
+
3843
+ const coords = getEventCoords(e);
3844
+ const target = e.target;
3845
+
3846
+ if (target === svg || target.tagName === 'svg') {
3847
+ // Clicked on empty area — clear selections and start marquee
3848
+ clearAnnotationSelection();
3849
+
3850
+ const pt = screenToViewBox(svg, coords.clientX, coords.clientY);
3851
+
3852
+ marqueeActive = true;
3853
+ marqueeStartX = pt.x;
3854
+ marqueeStartY = pt.y;
3855
+ marqueeSvg = svg;
3856
+ marqueePageNum = pageNum;
3857
+
3858
+ // Create marquee rectangle
3859
+ marqueeRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
3860
+ marqueeRect.setAttribute('class', 'marquee-rect');
3861
+ marqueeRect.setAttribute('x', pt.x);
3862
+ marqueeRect.setAttribute('y', pt.y);
3863
+ marqueeRect.setAttribute('width', 0);
3864
+ marqueeRect.setAttribute('height', 0);
3865
+ svg.appendChild(marqueeRect);
3866
+
3867
+ function onMarqueeMove(ev) {
3868
+ if (!marqueeActive || !marqueeRect) return;
3869
+ ev.preventDefault();
3870
+
3871
+ const moveCoords = getEventCoords(ev);
3872
+ const mpt = screenToViewBox(marqueeSvg, moveCoords.clientX, moveCoords.clientY);
3873
+
3874
+ const x = Math.min(marqueeStartX, mpt.x);
3875
+ const y = Math.min(marqueeStartY, mpt.y);
3876
+ const w = Math.abs(mpt.x - marqueeStartX);
3877
+ const h = Math.abs(mpt.y - marqueeStartY);
3878
+
3879
+ marqueeRect.setAttribute('x', x);
3880
+ marqueeRect.setAttribute('y', y);
3881
+ marqueeRect.setAttribute('width', w);
3882
+ marqueeRect.setAttribute('height', h);
3883
+ }
3884
+
3885
+ function onMarqueeEnd(ev) {
3886
+ document.removeEventListener('mousemove', onMarqueeMove);
3887
+ document.removeEventListener('mouseup', onMarqueeEnd);
3888
+ document.removeEventListener('touchmove', onMarqueeMove);
3889
+ document.removeEventListener('touchend', onMarqueeEnd);
3890
+ document.removeEventListener('touchcancel', onMarqueeEnd);
3891
+
3892
+ if (!marqueeRect || !marqueeSvg) { marqueeActive = false; return; }
3893
+
3894
+ // Marquee bounds
3895
+ const mx = parseFloat(marqueeRect.getAttribute('x'));
3896
+ const my = parseFloat(marqueeRect.getAttribute('y'));
3897
+ const mw = parseFloat(marqueeRect.getAttribute('width'));
3898
+ const mh = parseFloat(marqueeRect.getAttribute('height'));
3899
+
3900
+ // Remove marquee rectangle
3901
+ marqueeRect.remove();
3902
+ marqueeRect = null;
3903
+ marqueeActive = false;
3904
+
3905
+ // Ignore tiny marquees (accidental clicks)
3906
+ if (mw < 5 && mh < 5) return;
3907
+
3908
+ // Find elements intersecting the marquee
3909
+ const elements = marqueeSvg.querySelectorAll('path, rect, ellipse, line, text');
3910
+ multiSelectedAnnotations = [];
3911
+
3912
+ elements.forEach(el => {
3913
+ // Skip the marquee rect class itself (already removed, but safety)
3914
+ if (el.classList.contains('marquee-rect')) return;
3915
+
3916
+ const bbox = el.getBBox();
3917
+ let ex = bbox.x, ey = bbox.y;
3918
+ const transform = el.getAttribute('transform');
3919
+ if (transform) {
3920
+ const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
3921
+ if (match) { ex += parseFloat(match[1]); ey += parseFloat(match[2]); }
3922
+ }
3923
+
3924
+ // AABB intersection test
3925
+ if (ex + bbox.width > mx && ex < mx + mw &&
3926
+ ey + bbox.height > my && ey < my + mh) {
3927
+ el.classList.add('annotation-multi-selected');
3928
+ multiSelectedAnnotations.push(el);
3929
+ }
3930
+ });
3931
+
3932
+ // Enable multi-drag if we selected anything
3933
+ if (multiSelectedAnnotations.length > 0) {
3934
+ setupMultiDrag(marqueeSvg, marqueePageNum);
3935
+ }
3936
+ }
3937
+
3938
+ document.addEventListener('mousemove', onMarqueeMove, { passive: false });
3939
+ document.addEventListener('mouseup', onMarqueeEnd);
3940
+ document.addEventListener('touchmove', onMarqueeMove, { passive: false });
3941
+ document.addEventListener('touchend', onMarqueeEnd);
3942
+ document.addEventListener('touchcancel', onMarqueeEnd);
3943
+
3944
+ return true;
3945
+ }
3946
+
3947
+ // Check if clicked on an annotation element
3948
+ if (target.closest('.annotationLayer') && target !== svg) {
3949
+ e.preventDefault();
3950
+ e.stopPropagation();
3951
+
3952
+ selectAnnotation(target, svg, pageNum);
3953
+
3954
+ // Start drag
3955
+ isDraggingAnnotation = true;
3956
+ annotationDragStartX = coords.clientX;
3957
+ annotationDragStartY = coords.clientY;
3958
+
3959
+ target.classList.add('annotation-dragging');
3960
+
3961
+ function onMove(ev) {
3962
+ if (!isDraggingAnnotation) return;
3963
+ ev.preventDefault();
3964
+
3965
+ const moveCoords = getEventCoords(ev);
3966
+ const dxScreen = moveCoords.clientX - annotationDragStartX;
3967
+ const dyScreen = moveCoords.clientY - annotationDragStartY;
3968
+
3969
+ // Convert screen delta to viewBox delta (rotation-aware)
3970
+ const vbDelta = screenDeltaToViewBox(svg, dxScreen, dyScreen);
3971
+
3972
+ // Move the element
3973
+ moveAnnotation(target, vbDelta.dx, vbDelta.dy);
3974
+
3975
+ // Update start position for next move
3976
+ annotationDragStartX = moveCoords.clientX;
3977
+ annotationDragStartY = moveCoords.clientY;
3978
+ }
3979
+
3980
+ function onEnd(ev) {
3981
+ document.removeEventListener('mousemove', onMove);
3982
+ document.removeEventListener('mouseup', onEnd);
3983
+ document.removeEventListener('touchmove', onMove);
3984
+ document.removeEventListener('touchend', onEnd);
3985
+ document.removeEventListener('touchcancel', onEnd);
3986
+
3987
+ target.classList.remove('annotation-dragging');
3988
+ isDraggingAnnotation = false;
3989
+
3990
+ // Bug fix: Clamp annotation within page bounds to prevent cross-page loss
3991
+ const vbW = parseFloat(svg.dataset.viewboxWidth);
3992
+ const vbH = parseFloat(svg.dataset.viewboxHeight);
3993
+ clampAnnotationToPage(target, vbW, vbH);
3994
+
3995
+ // Bug fix: Check if SVG is still in DOM before saving
3996
+ if (svg.isConnected) {
3997
+ saveAnnotations(pageNum);
3998
+ }
3999
+ }
4000
+
4001
+ document.addEventListener('mousemove', onMove, { passive: false });
4002
+ document.addEventListener('mouseup', onEnd);
4003
+ document.addEventListener('touchmove', onMove, { passive: false });
4004
+ document.addEventListener('touchend', onEnd);
4005
+ document.addEventListener('touchcancel', onEnd);
4006
+
4007
+ return true;
4008
+ }
4009
+
4010
+ return false;
4011
+ }
4012
+
4013
+ // Multi-drag handler reference for cleanup
4014
+ let multiDragHandler = null;
4015
+
4016
+ // Setup multi-drag for marquee-selected annotations
4017
+ function setupMultiDrag(svg, pageNum) {
4018
+ function startMultiDragHandler(e) {
4019
+ if (currentTool !== 'select') return;
4020
+ e.preventDefault();
4021
+ e.stopPropagation();
4022
+
4023
+ const startCoords = getEventCoords(e);
4024
+ let lastX = startCoords.clientX;
4025
+ let lastY = startCoords.clientY;
4026
+
4027
+ multiSelectedAnnotations.forEach(el => el.classList.add('annotation-dragging'));
4028
+
4029
+ function onMove(ev) {
4030
+ ev.preventDefault();
4031
+ const moveCoords = getEventCoords(ev);
4032
+ const dx = moveCoords.clientX - lastX;
4033
+ const dy = moveCoords.clientY - lastY;
4034
+ const vbDelta = screenDeltaToViewBox(svg, dx, dy);
4035
+
4036
+ multiSelectedAnnotations.forEach(el => moveAnnotation(el, vbDelta.dx, vbDelta.dy));
4037
+
4038
+ lastX = moveCoords.clientX;
4039
+ lastY = moveCoords.clientY;
4040
+ }
4041
+
4042
+ function onEnd() {
4043
+ document.removeEventListener('mousemove', onMove);
4044
+ document.removeEventListener('mouseup', onEnd);
4045
+ document.removeEventListener('touchmove', onMove);
4046
+ document.removeEventListener('touchend', onEnd);
4047
+ document.removeEventListener('touchcancel', onEnd);
4048
+
4049
+ multiSelectedAnnotations.forEach(el => el.classList.remove('annotation-dragging'));
4050
+
4051
+ // Clamp all selected annotations within page bounds
4052
+ const vbW = parseFloat(svg.dataset.viewboxWidth);
4053
+ const vbH = parseFloat(svg.dataset.viewboxHeight);
4054
+ multiSelectedAnnotations.forEach(el => clampAnnotationToPage(el, vbW, vbH));
4055
+
4056
+ if (svg.isConnected) saveAnnotations(pageNum);
4057
+ }
4058
+
4059
+ document.addEventListener('mousemove', onMove, { passive: false });
4060
+ document.addEventListener('mouseup', onEnd);
4061
+ document.addEventListener('touchmove', onMove, { passive: false });
4062
+ document.addEventListener('touchend', onEnd);
4063
+ document.addEventListener('touchcancel', onEnd);
4064
+ }
4065
+
4066
+ multiDragHandler = startMultiDragHandler;
4067
+ multiSelectedAnnotations.forEach(el => {
4068
+ el.style.cursor = 'grab';
4069
+ el.addEventListener('mousedown', startMultiDragHandler);
4070
+ el.addEventListener('touchstart', startMultiDragHandler, { passive: false });
4071
+ });
4072
+ }
4073
+
4074
+ // moveAnnotation - applies delta movement to an annotation element
4075
+ function moveAnnotation(element, dx, dy) {
4076
+ if (element.tagName === 'path') {
4077
+ // Transform path using translate
4078
+ const currentTransform = element.getAttribute('transform') || '';
4079
+ const match = currentTransform.match(/translate\(([^,]+),\s*([^)]+)\)/);
4080
+ let tx = 0, ty = 0;
4081
+ if (match) {
4082
+ tx = parseFloat(match[1]);
4083
+ ty = parseFloat(match[2]);
4084
+ }
4085
+ element.setAttribute('transform', `translate(${tx + dx}, ${ty + dy})`);
4086
+ } else if (element.tagName === 'rect') {
4087
+ element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
4088
+ element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
4089
+ } else if (element.tagName === 'ellipse') {
4090
+ element.setAttribute('cx', parseFloat(element.getAttribute('cx')) + dx);
4091
+ element.setAttribute('cy', parseFloat(element.getAttribute('cy')) + dy);
4092
+ } else if (element.tagName === 'line') {
4093
+ element.setAttribute('x1', parseFloat(element.getAttribute('x1')) + dx);
4094
+ element.setAttribute('y1', parseFloat(element.getAttribute('y1')) + dy);
4095
+ element.setAttribute('x2', parseFloat(element.getAttribute('x2')) + dx);
4096
+ element.setAttribute('y2', parseFloat(element.getAttribute('y2')) + dy);
4097
+ } else if (element.tagName === 'text') {
4098
+ element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
4099
+ element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
4100
+ }
4101
+ }
4102
+
4103
+ // Clamp annotation element within page viewBox bounds
4104
+ function clampAnnotationToPage(element, maxW, maxH) {
4105
+ const margin = 10;
4106
+ function clamp(val, min, max) { return Math.max(min, Math.min(val, max)); }
4107
+
4108
+ if (element.tagName === 'path') {
4109
+ const transform = element.getAttribute('transform') || '';
4110
+ const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
4111
+ if (match) {
4112
+ const tx = clamp(parseFloat(match[1]), -maxW + margin, maxW - margin);
4113
+ const ty = clamp(parseFloat(match[2]), -maxH + margin, maxH - margin);
4114
+ element.setAttribute('transform', `translate(${tx}, ${ty})`);
4115
+ }
4116
+ } else if (element.tagName === 'rect') {
4117
+ element.setAttribute('x', clamp(parseFloat(element.getAttribute('x')), 0, maxW - margin));
4118
+ element.setAttribute('y', clamp(parseFloat(element.getAttribute('y')), 0, maxH - margin));
4119
+ } else if (element.tagName === 'ellipse') {
4120
+ element.setAttribute('cx', clamp(parseFloat(element.getAttribute('cx')), margin, maxW - margin));
4121
+ element.setAttribute('cy', clamp(parseFloat(element.getAttribute('cy')), margin, maxH - margin));
4122
+ } else if (element.tagName === 'line') {
4123
+ element.setAttribute('x1', clamp(parseFloat(element.getAttribute('x1')), 0, maxW));
4124
+ element.setAttribute('y1', clamp(parseFloat(element.getAttribute('y1')), 0, maxH));
4125
+ element.setAttribute('x2', clamp(parseFloat(element.getAttribute('x2')), 0, maxW));
4126
+ element.setAttribute('y2', clamp(parseFloat(element.getAttribute('y2')), 0, maxH));
4127
+ } else if (element.tagName === 'text') {
4128
+ element.setAttribute('x', clamp(parseFloat(element.getAttribute('x')), 0, maxW - margin));
4129
+ element.setAttribute('y', clamp(parseFloat(element.getAttribute('y')), margin, maxH - margin));
4130
+ }
4131
+ }
4132
+
4133
+ // Legacy function for backwards compatibility (used elsewhere)
4134
+ function handleSelectMouseDown(e, svg, pageNum) {
4135
+ return handleSelectPointerDown(e, svg, pageNum);
4136
+ }
4137
+
4138
+ // ==========================================
4139
+ // KEYBOARD SHORTCUTS
4140
+ // ==========================================
4141
+ document.addEventListener('keydown', (e) => {
4142
+ // Ignore if typing in input
4143
+ if (e.target.tagName === 'INPUT' || e.target.contentEditable === 'true') return;
4144
+
4145
+ const key = e.key.toLowerCase();
4146
+
4147
+ // Tool shortcuts
4148
+ if (key === 'h') { setTool('highlight'); e.preventDefault(); }
4149
+ if (key === 'p') { setTool('pen'); e.preventDefault(); }
4150
+ if (key === 'e') { setTool('eraser'); e.preventDefault(); }
4151
+ if (key === 't') { setTool('text'); e.preventDefault(); }
4152
+ if (key === 'r') { setTool('shape'); e.preventDefault(); }
4153
+ if (key === 'v') { setTool('select'); e.preventDefault(); }
4154
+ if (key === 'f') { toggleFullscreen(); e.preventDefault(); }
4155
+
4156
+ // Delete selected annotation(s)
4157
+ if ((key === 'delete' || key === 'backspace') && (selectedAnnotation || multiSelectedAnnotations.length > 0)) {
4158
+ deleteSelectedAnnotation();
4159
+ e.preventDefault();
4160
+ }
4161
+
4162
+ // Undo/Redo
4163
+ if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {
4164
+ performUndo();
4165
+ e.preventDefault();
4166
+ return;
4167
+ }
4168
+ if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {
4169
+ performRedo();
4170
+ e.preventDefault();
4171
+ return;
4172
+ }
4173
+
4174
+ // Copy/Paste annotations
4175
+ if ((e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
4176
+ copySelectedAnnotation();
4177
+ e.preventDefault();
4178
+ }
4179
+ if ((e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
4180
+ pasteAnnotation();
4181
+ e.preventDefault();
4182
+ }
4183
+
4184
+ // Navigation
4185
+ if (key === 's') {
4186
+ document.getElementById('sidebarBtn').click();
4187
+ e.preventDefault();
4188
+ }
4189
+
4190
+ // Arrow key navigation
4191
+ if (key === 'arrowleft' || key === 'arrowup') {
4192
+ if (pdfViewer && pdfViewer.currentPageNumber > 1) {
4193
+ pdfViewer.currentPageNumber--;
4194
+ }
4195
+ e.preventDefault();
4196
+ }
4197
+ if (key === 'arrowright' || key === 'arrowdown') {
4198
+ if (pdfViewer && pdfViewer.currentPageNumber < pdfViewer.pagesCount) {
4199
+ pdfViewer.currentPageNumber++;
4200
+ }
4201
+ e.preventDefault();
4202
+ }
4203
+
4204
+ // Home/End
4205
+ if (key === 'home') {
4206
+ if (pdfViewer) pdfViewer.currentPageNumber = 1;
4207
+ e.preventDefault();
4208
+ }
4209
+ if (key === 'end') {
4210
+ if (pdfViewer) pdfViewer.currentPageNumber = pdfViewer.pagesCount;
4211
+ e.preventDefault();
4212
+ }
4213
+
4214
+ // Zoom shortcuts - prevent browser zoom
4215
+ if ((e.ctrlKey || e.metaKey) && (key === '=' || key === '+' || e.code === 'Equal')) {
4216
+ e.preventDefault();
4217
+ e.stopPropagation();
4218
+ pdfViewer.currentScale += 0.25;
4219
+ return;
4220
+ }
4221
+ if ((e.ctrlKey || e.metaKey) && (key === '-' || e.code === 'Minus')) {
4222
+ e.preventDefault();
4223
+ e.stopPropagation();
4224
+ pdfViewer.currentScale -= 0.25;
4225
+ return;
4226
+ }
4227
+ if ((e.ctrlKey || e.metaKey) && (key === '0' || e.code === 'Digit0')) {
4228
+ e.preventDefault();
4229
+ e.stopPropagation();
4230
+ pdfViewer.currentScaleValue = 'page-width';
4231
+ return;
4232
+ }
4233
+
4234
+ // Escape to deselect tool
4235
+ if (key === 'escape') {
4236
+ if (currentTool) {
4237
+ setTool(currentTool); // Toggle off
4238
+ }
4239
+ closeAllDropdowns();
4240
+ }
4241
+
4242
+ // Sepia mode
4243
+ if (key === 'm') {
4244
+ document.getElementById('sepiaBtn').click();
4245
+ e.preventDefault();
4246
+ }
4247
+ });
4248
+
4249
+ // ==========================================
4250
+ // CONTEXT MENU (Right-click)
4251
+ // ==========================================
4252
+ const contextMenu = document.createElement('div');
4253
+ contextMenu.className = 'contextMenu';
4254
+ contextMenu.innerHTML = `
4255
+ <div class="contextMenuItem" data-action="highlight">
4256
+ <svg viewBox="0 0 24 24"><path d="M3 21h18v-2H3v2zM5 16h14l-3-10H8l-3 10z"/></svg>
4257
+ Vurgula
4258
+ <span class="shortcutHint">H</span>
4259
+ </div>
4260
+ <div class="contextMenuItem" data-action="pen">
4261
+ <svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
4262
+ Kalem
4263
+ <span class="shortcutHint">P</span>
4264
+ </div>
4265
+ <div class="contextMenuItem" data-action="text">
4266
+ <svg viewBox="0 0 24 24"><path d="M5 4v3h5.5v12h3V7H19V4H5z"/></svg>
4267
+ Metin Ekle
4268
+ <span class="shortcutHint">T</span>
4269
+ </div>
4270
+ <div class="contextMenuDivider"></div>
4271
+ <div class="contextMenuItem" data-action="zoomIn">
4272
+ <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
4273
+ Yakınlaştır
4274
+ <span class="shortcutHint">Ctrl++</span>
4275
+ </div>
4276
+ <div class="contextMenuItem" data-action="zoomOut">
4277
+ <svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>
4278
+ Uzaklaştır
4279
+ <span class="shortcutHint">Ctrl+-</span>
4280
+ </div>
4281
+ <div class="contextMenuDivider"></div>
4282
+ <div class="contextMenuItem" data-action="sepia">
4283
+ <svg viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/></svg>
4284
+ Okuma Modu
4285
+ <span class="shortcutHint">M</span>
4286
+ </div>
4287
+ `;
4288
+ document.body.appendChild(contextMenu);
4289
+
4290
+ // Show context menu on right-click in viewer
4291
+ function showCustomContextMenu(e) {
4292
+ e.preventDefault();
4293
+ contextMenu.style.left = e.clientX + 'px';
4294
+ contextMenu.style.top = e.clientY + 'px';
4295
+ contextMenu.classList.add('visible');
4296
+ }
4297
+ container.addEventListener('contextmenu', showCustomContextMenu);
4298
+
4299
+ // Hide context menu on click
4300
+ document.addEventListener('click', () => {
4301
+ contextMenu.classList.remove('visible');
4302
+ });
4303
+
4304
+ // Context menu actions
4305
+ contextMenu.addEventListener('click', (e) => {
4306
+ const item = e.target.closest('.contextMenuItem');
4307
+ if (!item) return;
4308
+
4309
+ const action = item.dataset.action;
4310
+ switch (action) {
4311
+ case 'highlight': setTool('highlight'); break;
4312
+ case 'pen': setTool('pen'); break;
4313
+ case 'text': setTool('text'); break;
4314
+ case 'zoomIn': pdfViewer.currentScale += 0.25; break;
4315
+ case 'zoomOut': pdfViewer.currentScale -= 0.25; break;
4316
+ case 'sepia': document.getElementById('sepiaBtn').click(); break;
4317
+ }
4318
+ contextMenu.classList.remove('visible');
4319
+ });
4320
+
4321
+ // ==========================================
4322
+ // ERGONOMIC FEATURES
4323
+ // ==========================================
4324
+
4325
+ // Fullscreen toggle function
4326
+ function toggleFullscreen() {
4327
+ if (document.fullscreenElement) {
4328
+ document.exitFullscreen();
4329
+ } else {
4330
+ document.documentElement.requestFullscreen().catch(() => { });
4331
+ }
4332
+ }
4333
+
4334
+ // Update fullscreen button icon
4335
+ function updateFullscreenIcon() {
4336
+ const icon = document.getElementById('fullscreenIcon');
4337
+ const btn = document.getElementById('fullscreenBtn');
4338
+ if (document.fullscreenElement) {
4339
+ icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4340
+ btn.classList.add('active');
4341
+ } else {
4342
+ icon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
4343
+ btn.classList.remove('active');
4344
+ }
4345
+ }
4346
+
4347
+ document.addEventListener('fullscreenchange', updateFullscreenIcon);
4348
+
4349
+ // Fullscreen button click
4350
+ document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
4351
+
4352
+ // Double-click on page for fullscreen
4353
+ let lastClickTime = 0;
4354
+ container.addEventListener('click', (e) => {
4355
+ const now = Date.now();
4356
+ if (now - lastClickTime < 300) {
4357
+ toggleFullscreen();
4358
+ }
4359
+ lastClickTime = now;
4360
+ });
4361
+
4362
+ // Auto-fullscreen when viewer loads inside iframe
4363
+ if (window.self !== window.top && window.PDF_SECURE_CONFIG) {
4364
+ // We're inside an iframe - request fullscreen on first user interaction
4365
+ const autoFullscreen = () => {
4366
+ document.documentElement.requestFullscreen().catch(() => { });
4367
+ container.removeEventListener('click', autoFullscreen);
4368
+ container.removeEventListener('touchstart', autoFullscreen);
4369
+ };
4370
+ container.addEventListener('click', autoFullscreen, { once: true });
4371
+ container.addEventListener('touchstart', autoFullscreen, { once: true });
4372
+ }
4373
+
4374
+ // Mouse wheel zoom with Ctrl
4375
+ container.addEventListener('wheel', (e) => {
4376
+ if (e.ctrlKey) {
4377
+ e.preventDefault();
4378
+ if (e.deltaY < 0) {
4379
+ pdfViewer.currentScale += 0.1;
4380
+ } else {
4381
+ pdfViewer.currentScale -= 0.1;
4382
+ }
4383
+ }
4384
+ }, { passive: false });
4385
+
4386
+ console.log('PDF Viewer Ready');
4387
+ console.log('Keyboard Shortcuts: H=Highlight, P=Pen, E=Eraser, T=Text, R=Shapes, S=Sidebar, M=ReadingMode, Arrows=Navigate');
4388
+
4389
+ // ==========================================
4390
+ // MOBILE / TABLET SUPPORT
4391
+ // ==========================================
4392
+ const isMobile = () => window.innerWidth <= 599;
4393
+ const isTabletPortrait = () => {
4394
+ const w = window.innerWidth;
4395
+ return w >= 600 && w <= 1024 && window.innerHeight > window.innerWidth;
4396
+ };
4397
+ const isTouch = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;
4398
+
4399
+ // Bottom toolbar element references
4400
+ const bottomToolbarInner = document.getElementById('bottomToolbarInner');
4401
+
4402
+ // Elements to move between top toolbar and bottom toolbar on mobile
4403
+ // We identify the annotation tools group (highlighter, pen, eraser, select, separator, undo, redo, clearAll, separator, text, shapes)
4404
+ const annotationToolsSelector = '#toolbar > .toolbarGroup:nth-child(3)';
4405
+ let toolsMovedToBottom = false;
4406
+ let annotationToolsPlaceholder = null;
4407
+
4408
+ function setupResponsiveToolbar() {
4409
+ const needsBottomBar = isMobile() || isTabletPortrait();
4410
+
4411
+ if (needsBottomBar && !toolsMovedToBottom) {
4412
+ // Move annotation tools to bottom toolbar
4413
+ const annotationGroup = document.querySelector(annotationToolsSelector);
4414
+ if (annotationGroup && bottomToolbarInner) {
4415
+ // Create placeholder to remember position
4416
+ annotationToolsPlaceholder = document.createComment('annotation-tools-placeholder');
4417
+ annotationGroup.parentNode.insertBefore(annotationToolsPlaceholder, annotationGroup);
4418
+
4419
+ // Move children into bottom toolbar
4420
+ while (annotationGroup.firstChild) {
4421
+ bottomToolbarInner.appendChild(annotationGroup.firstChild);
4422
+ }
4423
+ // Hide empty group
4424
+ annotationGroup.style.display = 'none';
4425
+ toolsMovedToBottom = true;
4426
+ }
4427
+ } else if (!needsBottomBar && toolsMovedToBottom) {
4428
+ // Move tools back to top toolbar
4429
+ const annotationGroup = document.querySelector(annotationToolsSelector);
4430
+ if (annotationGroup && bottomToolbarInner && annotationToolsPlaceholder) {
4431
+ while (bottomToolbarInner.firstChild) {
4432
+ annotationGroup.appendChild(bottomToolbarInner.firstChild);
4433
+ }
4434
+ annotationGroup.style.display = '';
4435
+ toolsMovedToBottom = false;
4436
+ }
4437
+ }
4438
+ }
4439
+
4440
+ // Run on load
4441
+ setupResponsiveToolbar();
4442
+
4443
+ // Use matchMedia for responsive switching
4444
+ const mobileMediaQuery = window.matchMedia('(max-width: 599px)');
4445
+ mobileMediaQuery.addEventListener('change', () => {
4446
+ setupResponsiveToolbar();
4447
+ });
4448
+
4449
+ const tabletPortraitQuery = window.matchMedia(
4450
+ '(min-width: 600px) and (max-width: 1024px) and (orientation: portrait)'
4451
+ );
4452
+ tabletPortraitQuery.addEventListener('change', () => {
4453
+ setupResponsiveToolbar();
4454
+ });
4455
+
4456
+ // Also handle resize for orientation changes
4457
+ window.addEventListener('resize', () => {
4458
+ setupResponsiveToolbar();
4459
+ });
4460
+
4461
+ // ==========================================
4462
+ // PINCH-TO-ZOOM (Touch devices)
4463
+ // ==========================================
4464
+ let pinchStartDistance = 0;
4465
+ let pinchStartScale = 1;
4466
+ let isPinching = false;
4467
+
4468
+ function getTouchDistance(touches) {
4469
+ const dx = touches[0].clientX - touches[1].clientX;
4470
+ const dy = touches[0].clientY - touches[1].clientY;
4471
+ return Math.sqrt(dx * dx + dy * dy);
4472
+ }
4473
+
4474
+ container.addEventListener('touchstart', (e) => {
4475
+ if (e.touches.length === 2 && !currentTool) {
4476
+ isPinching = true;
4477
+ pinchStartDistance = getTouchDistance(e.touches);
4478
+ pinchStartScale = pdfViewer.currentScale;
4479
+ e.preventDefault();
4480
+ }
4481
+ }, { passive: false });
4482
+
4483
+ container.addEventListener('touchmove', (e) => {
4484
+ if (isPinching && e.touches.length === 2) {
4485
+ const dist = getTouchDistance(e.touches);
4486
+ const ratio = dist / pinchStartDistance;
4487
+ const newScale = Math.min(Math.max(pinchStartScale * ratio, 0.5), 5.0);
4488
+ pdfViewer.currentScale = newScale;
4489
+ e.preventDefault();
4490
+ }
4491
+ }, { passive: false });
4492
+
4493
+ container.addEventListener('touchend', (e) => {
4494
+ if (e.touches.length < 2) {
4495
+ isPinching = false;
4496
+ }
4497
+ });
4498
+
4499
+ // ==========================================
4500
+ // CONTEXT MENU TOUCH HANDLING
4501
+ // ==========================================
4502
+ // On pure touch devices (no fine pointer), don't show custom context menu
4503
+ if (isTouch() && !window.matchMedia('(pointer: fine)').matches) {
4504
+ container.removeEventListener('contextmenu', showCustomContextMenu);
4505
+ }
4506
+
4507
+ // ==========================================
4508
+ // SECURITY FEATURES
4509
+ // ==========================================
4510
+
4511
+ (function initSecurityFeatures() {
4512
+ console.log('[Security] Initializing protection features...');
4513
+
4514
+ // 1. Block dangerous keyboard shortcuts
4515
+ document.addEventListener('keydown', function (e) {
4516
+ // Ctrl+S (Save)
4517
+ if (e.ctrlKey && e.key === 's') {
4518
+ e.preventDefault();
4519
+ console.log('[Security] Ctrl+S blocked');
4520
+ return false;
4521
+ }
4522
+ // Ctrl+P (Print)
4523
+ if (e.ctrlKey && e.key === 'p') {
4524
+ e.preventDefault();
4525
+ console.log('[Security] Ctrl+P blocked');
4526
+ return false;
4527
+ }
4528
+ // Ctrl+Shift+S (Save As)
4529
+ if (e.ctrlKey && e.shiftKey && e.key === 'S') {
4530
+ e.preventDefault();
4531
+ return false;
4532
+ }
4533
+ // F12 (DevTools)
4534
+ if (e.key === 'F12') {
4535
+ e.preventDefault();
4536
+ console.log('[Security] F12 blocked');
4537
+ return false;
4538
+ }
4539
+ // Ctrl+Shift+I (DevTools)
4540
+ if (e.ctrlKey && e.shiftKey && e.key === 'I') {
4541
+ e.preventDefault();
4542
+ return false;
4543
+ }
4544
+ // Ctrl+Shift+J (Console)
4545
+ if (e.ctrlKey && e.shiftKey && e.key === 'J') {
4546
+ e.preventDefault();
4547
+ return false;
4548
+ }
4549
+ // Ctrl+U (View Source)
4550
+ if (e.ctrlKey && e.key === 'u') {
4551
+ e.preventDefault();
4552
+ return false;
4553
+ }
4554
+ // Ctrl+Shift+C (Inspect Element)
4555
+ if (e.ctrlKey && e.shiftKey && e.key === 'C') {
4556
+ e.preventDefault();
4557
+ return false;
4558
+ }
4559
+ }, true);
4560
+
4561
+ // 2. Block context menu (right-click) - EVERYWHERE
4562
+ document.addEventListener('contextmenu', function (e) {
4563
+ e.preventDefault();
4564
+ e.stopPropagation();
4565
+ return false;
4566
+ }, true);
4567
+
4568
+ // 3. Block copy/cut/paste
4569
+ document.addEventListener('copy', function (e) {
4570
+ e.preventDefault();
4571
+ console.log('[Security] Copy blocked');
4572
+ return false;
4573
+ }, true);
4574
+
4575
+ document.addEventListener('cut', function (e) {
4576
+ e.preventDefault();
4577
+ return false;
4578
+ }, true);
4579
+
4580
+ // 4. Block drag events (prevent dragging content out)
4581
+ document.addEventListener('dragstart', function (e) {
4582
+ e.preventDefault();
4583
+ return false;
4584
+ }, true);
4585
+
4586
+ // 5. Block Print via window.print override
4587
+ window.print = function () {
4588
+ console.log('[Security] Print function blocked');
4589
+ alert('Yazdırma bu belgede engellenmiştir.');
4590
+ return false;
4591
+ };
4592
+
4593
+ // 6. Disable beforeprint event
4594
+ window.addEventListener('beforeprint', function (e) {
4595
+ e.preventDefault();
4596
+ document.body.style.display = 'none';
4597
+ });
4598
+
4599
+ window.addEventListener('afterprint', function () {
4600
+ document.body.style.display = '';
4601
+ });
4602
+
4603
+ // 7. Block screenshot keyboard shortcuts
4604
+ document.addEventListener('keyup', function (e) {
4605
+ // PrintScreen key
4606
+ if (e.key === 'PrintScreen') {
4607
+ navigator.clipboard.writeText('');
4608
+ console.log('[Security] PrintScreen clipboard cleared');
4609
+ }
4610
+ }, true);
4611
+
4612
+ // 8. Visibility change detection (tab switching for screenshots)
4613
+ document.addEventListener('visibilitychange', function () {
4614
+ if (document.hidden) {
4615
+ console.log('[Security] Tab hidden');
4616
+ }
4617
+ });
4618
+
4619
+ console.log('[Security] All protection features initialized');
4620
+ })();
4621
+
4622
+ // End of main IIFE - pdfDoc, pdfViewer not accessible from console
4623
+ })();
4624
+ </script>
380
4625
  </body>
381
4626
 
382
4627
  </html>