@libresign/pdf-elements 0.1.0

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.
@@ -0,0 +1,870 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3
+ SPDX-License-Identifier: AGPL-3.0-or-later
4
+ -->
5
+
6
+ <template>
7
+ <div :style="{ width, height }" class="pdf-elements-root">
8
+ <div
9
+ v-if="pdfDocuments.length"
10
+ class="pages-container"
11
+ :style="{ transform: `scale(${visualScale / scale})`, transformOrigin: 'top center' }"
12
+ >
13
+ <div v-for="(pdfDoc, docIndex) in pdfDocuments" :key="docIndex">
14
+ <div v-for="(page, pIndex) in pdfDoc.pages" :key="`${docIndex}-${pIndex}`" class="page-slot">
15
+ <div class="page-wrapper"
16
+ @mousedown="selectPage(docIndex, pIndex)"
17
+ @touchstart="selectPage(docIndex, pIndex)">
18
+ <div class="page-canvas" :class="{ 'shadow-outline': docIndex === selectedDocIndex && pIndex === selectedPageIndex }">
19
+ <PDFPage
20
+ :ref="`page${docIndex}-${pIndex}`"
21
+ :page="page"
22
+ :scale="scale"
23
+ @onMeasure="onMeasure($event, docIndex, pIndex)"
24
+ />
25
+ <div
26
+ class="overlay"
27
+ >
28
+ <div
29
+ v-if="isAddingMode && previewPageDocIndex === docIndex && previewPageIndex === pIndex && previewElement && previewVisible"
30
+ class="preview-element"
31
+ :style="{
32
+ left: `${previewPosition.x * previewScale.x}px`,
33
+ top: `${previewPosition.y * previewScale.y}px`,
34
+ width: `${previewElement.width * previewScale.x}px`,
35
+ height: `${previewElement.height * previewScale.y}px`,
36
+ }"
37
+ >
38
+ <slot
39
+ name="custom"
40
+ :object="previewElement"
41
+ :isSelected="false"
42
+ />
43
+ </div>
44
+
45
+ <DraggableElement
46
+ v-for="object in pdfDoc.allObjects[pIndex]"
47
+ :key="object.id"
48
+ :ref="`draggable${docIndex}-${pIndex}-${object.id}`"
49
+ :object="object"
50
+ :pages-scale="getRenderPageScale(docIndex, pIndex)"
51
+ :page-width="getPageWidth(docIndex, pIndex)"
52
+ :page-height="getPageHeight(docIndex, pIndex)"
53
+ :on-update="(payload) => updateObject(docIndex, object.id, payload)"
54
+ :on-delete="() => deleteObject(docIndex, object.id)"
55
+ :on-drag-start="(mouseX, mouseY, pointerOffset, dragShift) => startDraggingElement(docIndex, pIndex, object, mouseX, mouseY, pointerOffset, dragShift)"
56
+ :on-drag-move="updateDraggingPosition"
57
+ :on-drag-end="stopDraggingElement"
58
+ :is-being-dragged-globally="isDraggingElement && draggingObject && draggingObject.id === object.id"
59
+ :dragging-client-pos="draggingClientPosition"
60
+ :current-doc-index="docIndex"
61
+ :current-page-index="pIndex"
62
+ :global-drag-doc-index="draggingDocIndex"
63
+ :global-drag-page-index="draggingPageIndex"
64
+ :show-selection-ui="showSelectionHandles && !hideSelectionUI && object.resizable !== false"
65
+ :show-default-actions="showElementActions && !hideSelectionUI"
66
+ >
67
+ <template #default="slotProps">
68
+ <slot
69
+ :name="slotProps.object.type ? `element-${slotProps.object.type}` : 'custom'"
70
+ :object="slotProps.object"
71
+ :onDelete="slotProps.onDelete"
72
+ :onResize="slotProps.onResize"
73
+ >
74
+ <slot
75
+ name="custom"
76
+ :object="slotProps.object"
77
+ :onDelete="slotProps.onDelete"
78
+ :onResize="slotProps.onResize"
79
+ />
80
+ </slot>
81
+ </template>
82
+ <template #actions="slotProps">
83
+ <slot
84
+ name="actions"
85
+ :object="slotProps.object"
86
+ :onDelete="slotProps.onDelete"
87
+ />
88
+ </template>
89
+ </DraggableElement>
90
+ </div>
91
+ </div>
92
+ <div v-if="showPageFooter" class="page-footer">
93
+ <span>{{ pdfDoc.name }}</span>
94
+ <span>{{ formatPageNumber(pIndex + 1, pdfDoc.numPages) }}</span>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <div
102
+ v-if="isDraggingElement && draggingObject"
103
+ class="drag-portal"
104
+ :style="{
105
+ position: 'fixed',
106
+ left: `${draggingClientPosition.x}px`,
107
+ top: `${draggingClientPosition.y}px`,
108
+ width: `${draggingObject.width * draggingScale}px`,
109
+ height: `${draggingObject.height * draggingScale}px`,
110
+ pointerEvents: 'none',
111
+ }"
112
+ >
113
+ <slot
114
+ :name="draggingObject.type ? `element-${draggingObject.type}` : 'custom'"
115
+ :object="draggingObject"
116
+ :isSelected="false"
117
+ >
118
+ <slot
119
+ name="custom"
120
+ :object="draggingObject"
121
+ :isSelected="false"
122
+ />
123
+ </slot>
124
+ </div>
125
+
126
+ </div>
127
+ </template>
128
+
129
+ <script>
130
+ import debounce from 'debounce'
131
+ import PDFPage from './PDFPage.vue'
132
+ import DraggableElement from './DraggableElement.vue'
133
+ import { readAsPDF, readAsArrayBuffer } from '../utils/asyncReader.js'
134
+
135
+ export default {
136
+ name: 'PDFElements',
137
+ components: {
138
+ PDFPage,
139
+ DraggableElement,
140
+ },
141
+ props: {
142
+ width: {
143
+ type: String,
144
+ default: '100%',
145
+ },
146
+ height: {
147
+ type: String,
148
+ default: '100%',
149
+ },
150
+ initFiles: {
151
+ type: Array,
152
+ default: () => [],
153
+ },
154
+ initFileNames: {
155
+ type: Array,
156
+ default: () => [],
157
+ },
158
+ initialScale: {
159
+ type: Number,
160
+ default: 1,
161
+ },
162
+ showPageFooter: {
163
+ type: Boolean,
164
+ default: true,
165
+ },
166
+ hideSelectionUI: {
167
+ type: Boolean,
168
+ default: false,
169
+ },
170
+ showSelectionHandles: {
171
+ type: Boolean,
172
+ default: true,
173
+ },
174
+ showElementActions: {
175
+ type: Boolean,
176
+ default: true,
177
+ },
178
+ pageCountFormat: {
179
+ type: String,
180
+ default: '{currentPage} of {totalPages}',
181
+ },
182
+ },
183
+ data() {
184
+ return {
185
+ scale: this.initialScale,
186
+ pdfDocuments: [],
187
+ selectedDocIndex: -1,
188
+ selectedPageIndex: -1,
189
+ isAddingMode: false,
190
+ previewElement: null,
191
+ previewPosition: { x: 0, y: 0 },
192
+ previewScale: { x: 1, y: 1 },
193
+ previewPageDocIndex: -1,
194
+ previewPageIndex: -1,
195
+ previewVisible: false,
196
+ pagesBoundingRects: {},
197
+ isDraggingElement: false,
198
+ draggingObject: null,
199
+ draggingDocIndex: -1,
200
+ draggingPageIndex: -1,
201
+ draggingClientPosition: { x: 0, y: 0 },
202
+ draggingScale: 1,
203
+ draggingInitialMouseOffset: { x: 0, y: 0 },
204
+ draggingElementShift: { x: 0, y: 0 },
205
+ lastMouseClientPos: { x: 0, y: 0 },
206
+ viewportRafId: 0,
207
+ objectIndexCache: {},
208
+ zoomRafId: null,
209
+ boundHandleWheel: null,
210
+ debouncedApplyZoom: null,
211
+ visualScale: this.initialScale,
212
+ }
213
+ },
214
+ mounted() {
215
+ this.boundHandleWheel = this.handleWheel.bind(this)
216
+ this.debouncedApplyZoom = debounce(this.commitZoom, 150)
217
+ this.init()
218
+ document.addEventListener('mousemove', this.handleMouseMove)
219
+ document.addEventListener('touchmove', this.handleMouseMove, { passive: true })
220
+ document.addEventListener('mouseup', this.finishAdding)
221
+ document.addEventListener('touchend', this.finishAdding)
222
+ document.addEventListener('keydown', this.handleKeyDown)
223
+ window.addEventListener('scroll', this.onViewportScroll, { passive: true })
224
+ window.addEventListener('resize', this.onViewportScroll)
225
+ this.$el?.addEventListener('scroll', this.onViewportScroll, { passive: true })
226
+ this.$el?.addEventListener('wheel', this.boundHandleWheel, { passive: false })
227
+ },
228
+ beforeUnmount() {
229
+ if (this.zoomRafId) {
230
+ cancelAnimationFrame(this.zoomRafId)
231
+ }
232
+ this.debouncedApplyZoom?.clear?.()
233
+ if (this.boundHandleWheel) {
234
+ this.$el?.removeEventListener('wheel', this.boundHandleWheel)
235
+ }
236
+ document.removeEventListener('mousemove', this.handleMouseMove)
237
+ document.removeEventListener('touchmove', this.handleMouseMove)
238
+ document.removeEventListener('mouseup', this.finishAdding)
239
+ document.removeEventListener('touchend', this.finishAdding)
240
+ document.removeEventListener('keydown', this.handleKeyDown)
241
+ window.removeEventListener('scroll', this.onViewportScroll)
242
+ window.removeEventListener('resize', this.onViewportScroll)
243
+ this.$el?.removeEventListener('scroll', this.onViewportScroll)
244
+ if (this.viewportRafId) {
245
+ window.cancelAnimationFrame(this.viewportRafId)
246
+ this.viewportRafId = 0
247
+ }
248
+ },
249
+ methods: {
250
+ async init() {
251
+ if (!this.initFiles || this.initFiles.length === 0) return
252
+ const docs = []
253
+
254
+ for (let i = 0; i < this.initFiles.length; i++) {
255
+ const file = this.initFiles[i]
256
+ const name = this.initFileNames[i] || `document-${i + 1}.pdf`
257
+
258
+ let pdfDoc
259
+ if (file instanceof Blob) {
260
+ const buffer = await readAsArrayBuffer(file)
261
+ pdfDoc = await readAsPDF({ data: buffer })
262
+ } else {
263
+ pdfDoc = await readAsPDF(file)
264
+ }
265
+
266
+ const pages = []
267
+ for (let p = 1; p <= pdfDoc.numPages; p++) {
268
+ pages.push(pdfDoc.getPage(p))
269
+ }
270
+
271
+ docs.push({
272
+ name,
273
+ file,
274
+ pdfDoc,
275
+ numPages: pdfDoc.numPages,
276
+ pages,
277
+ pagesScale: Array(pdfDoc.numPages).fill(this.scale),
278
+ allObjects: Array(pdfDoc.numPages).fill(0).map(() => []),
279
+ })
280
+ }
281
+
282
+ this.pdfDocuments = docs
283
+ if (docs.length) {
284
+ this.selectedDocIndex = 0
285
+ this.selectedPageIndex = 0
286
+ this.$emit('pdf-elements:end-init', { docsCount: docs.length })
287
+ }
288
+ },
289
+
290
+ selectPage(docIndex, pageIndex) {
291
+ this.selectedDocIndex = docIndex
292
+ this.selectedPageIndex = pageIndex
293
+ },
294
+
295
+ startDraggingElement(docIndex, pageIndex, object, mouseX, mouseY, pointerOffset, dragShift) {
296
+ this.isDraggingElement = true
297
+ this.draggingObject = { ...object }
298
+ this.draggingDocIndex = docIndex
299
+ this.draggingPageIndex = pageIndex
300
+ this.draggingScale = this.getDisplayedPageScale(docIndex, pageIndex)
301
+ this.lastMouseClientPos.x = mouseX
302
+ this.lastMouseClientPos.y = mouseY
303
+ this.draggingElementShift = dragShift && typeof dragShift.x === 'number' && typeof dragShift.y === 'number'
304
+ ? dragShift
305
+ : { x: 0, y: 0 }
306
+
307
+ const pageKey = `${docIndex}-${pageIndex}`
308
+ if (!this.pagesBoundingRects[pageKey]) {
309
+ this.cachePageBounds()
310
+ }
311
+ const pageRect = this.pagesBoundingRects[pageKey]?.rect
312
+ if (pointerOffset && typeof pointerOffset.x === 'number' && typeof pointerOffset.y === 'number') {
313
+ this.draggingInitialMouseOffset.x = pointerOffset.x
314
+ this.draggingInitialMouseOffset.y = pointerOffset.y
315
+ this.draggingClientPosition.x = mouseX - this.draggingInitialMouseOffset.x
316
+ this.draggingClientPosition.y = mouseY - this.draggingInitialMouseOffset.y
317
+ } else if (pageRect) {
318
+ const elementScreenX = pageRect.left + (object.x * this.draggingScale)
319
+ const elementScreenY = pageRect.top + (object.y * this.draggingScale)
320
+ this.draggingInitialMouseOffset.x = mouseX - elementScreenX
321
+ this.draggingInitialMouseOffset.y = mouseY - elementScreenY
322
+
323
+ this.draggingClientPosition.x = mouseX - this.draggingInitialMouseOffset.x
324
+ this.draggingClientPosition.y = mouseY - this.draggingInitialMouseOffset.y
325
+ }
326
+
327
+ this.cachePageBounds()
328
+ },
329
+
330
+ updateDraggingPosition(clientX, clientY) {
331
+ if (!this.isDraggingElement) return
332
+
333
+ this.lastMouseClientPos.x = clientX
334
+ this.lastMouseClientPos.y = clientY
335
+
336
+ this.draggingClientPosition.x = clientX - this.draggingInitialMouseOffset.x
337
+ this.draggingClientPosition.y = clientY - this.draggingInitialMouseOffset.y
338
+ },
339
+
340
+ stopDraggingElement() {
341
+ if (this.draggingObject) {
342
+ const objectId = this.draggingObject.id
343
+ const originalDocIndex = this.draggingDocIndex
344
+
345
+ const finalPageIndex = this.checkAndMoveObjectPage(
346
+ this.draggingDocIndex,
347
+ objectId,
348
+ this.lastMouseClientPos.x,
349
+ this.lastMouseClientPos.y
350
+ )
351
+
352
+ if (finalPageIndex !== undefined) {
353
+ this.$nextTick(() => {
354
+ this.selectPage(originalDocIndex, finalPageIndex)
355
+
356
+ const refKey = `draggable${originalDocIndex}-${finalPageIndex}-${objectId}`
357
+ const draggableRefs = this.$refs[refKey]
358
+ if (draggableRefs && Array.isArray(draggableRefs) && draggableRefs[0]) {
359
+ draggableRefs[0].isSelected = true
360
+ }
361
+ })
362
+ }
363
+ }
364
+ this.isDraggingElement = false
365
+ this.draggingObject = null
366
+ this.draggingDocIndex = -1
367
+ this.draggingPageIndex = -1
368
+ this.draggingElementShift = { x: 0, y: 0 }
369
+ },
370
+
371
+ startAddingElement(templateObject) {
372
+ if (!this.pdfDocuments.length) return
373
+ this.isAddingMode = true
374
+ this.previewElement = { ...templateObject }
375
+ this.previewPageDocIndex = 0
376
+ this.previewPageIndex = 0
377
+ this.previewVisible = false
378
+ this.previewScale = { x: 1, y: 1 }
379
+ this.cachePageBounds()
380
+ },
381
+
382
+ cachePageBounds() {
383
+ this.pagesBoundingRects = {}
384
+ for (let docIdx = 0; docIdx < this.pdfDocuments.length; docIdx++) {
385
+ for (let pageIdx = 0; pageIdx < this.pdfDocuments[docIdx].pages.length; pageIdx++) {
386
+ const canvas = this.getPageCanvasElement(docIdx, pageIdx)
387
+ if (!canvas) continue
388
+ const rect = canvas.getBoundingClientRect()
389
+ this.pagesBoundingRects[`${docIdx}-${pageIdx}`] = {
390
+ docIndex: docIdx,
391
+ pageIndex: pageIdx,
392
+ rect,
393
+ }
394
+ }
395
+ }
396
+ },
397
+
398
+ getDisplayedPageScale(docIndex, pageIndex) {
399
+ const doc = this.pdfDocuments[docIndex]
400
+ if (!doc) return 1
401
+ const base = doc.pagesScale[pageIndex] || 1
402
+ const factor = this.visualScale && this.scale ? (this.visualScale / this.scale) : 1
403
+ return base * factor
404
+ },
405
+ getRenderPageScale(docIndex, pageIndex) {
406
+ const doc = this.pdfDocuments[docIndex]
407
+ if (!doc) return 1
408
+ return doc.pagesScale[pageIndex] || 1
409
+ },
410
+ getPageComponent(docIndex, pageIndex) {
411
+ const pageRef = this.$refs[`page${docIndex}-${pageIndex}`]
412
+ return pageRef && Array.isArray(pageRef) && pageRef[0] ? pageRef[0] : null
413
+ },
414
+ getPageCanvasElement(docIndex, pageIndex) {
415
+ const pageComponent = this.getPageComponent(docIndex, pageIndex)
416
+ return pageComponent ? (pageComponent.$el || pageComponent) : null
417
+ },
418
+
419
+ onViewportScroll() {
420
+ if (this.viewportRafId) return
421
+ this.viewportRafId = window.requestAnimationFrame(() => {
422
+ if (this.isAddingMode || this.isDraggingElement) {
423
+ this.cachePageBounds()
424
+ }
425
+ this.viewportRafId = 0
426
+ })
427
+ },
428
+
429
+ handleMouseMove(event) {
430
+ if (!this.isAddingMode || !this.previewElement) return
431
+
432
+ const clientX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX
433
+ const clientY = event.type.includes('touch') ? event.touches[0].clientY : event.clientY
434
+
435
+ let foundPage = false
436
+ for (const key in this.pagesBoundingRects) {
437
+ const { docIndex, pageIndex, rect } = this.pagesBoundingRects[key]
438
+ if (clientX >= rect.left && clientX <= rect.right &&
439
+ clientY >= rect.top && clientY <= rect.bottom) {
440
+ this.previewPageDocIndex = docIndex
441
+ this.previewPageIndex = pageIndex
442
+ foundPage = true
443
+
444
+ const canvasEl = this.getPageCanvasElement(docIndex, pageIndex)
445
+ const pagesScale = this.pdfDocuments[docIndex]?.pagesScale?.[pageIndex] || 1
446
+ const layoutWidth = canvasEl?.offsetWidth || rect.width
447
+ const layoutHeight = canvasEl?.offsetHeight || rect.height
448
+ const layoutScaleX = layoutWidth ? rect.width / layoutWidth : 1
449
+ const layoutScaleY = layoutHeight ? rect.height / layoutHeight : 1
450
+ const relX = (clientX - rect.left) / layoutScaleX / pagesScale
451
+ const relY = (clientY - rect.top) / layoutScaleY / pagesScale
452
+
453
+ const pageWidth = layoutWidth / pagesScale
454
+ const pageHeight = layoutHeight / pagesScale
455
+ this.previewScale.x = pagesScale * layoutScaleX
456
+ this.previewScale.y = pagesScale * layoutScaleY
457
+ let x = relX - this.previewElement.width / 2
458
+ let y = relY - this.previewElement.height / 2
459
+
460
+ x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width))
461
+ y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height))
462
+
463
+ this.previewPosition.x = x
464
+ this.previewPosition.y = y
465
+ this.previewVisible = true
466
+ break
467
+ }
468
+ }
469
+
470
+ if (!foundPage) {
471
+ this.previewVisible = false
472
+ this.previewScale = { x: 1, y: 1 }
473
+ }
474
+ },
475
+
476
+ handleKeyDown(event) {
477
+ if (event.key === 'Escape' && this.isAddingMode) {
478
+ this.cancelAdding()
479
+ }
480
+ },
481
+
482
+ handleWheel(event) {
483
+ if (!event.ctrlKey) return
484
+ event.preventDefault()
485
+
486
+ const factor = 1 - (event.deltaY * 0.002)
487
+ const nextVisual = Math.max(0.5, Math.min(3.0, this.visualScale * factor))
488
+ this.visualScale = nextVisual
489
+ this.debouncedApplyZoom()
490
+ },
491
+
492
+ commitZoom() {
493
+ const newScale = this.visualScale
494
+
495
+ this.scale = newScale
496
+
497
+ this.pdfDocuments.forEach((doc) => {
498
+ doc.pagesScale = doc.pagesScale.map(() => this.scale)
499
+ })
500
+
501
+ this.cachePageBounds()
502
+ },
503
+
504
+ finishAdding() {
505
+ if (!this.isAddingMode || !this.previewElement) return
506
+ if (!this.previewVisible) return
507
+
508
+ const objectToAdd = {
509
+ ...this.previewElement,
510
+ id: `obj-${Date.now()}`,
511
+ x: Math.round(this.previewPosition.x),
512
+ y: Math.round(this.previewPosition.y),
513
+ }
514
+
515
+ const doc = this.pdfDocuments[this.previewPageDocIndex]
516
+ const pageWidth = this.getPageWidth(this.previewPageDocIndex, this.previewPageIndex)
517
+ const pageHeight = this.getPageHeight(this.previewPageDocIndex, this.previewPageIndex)
518
+
519
+ if (objectToAdd.x < 0 || objectToAdd.y < 0 ||
520
+ objectToAdd.x + objectToAdd.width > pageWidth ||
521
+ objectToAdd.y + objectToAdd.height > pageHeight) {
522
+ this.cancelAdding()
523
+ return
524
+ }
525
+
526
+ doc.allObjects[this.previewPageIndex].push(objectToAdd)
527
+
528
+ const pageIndex = this.previewPageIndex
529
+ const docIndex = this.previewPageDocIndex
530
+ const objectId = objectToAdd.id
531
+
532
+ this.cancelAdding()
533
+
534
+ this.$nextTick(() => {
535
+ const refKey = `draggable${docIndex}-${pageIndex}-${objectId}`
536
+ const draggableRefs = this.$refs[refKey]
537
+ if (draggableRefs && Array.isArray(draggableRefs) && draggableRefs[0]) {
538
+ draggableRefs[0].isSelected = true
539
+ }
540
+ })
541
+ },
542
+
543
+ cancelAdding() {
544
+ this.isAddingMode = false
545
+ this.previewElement = null
546
+ this.previewVisible = false
547
+ },
548
+
549
+ addObjectToPage(object, pageIndex = this.selectedPageIndex, docIndex = this.selectedDocIndex) {
550
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return false
551
+ if (pageIndex < 0 || pageIndex >= this.pdfDocuments[docIndex].pages.length) return false
552
+
553
+ const doc = this.pdfDocuments[docIndex]
554
+ const pageRef = this.getPageComponent(docIndex, pageIndex)
555
+ if (!pageRef) return false
556
+
557
+ const pageWidth = this.getPageWidth(docIndex, pageIndex)
558
+ const pageHeight = this.getPageHeight(docIndex, pageIndex)
559
+
560
+ if (object.x < 0 || object.y < 0 ||
561
+ object.x + object.width > pageWidth ||
562
+ object.y + object.height > pageHeight) {
563
+ return false
564
+ }
565
+
566
+ doc.allObjects = doc.allObjects.map((objects, pIndex) =>
567
+ pIndex === pageIndex ? [...objects, object] : objects,
568
+ )
569
+ return true
570
+ },
571
+
572
+ getAllObjects(docIndex = this.selectedDocIndex) {
573
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return []
574
+
575
+ const doc = this.pdfDocuments[docIndex]
576
+ const scale = this.scale || 1
577
+ const result = []
578
+
579
+ doc.allObjects.forEach((pageObjects, pageIndex) => {
580
+ const pageRef = this.getPageComponent(docIndex, pageIndex)
581
+ if (!pageRef) return
582
+
583
+ const measurement = pageRef.getCanvasMeasurement()
584
+ const normalizedCanvasHeight = measurement.canvasHeight / scale
585
+
586
+ pageObjects.forEach(object => {
587
+ result.push({
588
+ ...object,
589
+ pageIndex,
590
+ pageNumber: pageIndex + 1,
591
+ scale,
592
+ normalizedCoordinates: {
593
+ llx: parseInt(object.x, 10),
594
+ lly: parseInt(normalizedCanvasHeight - object.y, 10),
595
+ ury: parseInt(normalizedCanvasHeight - object.y - object.height, 10),
596
+ },
597
+ })
598
+ })
599
+ })
600
+
601
+ return result
602
+ },
603
+
604
+ updateObject(docIndex, objectId, payload) {
605
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
606
+ const doc = this.pdfDocuments[docIndex]
607
+
608
+ const cacheKey = `${docIndex}-${objectId}`
609
+ let currentPageIndex = this.objectIndexCache[cacheKey]
610
+
611
+ if (currentPageIndex === undefined) {
612
+ doc.allObjects.forEach((objects, pIndex) => {
613
+ if (objects.find(o => o.id === objectId)) {
614
+ currentPageIndex = pIndex
615
+ this.objectIndexCache[cacheKey] = pIndex
616
+ }
617
+ })
618
+ }
619
+
620
+ if (currentPageIndex === undefined) return
621
+
622
+ const targetObject = doc.allObjects[currentPageIndex]?.find(o => o.id === objectId)
623
+ if (!targetObject) return
624
+
625
+ if (payload._globalDrag && payload._mouseX !== undefined && payload._mouseY !== undefined) {
626
+ const mouseX = payload._mouseX
627
+ const mouseY = payload._mouseY
628
+
629
+ if (!this.pagesBoundingRects || Object.keys(this.pagesBoundingRects).length === 0) {
630
+ this.cachePageBounds()
631
+ }
632
+
633
+ const currentPageRect = this.pagesBoundingRects[`${docIndex}-${currentPageIndex}`]?.rect
634
+ if (currentPageRect) {
635
+ const pagesScale = this.getDisplayedPageScale(docIndex, currentPageIndex)
636
+ const relX = (mouseX - currentPageRect.left - this.draggingElementShift.x) / pagesScale - (this.draggingInitialMouseOffset.x / pagesScale)
637
+ const relY = (mouseY - currentPageRect.top - this.draggingElementShift.y) / pagesScale - (this.draggingInitialMouseOffset.y / pagesScale)
638
+
639
+ doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].map(obj =>
640
+ obj.id === objectId ? { ...obj, x: relX, y: relY } : obj
641
+ )
642
+ }
643
+ return
644
+ }
645
+
646
+ if (payload.x !== undefined || payload.y !== undefined) {
647
+ const newX = payload.x !== undefined ? payload.x : targetObject.x
648
+ const newY = payload.y !== undefined ? payload.y : targetObject.y
649
+ const objWidth = payload.width !== undefined ? payload.width : targetObject.width
650
+ const objHeight = payload.height !== undefined ? payload.height : targetObject.height
651
+
652
+ let bestPageIndex = currentPageIndex
653
+ let maxVisibleArea = 0
654
+
655
+ for (let pIndex = 0; pIndex < doc.pages.length; pIndex++) {
656
+ const pageWidth = this.getPageWidth(docIndex, pIndex)
657
+ const pageHeight = this.getPageHeight(docIndex, pIndex)
658
+
659
+ const visibleLeft = Math.max(0, newX)
660
+ const visibleTop = Math.max(0, newY)
661
+ const visibleRight = Math.min(pageWidth, newX + objWidth)
662
+ const visibleBottom = Math.min(pageHeight, newY + objHeight)
663
+
664
+ if (visibleRight > visibleLeft && visibleBottom > visibleTop) {
665
+ const visibleArea = (visibleRight - visibleLeft) * (visibleBottom - visibleTop)
666
+ if (visibleArea > maxVisibleArea) {
667
+ maxVisibleArea = visibleArea
668
+ bestPageIndex = pIndex
669
+ }
670
+ }
671
+ }
672
+
673
+ if (bestPageIndex !== currentPageIndex) {
674
+ const pageWidth = this.getPageWidth(docIndex, bestPageIndex)
675
+ const pageHeight = this.getPageHeight(docIndex, bestPageIndex)
676
+
677
+ const adjustedX = Math.max(0, Math.min(newX, pageWidth - objWidth))
678
+ const adjustedY = Math.max(0, Math.min(newY, pageHeight - objHeight))
679
+
680
+ doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].filter(
681
+ obj => obj.id !== objectId
682
+ )
683
+ const updatedObject = {
684
+ ...targetObject,
685
+ ...payload,
686
+ x: adjustedX,
687
+ y: adjustedY,
688
+ }
689
+ doc.allObjects[bestPageIndex].push(updatedObject)
690
+ this.objectIndexCache[`${docIndex}-${objectId}`] = bestPageIndex
691
+ return
692
+ }
693
+
694
+ const pageWidth = this.getPageWidth(docIndex, currentPageIndex)
695
+ const pageHeight = this.getPageHeight(docIndex, currentPageIndex)
696
+
697
+ if (newX < 0 || newY < 0 ||
698
+ newX + objWidth > pageWidth ||
699
+ newY + objHeight > pageHeight) {
700
+ return
701
+ }
702
+ }
703
+
704
+ doc.allObjects = doc.allObjects.map(objects =>
705
+ objects.map(object => (object.id === objectId ? { ...object, ...payload } : object)),
706
+ )
707
+ },
708
+
709
+ deleteObject(docIndex, objectId) {
710
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
711
+ const doc = this.pdfDocuments[docIndex]
712
+ doc.allObjects = doc.allObjects.map(objects =>
713
+ objects.filter(object => object.id !== objectId),
714
+ )
715
+ delete this.objectIndexCache[`${docIndex}-${objectId}`]
716
+ },
717
+
718
+ checkAndMoveObjectPage(docIndex, objectId, mouseX, mouseY) {
719
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return undefined
720
+ const doc = this.pdfDocuments[docIndex]
721
+
722
+ const cacheKey = `${docIndex}-${objectId}`
723
+ let currentPageIndex = this.objectIndexCache[cacheKey]
724
+
725
+ if (currentPageIndex === undefined) {
726
+ doc.allObjects.forEach((objects, pIndex) => {
727
+ if (objects.find(o => o.id === objectId)) {
728
+ currentPageIndex = pIndex
729
+ this.objectIndexCache[cacheKey] = pIndex
730
+ }
731
+ })
732
+ }
733
+
734
+ if (currentPageIndex === undefined) return undefined
735
+
736
+ const targetObject = doc.allObjects[currentPageIndex]?.find(o => o.id === objectId)
737
+ if (!targetObject) return currentPageIndex
738
+
739
+ let targetPageIndex = currentPageIndex
740
+ for (const key in this.pagesBoundingRects) {
741
+ const { docIndex: rectDocIndex, pageIndex, rect } = this.pagesBoundingRects[key]
742
+ if (rectDocIndex === docIndex &&
743
+ mouseX >= rect.left && mouseX <= rect.right &&
744
+ mouseY >= rect.top && mouseY <= rect.bottom) {
745
+ targetPageIndex = pageIndex
746
+ break
747
+ }
748
+ }
749
+
750
+ const targetPageRect = this.pagesBoundingRects[`${docIndex}-${targetPageIndex}`]?.rect
751
+ if (!targetPageRect) return currentPageIndex
752
+
753
+ const pagesScale = this.getDisplayedPageScale(docIndex, targetPageIndex)
754
+ const relX = (mouseX - targetPageRect.left - this.draggingElementShift.x) / pagesScale - (this.draggingInitialMouseOffset.x / pagesScale)
755
+ const relY = (mouseY - targetPageRect.top - this.draggingElementShift.y) / pagesScale - (this.draggingInitialMouseOffset.y / pagesScale)
756
+
757
+ const pageWidth = this.getPageWidth(docIndex, targetPageIndex)
758
+ const pageHeight = this.getPageHeight(docIndex, targetPageIndex)
759
+
760
+ const clampedX = Math.max(0, Math.min(relX, pageWidth - targetObject.width))
761
+ const clampedY = Math.max(0, Math.min(relY, pageHeight - targetObject.height))
762
+
763
+ if (targetPageIndex !== currentPageIndex) {
764
+ doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].filter(
765
+ obj => obj.id !== objectId
766
+ )
767
+ doc.allObjects[targetPageIndex].push({
768
+ ...targetObject,
769
+ x: clampedX,
770
+ y: clampedY,
771
+ })
772
+ this.objectIndexCache[cacheKey] = targetPageIndex
773
+ } else if (clampedX !== targetObject.x || clampedY !== targetObject.y) {
774
+ doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].map(obj =>
775
+ obj.id === objectId ? { ...obj, x: clampedX, y: clampedY } : obj
776
+ )
777
+ }
778
+
779
+ return targetPageIndex
780
+ },
781
+
782
+ onMeasure(e, docIndex, pageIndex) {
783
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
784
+ this.pdfDocuments[docIndex].pagesScale[pageIndex] = e.scale
785
+ this.cachePageBounds()
786
+ },
787
+
788
+ formatPageNumber(currentPage, totalPages) {
789
+ return this.pageCountFormat
790
+ .replace('{currentPage}', currentPage)
791
+ .replace('{totalPages}', totalPages)
792
+ },
793
+ getPageWidth(docIndex, pageIndex) {
794
+ const pageRef = this.getPageComponent(docIndex, pageIndex)
795
+ if (!pageRef) return 0
796
+ const doc = this.pdfDocuments[docIndex]
797
+ const pagesScale = doc.pagesScale[pageIndex] || 1
798
+ return pageRef.getCanvasMeasurement().canvasWidth / pagesScale
799
+ },
800
+ getPageHeight(docIndex, pageIndex) {
801
+ const pageRef = this.getPageComponent(docIndex, pageIndex)
802
+ if (!pageRef) return 0
803
+ const doc = this.pdfDocuments[docIndex]
804
+ const pagesScale = doc.pagesScale[pageIndex] || 1
805
+ return pageRef.getCanvasMeasurement().canvasHeight / pagesScale
806
+ },
807
+ },
808
+ }
809
+ </script>
810
+
811
+ <style scoped>
812
+ .pdf-elements-root {
813
+ width: 100%;
814
+ height: 100%;
815
+ overflow-y: auto;
816
+ overflow-x: hidden;
817
+ box-sizing: border-box;
818
+ }
819
+ .pages-container {
820
+ width: 100%;
821
+ padding: 20px 0 0 0;
822
+ text-align: center;
823
+ background: #f7fafc;
824
+ overflow: hidden;
825
+ }
826
+ .page-slot {
827
+ margin: 0 auto;
828
+ }
829
+ .page-wrapper {
830
+ display: inline-block;
831
+ margin-bottom: 0;
832
+ }
833
+ .page-canvas {
834
+ position: relative;
835
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
836
+ }
837
+ .shadow-outline {
838
+ box-shadow: 0 0 0 3px rgb(66 153 225 / 50%);
839
+ }
840
+ .preview-element {
841
+ position: absolute;
842
+ opacity: 0.7;
843
+ pointer-events: none;
844
+ }
845
+ .drag-ghost {
846
+ position: fixed;
847
+ opacity: 0.9;
848
+ pointer-events: none;
849
+ z-index: 10000;
850
+ transform-origin: top left;
851
+ transition: none;
852
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
853
+ }
854
+ .overlay {
855
+ position: absolute;
856
+ top: 0;
857
+ left: 0;
858
+ transform-origin: top left;
859
+ width: 100%;
860
+ height: 100%;
861
+ }
862
+ .page-footer {
863
+ display: flex;
864
+ justify-content: space-between;
865
+ align-items: center;
866
+ padding: 12px 20px 20px 20px;
867
+ color: #4b5563;
868
+ font-size: 14px;
869
+ }
870
+ </style>