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