@libresign/pdf-elements 0.2.4 → 0.3.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.
@@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
60
60
  :read-only="readOnly"
61
61
  :on-update="(payload) => updateObject(docIndex, object.id, payload)"
62
62
  :on-delete="() => deleteObject(docIndex, object.id)"
63
+ :on-duplicate="() => duplicateObject(docIndex, object.id)"
63
64
  :on-drag-start="(mouseX, mouseY, pointerOffset, dragShift) => startDraggingElement(docIndex, pIndex, object, mouseX, mouseY, pointerOffset, dragShift)"
64
65
  :on-drag-move="updateDraggingPosition"
65
66
  :on-drag-end="stopDraggingElement"
@@ -71,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
71
72
  :global-drag-page-index="draggingPageIndex"
72
73
  :show-selection-ui="showSelectionHandles && !hideSelectionUI && object.resizable !== false"
73
74
  :show-default-actions="showElementActions && !hideSelectionUI"
75
+ :ignore-click-outside-selectors="ignoreClickOutsideSelectors"
74
76
  >
75
77
  <template #default="slotProps">
76
78
  <slot
@@ -92,6 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
92
94
  name="actions"
93
95
  :object="slotProps.object"
94
96
  :onDelete="slotProps.onDelete"
97
+ :onDuplicate="slotProps.onDuplicate"
95
98
  />
96
99
  </template>
97
100
  </DraggableElement>
@@ -138,6 +141,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later
138
141
  import PDFPage from './PDFPage.vue'
139
142
  import DraggableElement from './DraggableElement.vue'
140
143
  import { readAsPDF, readAsArrayBuffer } from '../utils/asyncReader.js'
144
+ import { clampPosition, getVisibleArea } from '../utils/geometry.js'
145
+ import { getViewportWindow, isPageInViewport } from '../utils/pageBounds.js'
146
+ import { applyScaleToDocs } from '../utils/zoom.js'
147
+ import { objectIdExistsInDoc, findObjectPageIndex, updateObjectInDoc, removeObjectFromDoc } from '../utils/objectStore.js'
148
+ import { getCachedMeasurement } from '../utils/measurements.js'
141
149
 
142
150
  export default {
143
151
  name: 'PDFElements',
@@ -186,6 +194,10 @@ export default {
186
194
  type: Boolean,
187
195
  default: false,
188
196
  },
197
+ ignoreClickOutsideSelectors: {
198
+ type: Array,
199
+ default: () => [],
200
+ },
189
201
  pageCountFormat: {
190
202
  type: String,
191
203
  default: '{currentPage} of {totalPages}',
@@ -208,7 +220,16 @@ export default {
208
220
  previewPageDocIndex: -1,
209
221
  previewPageIndex: -1,
210
222
  previewVisible: false,
211
- pagesBoundingRects: {},
223
+ hoverRafId: 0,
224
+ pendingHoverClientPos: null,
225
+ lastHoverRect: null,
226
+ addingListenersAttached: false,
227
+ dragRafId: 0,
228
+ pendingDragClientPos: null,
229
+ pageBoundsVersion: 0,
230
+ lastScrollTop: 0,
231
+ lastClientWidth: 0,
232
+ nextObjectCounter: 0,
212
233
  isDraggingElement: false,
213
234
  draggingObject: null,
214
235
  draggingDocIndex: -1,
@@ -228,14 +249,16 @@ export default {
228
249
  lastContainerWidth: 0,
229
250
  }
230
251
  },
252
+ created() {
253
+ this._pagesBoundingRects = {}
254
+ this._pagesBoundingRectsList = []
255
+ this._pageMeasurementCache = {}
256
+ this._lastPageBoundsScrollTop = 0
257
+ this._lastPageBoundsClientHeight = 0
258
+ },
231
259
  mounted() {
232
260
  this.boundHandleWheel = this.handleWheel.bind(this)
233
261
  this.init()
234
- document.addEventListener('mousemove', this.handleMouseMove)
235
- document.addEventListener('touchmove', this.handleMouseMove, { passive: true })
236
- document.addEventListener('mouseup', this.finishAdding)
237
- document.addEventListener('touchend', this.finishAdding)
238
- document.addEventListener('keydown', this.handleKeyDown)
239
262
  window.addEventListener('scroll', this.onViewportScroll, { passive: true })
240
263
  window.addEventListener('resize', this.onViewportScroll)
241
264
  this.$el?.addEventListener('scroll', this.onViewportScroll, { passive: true })
@@ -252,11 +275,7 @@ export default {
252
275
  if (this.boundHandleWheel) {
253
276
  this.$el?.removeEventListener('wheel', this.boundHandleWheel)
254
277
  }
255
- document.removeEventListener('mousemove', this.handleMouseMove)
256
- document.removeEventListener('touchmove', this.handleMouseMove)
257
- document.removeEventListener('mouseup', this.finishAdding)
258
- document.removeEventListener('touchend', this.finishAdding)
259
- document.removeEventListener('keydown', this.handleKeyDown)
278
+ this.detachAddingListeners()
260
279
  window.removeEventListener('scroll', this.onViewportScroll)
261
280
  window.removeEventListener('resize', this.onViewportScroll)
262
281
  this.$el?.removeEventListener('scroll', this.onViewportScroll)
@@ -264,6 +283,14 @@ export default {
264
283
  window.cancelAnimationFrame(this.viewportRafId)
265
284
  this.viewportRafId = 0
266
285
  }
286
+ if (this.hoverRafId) {
287
+ window.cancelAnimationFrame(this.hoverRafId)
288
+ this.hoverRafId = 0
289
+ }
290
+ if (this.dragRafId) {
291
+ window.cancelAnimationFrame(this.dragRafId)
292
+ this.dragRafId = 0
293
+ }
267
294
  },
268
295
  methods: {
269
296
  async init() {
@@ -288,7 +315,7 @@ export default {
288
315
  for (let p = 1; p <= pdfDoc.numPages; p++) {
289
316
  const pagePromise = pdfDoc.getPage(p)
290
317
  pagePromise.then((page) => {
291
- pageWidths[p - 1] = page.getViewport({ scale: 1 }).width
318
+ pageWidths.splice(p - 1, 1, page.getViewport({ scale: 1 }).width)
292
319
  if (this.autoFitZoom) {
293
320
  this.scheduleAutoFitZoom()
294
321
  }
@@ -309,6 +336,7 @@ export default {
309
336
  }
310
337
 
311
338
  this.pdfDocuments = docs
339
+ this._pageMeasurementCache = {}
312
340
  if (docs.length) {
313
341
  this.selectedDocIndex = 0
314
342
  this.selectedPageIndex = 0
@@ -338,11 +366,8 @@ export default {
338
366
  ? dragShift
339
367
  : { x: 0, y: 0 }
340
368
 
341
- const pageKey = `${docIndex}-${pageIndex}`
342
- if (!this.pagesBoundingRects[pageKey]) {
343
- this.cachePageBounds()
344
- }
345
- const pageRect = this.pagesBoundingRects[pageKey]?.rect
369
+ this.cachePageBounds()
370
+ const pageRect = this.getPageRect(docIndex, pageIndex)
346
371
  if (pointerOffset && typeof pointerOffset.x === 'number' && typeof pointerOffset.y === 'number') {
347
372
  this.draggingInitialMouseOffset.x = pointerOffset.x
348
373
  this.draggingInitialMouseOffset.y = pointerOffset.y
@@ -358,17 +383,22 @@ export default {
358
383
  this.draggingClientPosition.y = mouseY - this.draggingInitialMouseOffset.y
359
384
  }
360
385
 
361
- this.cachePageBounds()
362
386
  },
363
387
 
364
388
  updateDraggingPosition(clientX, clientY) {
365
389
  if (!this.isDraggingElement) return
366
390
 
367
- this.lastMouseClientPos.x = clientX
368
- this.lastMouseClientPos.y = clientY
369
-
370
- this.draggingClientPosition.x = clientX - this.draggingInitialMouseOffset.x
371
- this.draggingClientPosition.y = clientY - this.draggingInitialMouseOffset.y
391
+ this.pendingDragClientPos = { x: clientX, y: clientY }
392
+ if (this.dragRafId) return
393
+ this.dragRafId = window.requestAnimationFrame(() => {
394
+ this.dragRafId = 0
395
+ const pending = this.pendingDragClientPos
396
+ if (!pending) return
397
+ this.lastMouseClientPos.x = pending.x
398
+ this.lastMouseClientPos.y = pending.y
399
+ this.draggingClientPosition.x = pending.x - this.draggingInitialMouseOffset.x
400
+ this.draggingClientPosition.y = pending.y - this.draggingInitialMouseOffset.y
401
+ })
372
402
  },
373
403
 
374
404
  stopDraggingElement() {
@@ -400,10 +430,12 @@ export default {
400
430
  this.draggingDocIndex = -1
401
431
  this.draggingPageIndex = -1
402
432
  this.draggingElementShift = { x: 0, y: 0 }
433
+ this.pendingDragClientPos = null
403
434
  },
404
435
 
405
436
  startAddingElement(templateObject) {
406
437
  if (!this.pdfDocuments.length) return
438
+ this.attachAddingListeners()
407
439
  this.isAddingMode = true
408
440
  this.previewElement = { ...templateObject }
409
441
  this.previewPageDocIndex = 0
@@ -415,10 +447,29 @@ export default {
415
447
 
416
448
  cachePageBounds() {
417
449
  const nextRects = {}
450
+ const container = this.$el
451
+ const scrollTop = container?.scrollTop || 0
452
+ const viewHeight = container?.clientHeight || 0
453
+ if (!this.isAddingMode && !this.isDraggingElement &&
454
+ scrollTop === this._lastPageBoundsScrollTop &&
455
+ viewHeight === this._lastPageBoundsClientHeight) {
456
+ return
457
+ }
458
+ this._lastPageBoundsScrollTop = scrollTop
459
+ this._lastPageBoundsClientHeight = viewHeight
460
+ const { minY, maxY } = getViewportWindow(scrollTop, viewHeight)
418
461
  for (let docIdx = 0; docIdx < this.pdfDocuments.length; docIdx++) {
419
462
  for (let pageIdx = 0; pageIdx < this.pdfDocuments[docIdx].pages.length; pageIdx++) {
420
463
  const canvas = this.getPageCanvasElement(docIdx, pageIdx)
421
464
  if (!canvas) continue
465
+ if (viewHeight) {
466
+ const wrapper = canvas.closest('.page-wrapper') || canvas
467
+ const offsetTop = wrapper.offsetTop || 0
468
+ const offsetHeight = wrapper.offsetHeight || 0
469
+ if (!isPageInViewport(offsetTop, offsetHeight, minY, maxY)) {
470
+ continue
471
+ }
472
+ }
422
473
  const rect = canvas.getBoundingClientRect()
423
474
  nextRects[`${docIdx}-${pageIdx}`] = {
424
475
  docIndex: docIdx,
@@ -427,31 +478,71 @@ export default {
427
478
  }
428
479
  }
429
480
  }
430
- this.pagesBoundingRects = nextRects
481
+ this._pagesBoundingRects = nextRects
482
+ this._pagesBoundingRectsList = Object.values(nextRects)
483
+ this.pageBoundsVersion++
484
+ },
485
+ cachePageBoundsForPage(docIndex, pageIndex) {
486
+ const canvas = this.getPageCanvasElement(docIndex, pageIndex)
487
+ if (!canvas) return
488
+ const rect = canvas.getBoundingClientRect()
489
+ this._pagesBoundingRects = {
490
+ ...this._pagesBoundingRects,
491
+ [`${docIndex}-${pageIndex}`]: {
492
+ docIndex,
493
+ pageIndex,
494
+ rect,
495
+ },
496
+ }
497
+ this._pagesBoundingRectsList = Object.values(this._pagesBoundingRects)
498
+ this.pageBoundsVersion++
499
+ },
500
+ getPageBoundsMap() {
501
+ return this._pagesBoundingRects || {}
502
+ },
503
+ getPageBoundsList() {
504
+ return this._pagesBoundingRectsList || []
505
+ },
506
+ getPageRect(docIndex, pageIndex) {
507
+ return this.getPageBoundsMap()[`${docIndex}-${pageIndex}`]?.rect || null
508
+ },
509
+ getPointerPosition(event) {
510
+ if (event?.type?.includes?.('touch')) {
511
+ return {
512
+ x: event.touches?.[0]?.clientX,
513
+ y: event.touches?.[0]?.clientY,
514
+ }
515
+ }
516
+ return {
517
+ x: event?.clientX,
518
+ y: event?.clientY,
519
+ }
431
520
  },
432
521
 
433
522
  getDisplayedPageScale(docIndex, pageIndex) {
523
+ this.pageBoundsVersion
434
524
  const doc = this.pdfDocuments[docIndex]
435
525
  if (!doc) return 1
436
526
  const baseWidth = doc.pageWidths?.[pageIndex] || 0
437
- const rectWidth = this.pagesBoundingRects[`${docIndex}-${pageIndex}`]?.rect?.width || 0
527
+ const pageBoundsMap = this.getPageBoundsMap()
528
+ if (!pageBoundsMap[`${docIndex}-${pageIndex}`]) {
529
+ this.cachePageBoundsForPage(docIndex, pageIndex)
530
+ }
531
+ const rectWidth = this.getPageBoundsMap()[`${docIndex}-${pageIndex}`]?.rect?.width || 0
438
532
  if (rectWidth && baseWidth) {
439
533
  return rectWidth / baseWidth
440
534
  }
441
- const canvas = this.getPageCanvasElement(docIndex, pageIndex)
442
- const fallbackRectWidth = canvas?.getBoundingClientRect?.().width || 0
443
- if (fallbackRectWidth && baseWidth) {
444
- return fallbackRectWidth / baseWidth
535
+ if (this.isAddingMode || this.isDraggingElement) {
536
+ const canvas = this.getPageCanvasElement(docIndex, pageIndex)
537
+ const fallbackRectWidth = canvas?.getBoundingClientRect?.().width || 0
538
+ if (fallbackRectWidth && baseWidth) {
539
+ return fallbackRectWidth / baseWidth
540
+ }
445
541
  }
446
542
  const base = doc.pagesScale[pageIndex] || 1
447
543
  const factor = this.visualScale && this.scale ? (this.visualScale / this.scale) : 1
448
544
  return base * factor
449
545
  },
450
- getRenderPageScale(docIndex, pageIndex) {
451
- const doc = this.pdfDocuments[docIndex]
452
- if (!doc) return 1
453
- return doc.pagesScale[pageIndex] || 1
454
- },
455
546
  getPageComponent(docIndex, pageIndex) {
456
547
  const pageRef = this.$refs[`page${docIndex}-${pageIndex}`]
457
548
  return pageRef && Array.isArray(pageRef) && pageRef[0] ? pageRef[0] : null
@@ -464,13 +555,22 @@ export default {
464
555
  onViewportScroll() {
465
556
  if (this.viewportRafId) return
466
557
  this.viewportRafId = window.requestAnimationFrame(() => {
558
+ const container = this.$el
559
+ const scrollTop = container?.scrollTop || 0
560
+ const clientWidth = container?.clientWidth || 0
561
+ const scrollChanged = scrollTop !== this.lastScrollTop
562
+ const widthChanged = clientWidth !== this.lastClientWidth
563
+ this.lastScrollTop = scrollTop
564
+ this.lastClientWidth = clientWidth
565
+
467
566
  if (this.isAddingMode || this.isDraggingElement) {
468
- this.cachePageBounds()
567
+ if (scrollChanged || widthChanged) {
568
+ this.cachePageBounds()
569
+ }
469
570
  }
470
571
  if (this.autoFitZoom && !this.isAddingMode && !this.isDraggingElement) {
471
- const containerWidth = this.$el?.clientWidth || 0
472
- if (containerWidth && containerWidth !== this.lastContainerWidth) {
473
- this.lastContainerWidth = containerWidth
572
+ if (clientWidth && widthChanged) {
573
+ this.lastContainerWidth = clientWidth
474
574
  this.autoFitApplied = false
475
575
  this.scheduleAutoFitZoom()
476
576
  }
@@ -481,49 +581,75 @@ export default {
481
581
 
482
582
  handleMouseMove(event) {
483
583
  if (!this.isAddingMode || !this.previewElement) return
584
+ const { x, y } = this.getPointerPosition(event)
585
+ if (x === undefined || y === undefined) return
586
+ this.pendingHoverClientPos = { x, y }
587
+ if (this.hoverRafId) return
588
+ this.hoverRafId = window.requestAnimationFrame(() => {
589
+ this.hoverRafId = 0
590
+ const pending = this.pendingHoverClientPos
591
+ if (!pending) return
592
+
593
+ const cursorX = pending.x
594
+ const cursorY = pending.y
595
+ let target = null
596
+
597
+ if (this.lastHoverRect &&
598
+ cursorX >= this.lastHoverRect.left && cursorX <= this.lastHoverRect.right &&
599
+ cursorY >= this.lastHoverRect.top && cursorY <= this.lastHoverRect.bottom) {
600
+ target = {
601
+ docIndex: this.previewPageDocIndex,
602
+ pageIndex: this.previewPageIndex,
603
+ rect: this.lastHoverRect,
604
+ }
605
+ } else {
606
+ const rects = this.getPageBoundsList().length
607
+ ? this.getPageBoundsList()
608
+ : Object.values(this.getPageBoundsMap())
609
+ for (let i = 0; i < rects.length; i++) {
610
+ const entry = rects[i]
611
+ const rect = entry.rect
612
+ if (cursorX >= rect.left && cursorX <= rect.right &&
613
+ cursorY >= rect.top && cursorY <= rect.bottom) {
614
+ target = entry
615
+ break
616
+ }
617
+ }
618
+ }
484
619
 
485
- const clientX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX
486
- const clientY = event.type.includes('touch') ? event.touches[0].clientY : event.clientY
487
-
488
- let foundPage = false
489
- for (const key in this.pagesBoundingRects) {
490
- const { docIndex, pageIndex, rect } = this.pagesBoundingRects[key]
491
- if (clientX >= rect.left && clientX <= rect.right &&
492
- clientY >= rect.top && clientY <= rect.bottom) {
493
- this.previewPageDocIndex = docIndex
494
- this.previewPageIndex = pageIndex
495
- foundPage = true
496
-
497
- const canvasEl = this.getPageCanvasElement(docIndex, pageIndex)
498
- const pagesScale = this.pdfDocuments[docIndex]?.pagesScale?.[pageIndex] || 1
499
- const renderWidth = canvasEl?.width || rect.width
500
- const renderHeight = canvasEl?.height || rect.height
501
- const layoutScaleX = renderWidth ? rect.width / renderWidth : 1
502
- const layoutScaleY = renderHeight ? rect.height / renderHeight : 1
503
- const relX = (clientX - rect.left) / layoutScaleX / pagesScale
504
- const relY = (clientY - rect.top) / layoutScaleY / pagesScale
505
-
506
- const pageWidth = renderWidth / pagesScale
507
- const pageHeight = renderHeight / pagesScale
508
- this.previewScale.x = pagesScale
509
- this.previewScale.y = pagesScale
510
- let x = relX - this.previewElement.width / 2
511
- let y = relY - this.previewElement.height / 2
512
-
513
- x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width))
514
- y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height))
515
-
516
- this.previewPosition.x = x
517
- this.previewPosition.y = y
518
- this.previewVisible = true
519
- break
620
+ if (!target) {
621
+ this.previewVisible = false
622
+ this.previewScale = { x: 1, y: 1 }
623
+ this.lastHoverRect = null
624
+ return
520
625
  }
521
- }
522
626
 
523
- if (!foundPage) {
524
- this.previewVisible = false
525
- this.previewScale = { x: 1, y: 1 }
526
- }
627
+ this.previewPageDocIndex = target.docIndex
628
+ this.previewPageIndex = target.pageIndex
629
+ this.lastHoverRect = target.rect
630
+ const canvasEl = this.getPageCanvasElement(target.docIndex, target.pageIndex)
631
+ const pagesScale = this.pdfDocuments[target.docIndex]?.pagesScale?.[target.pageIndex] || 1
632
+ const renderWidth = canvasEl?.width || target.rect.width
633
+ const renderHeight = canvasEl?.height || target.rect.height
634
+ const layoutScaleX = renderWidth ? target.rect.width / renderWidth : 1
635
+ const layoutScaleY = renderHeight ? target.rect.height / renderHeight : 1
636
+ const relX = (cursorX - target.rect.left) / layoutScaleX / pagesScale
637
+ const relY = (cursorY - target.rect.top) / layoutScaleY / pagesScale
638
+
639
+ const pageWidth = renderWidth / pagesScale
640
+ const pageHeight = renderHeight / pagesScale
641
+ this.previewScale.x = pagesScale
642
+ this.previewScale.y = pagesScale
643
+ let x = relX - this.previewElement.width / 2
644
+ let y = relY - this.previewElement.height / 2
645
+
646
+ x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width))
647
+ y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height))
648
+
649
+ this.previewPosition.x = x
650
+ this.previewPosition.y = y
651
+ this.previewVisible = true
652
+ })
527
653
  },
528
654
 
529
655
  handleKeyDown(event) {
@@ -551,10 +677,9 @@ export default {
551
677
 
552
678
  this.scale = newScale
553
679
 
554
- this.pdfDocuments.forEach((doc) => {
555
- doc.pagesScale = doc.pagesScale.map(() => this.scale)
556
- })
680
+ applyScaleToDocs(this.pdfDocuments, this.scale)
557
681
 
682
+ this._pageMeasurementCache = {}
558
683
  this.cachePageBounds()
559
684
  },
560
685
 
@@ -564,7 +689,7 @@ export default {
564
689
 
565
690
  const objectToAdd = {
566
691
  ...this.previewElement,
567
- id: `obj-${Date.now()}`,
692
+ id: this.generateObjectId(),
568
693
  x: Math.round(this.previewPosition.x),
569
694
  y: Math.round(this.previewPosition.y),
570
695
  }
@@ -601,6 +726,30 @@ export default {
601
726
  this.isAddingMode = false
602
727
  this.previewElement = null
603
728
  this.previewVisible = false
729
+ this.detachAddingListeners()
730
+ },
731
+ generateObjectId() {
732
+ const counter = this.nextObjectCounter++
733
+ const rand = Math.random().toString(36).slice(2, 8)
734
+ return `obj-${Date.now()}-${counter}-${rand}`
735
+ },
736
+ attachAddingListeners() {
737
+ if (this.addingListenersAttached) return
738
+ this.addingListenersAttached = true
739
+ document.addEventListener('mousemove', this.handleMouseMove)
740
+ document.addEventListener('touchmove', this.handleMouseMove, { passive: true })
741
+ document.addEventListener('mouseup', this.finishAdding)
742
+ document.addEventListener('touchend', this.finishAdding)
743
+ document.addEventListener('keydown', this.handleKeyDown)
744
+ },
745
+ detachAddingListeners() {
746
+ if (!this.addingListenersAttached) return
747
+ this.addingListenersAttached = false
748
+ document.removeEventListener('mousemove', this.handleMouseMove)
749
+ document.removeEventListener('touchmove', this.handleMouseMove, { passive: true })
750
+ document.removeEventListener('mouseup', this.finishAdding)
751
+ document.removeEventListener('touchend', this.finishAdding)
752
+ document.removeEventListener('keydown', this.handleKeyDown)
604
753
  },
605
754
 
606
755
  addObjectToPage(object, pageIndex = this.selectedPageIndex, docIndex = this.selectedDocIndex) {
@@ -611,20 +760,39 @@ export default {
611
760
  const pageRef = this.getPageComponent(docIndex, pageIndex)
612
761
  if (!pageRef) return false
613
762
 
763
+ let objectToAdd = object
764
+ if (!objectToAdd.id || this.objectIdExists(docIndex, objectToAdd.id)) {
765
+ objectToAdd = { ...objectToAdd, id: this.generateObjectId() }
766
+ }
767
+
614
768
  const pageWidth = this.getPageWidth(docIndex, pageIndex)
615
769
  const pageHeight = this.getPageHeight(docIndex, pageIndex)
616
770
 
617
- if (object.x < 0 || object.y < 0 ||
618
- object.x + object.width > pageWidth ||
619
- object.y + object.height > pageHeight) {
771
+ if (objectToAdd.x < 0 || objectToAdd.y < 0 ||
772
+ objectToAdd.x + objectToAdd.width > pageWidth ||
773
+ objectToAdd.y + objectToAdd.height > pageHeight) {
620
774
  return false
621
775
  }
622
776
 
623
- doc.allObjects = doc.allObjects.map((objects, pIndex) =>
624
- pIndex === pageIndex ? [...objects, object] : objects,
625
- )
777
+ doc.allObjects[pageIndex].push(objectToAdd)
778
+ this.objectIndexCache[`${docIndex}-${objectToAdd.id}`] = pageIndex
626
779
  return true
627
780
  },
781
+ objectIdExists(docIndex, objectId) {
782
+ if (!objectId) return false
783
+ const cacheKey = `${docIndex}-${objectId}`
784
+ if (this.objectIndexCache[cacheKey] !== undefined) return true
785
+ const doc = this.pdfDocuments[docIndex]
786
+ return objectIdExistsInDoc(doc, objectId)
787
+ },
788
+ updateObjectInPage(docIndex, pageIndex, objectId, payload) {
789
+ const doc = this.pdfDocuments[docIndex]
790
+ updateObjectInDoc(doc, pageIndex, objectId, payload)
791
+ },
792
+ removeObjectFromPage(docIndex, pageIndex, objectId) {
793
+ const doc = this.pdfDocuments[docIndex]
794
+ removeObjectFromDoc(doc, pageIndex, objectId)
795
+ },
628
796
 
629
797
  getAllObjects(docIndex = this.selectedDocIndex) {
630
798
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return []
@@ -635,9 +803,9 @@ export default {
635
803
  doc.allObjects.forEach((pageObjects, pageIndex) => {
636
804
  const pageRef = this.getPageComponent(docIndex, pageIndex)
637
805
  if (!pageRef) return
638
- const measurement = pageRef.getCanvasMeasurement()
806
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
807
+ const normalizedCanvasHeight = measurement.height
639
808
  const pagesScale = doc.pagesScale[pageIndex] || 1
640
- const normalizedCanvasHeight = measurement.canvasHeight / pagesScale
641
809
 
642
810
  pageObjects.forEach(object => {
643
811
  result.push({
@@ -667,12 +835,10 @@ export default {
667
835
  let currentPageIndex = this.objectIndexCache[cacheKey]
668
836
 
669
837
  if (currentPageIndex === undefined) {
670
- doc.allObjects.forEach((objects, pIndex) => {
671
- if (objects.find(o => o.id === objectId)) {
672
- currentPageIndex = pIndex
673
- this.objectIndexCache[cacheKey] = pIndex
674
- }
675
- })
838
+ currentPageIndex = findObjectPageIndex(doc, objectId)
839
+ if (currentPageIndex !== undefined) {
840
+ this.objectIndexCache[cacheKey] = currentPageIndex
841
+ }
676
842
  }
677
843
 
678
844
  if (currentPageIndex === undefined) return
@@ -684,19 +850,18 @@ export default {
684
850
  const mouseX = payload._mouseX
685
851
  const mouseY = payload._mouseY
686
852
 
687
- if (!this.pagesBoundingRects || Object.keys(this.pagesBoundingRects).length === 0) {
853
+ const pageBoundsMap = this.getPageBoundsMap()
854
+ if (!pageBoundsMap || Object.keys(pageBoundsMap).length === 0) {
688
855
  this.cachePageBounds()
689
856
  }
690
857
 
691
- const currentPageRect = this.pagesBoundingRects[`${docIndex}-${currentPageIndex}`]?.rect
858
+ const currentPageRect = this.getPageRect(docIndex, currentPageIndex)
692
859
  if (currentPageRect) {
693
860
  const pagesScale = this.getDisplayedPageScale(docIndex, currentPageIndex)
694
861
  const relX = (mouseX - currentPageRect.left - this.draggingElementShift.x) / pagesScale - (this.draggingInitialMouseOffset.x / pagesScale)
695
862
  const relY = (mouseY - currentPageRect.top - this.draggingElementShift.y) / pagesScale - (this.draggingInitialMouseOffset.y / pagesScale)
696
863
 
697
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].map(obj =>
698
- obj.id === objectId ? { ...obj, x: relX, y: relY } : obj
699
- )
864
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, { x: relX, y: relY })
700
865
  }
701
866
  return
702
867
  }
@@ -707,6 +872,14 @@ export default {
707
872
  const objWidth = payload.width !== undefined ? payload.width : targetObject.width
708
873
  const objHeight = payload.height !== undefined ? payload.height : targetObject.height
709
874
 
875
+ const { width: currentPageWidth, height: currentPageHeight } = this.getPageSize(docIndex, currentPageIndex)
876
+ if (newX >= 0 && newY >= 0 &&
877
+ newX + objWidth <= currentPageWidth &&
878
+ newY + objHeight <= currentPageHeight) {
879
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, payload)
880
+ return
881
+ }
882
+
710
883
  let bestPageIndex = currentPageIndex
711
884
  let maxVisibleArea = 0
712
885
 
@@ -714,30 +887,18 @@ export default {
714
887
  const pageWidth = this.getPageWidth(docIndex, pIndex)
715
888
  const pageHeight = this.getPageHeight(docIndex, pIndex)
716
889
 
717
- const visibleLeft = Math.max(0, newX)
718
- const visibleTop = Math.max(0, newY)
719
- const visibleRight = Math.min(pageWidth, newX + objWidth)
720
- const visibleBottom = Math.min(pageHeight, newY + objHeight)
721
-
722
- if (visibleRight > visibleLeft && visibleBottom > visibleTop) {
723
- const visibleArea = (visibleRight - visibleLeft) * (visibleBottom - visibleTop)
724
- if (visibleArea > maxVisibleArea) {
725
- maxVisibleArea = visibleArea
726
- bestPageIndex = pIndex
727
- }
890
+ const visibleArea = getVisibleArea(newX, newY, objWidth, objHeight, pageWidth, pageHeight)
891
+ if (visibleArea > maxVisibleArea) {
892
+ maxVisibleArea = visibleArea
893
+ bestPageIndex = pIndex
728
894
  }
729
895
  }
730
896
 
731
897
  if (bestPageIndex !== currentPageIndex) {
732
- const pageWidth = this.getPageWidth(docIndex, bestPageIndex)
733
- const pageHeight = this.getPageHeight(docIndex, bestPageIndex)
734
-
735
- const adjustedX = Math.max(0, Math.min(newX, pageWidth - objWidth))
736
- const adjustedY = Math.max(0, Math.min(newY, pageHeight - objHeight))
898
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, bestPageIndex)
899
+ const { x: adjustedX, y: adjustedY } = clampPosition(newX, newY, objWidth, objHeight, pageWidth, pageHeight)
737
900
 
738
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].filter(
739
- obj => obj.id !== objectId
740
- )
901
+ this.removeObjectFromPage(docIndex, currentPageIndex, objectId)
741
902
  const updatedObject = {
742
903
  ...targetObject,
743
904
  ...payload,
@@ -749,8 +910,7 @@ export default {
749
910
  return
750
911
  }
751
912
 
752
- const pageWidth = this.getPageWidth(docIndex, currentPageIndex)
753
- const pageHeight = this.getPageHeight(docIndex, currentPageIndex)
913
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, currentPageIndex)
754
914
 
755
915
  if (newX < 0 || newY < 0 ||
756
916
  newX + objWidth > pageWidth ||
@@ -759,9 +919,7 @@ export default {
759
919
  }
760
920
  }
761
921
 
762
- doc.allObjects = doc.allObjects.map(objects =>
763
- objects.map(object => (object.id === objectId ? { ...object, ...payload } : object)),
764
- )
922
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, payload)
765
923
  },
766
924
 
767
925
  deleteObject(docIndex, objectId) {
@@ -770,16 +928,16 @@ export default {
770
928
  let deletedObject = null
771
929
  let deletedPageIndex = -1
772
930
 
773
- doc.allObjects = doc.allObjects.map((objects, pageIndex) =>
774
- objects.filter(object => {
775
- if (object.id === objectId) {
776
- deletedObject = object
777
- deletedPageIndex = pageIndex
778
- return false
779
- }
780
- return true
781
- }),
782
- )
931
+ doc.allObjects.some((objects, pageIndex) => {
932
+ const objectIndex = objects.findIndex(object => object.id === objectId)
933
+ if (objectIndex === -1) {
934
+ return false
935
+ }
936
+ deletedObject = objects[objectIndex]
937
+ deletedPageIndex = pageIndex
938
+ objects.splice(objectIndex, 1)
939
+ return true
940
+ })
783
941
  delete this.objectIndexCache[`${docIndex}-${objectId}`]
784
942
  if (deletedObject) {
785
943
  this.$emit('pdf-elements:delete-object', {
@@ -789,6 +947,64 @@ export default {
789
947
  })
790
948
  }
791
949
  },
950
+ duplicateObject(docIndex, objectId) {
951
+ if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
952
+ const doc = this.pdfDocuments[docIndex]
953
+
954
+ const cacheKey = `${docIndex}-${objectId}`
955
+ let pageIndex = this.objectIndexCache[cacheKey]
956
+
957
+ if (pageIndex === undefined) {
958
+ pageIndex = findObjectPageIndex(doc, objectId)
959
+ if (pageIndex !== undefined) {
960
+ this.objectIndexCache[cacheKey] = pageIndex
961
+ }
962
+ }
963
+
964
+ if (pageIndex === undefined) return
965
+
966
+ const sourceObject = doc.allObjects[pageIndex]?.find(o => o.id === objectId)
967
+ if (!sourceObject) return
968
+
969
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, pageIndex)
970
+ const offset = 12
971
+ const { x, y } = clampPosition(
972
+ sourceObject.x + offset,
973
+ sourceObject.y + offset,
974
+ sourceObject.width,
975
+ sourceObject.height,
976
+ pageWidth,
977
+ pageHeight,
978
+ )
979
+
980
+ let duplicatedSigner = sourceObject.signer
981
+ if (duplicatedSigner?.element && Object.prototype.hasOwnProperty.call(duplicatedSigner.element, 'elementId')) {
982
+ duplicatedSigner = {
983
+ ...duplicatedSigner,
984
+ element: { ...duplicatedSigner.element },
985
+ }
986
+ delete duplicatedSigner.element.elementId
987
+ }
988
+
989
+ const duplicatedObject = {
990
+ ...sourceObject,
991
+ id: this.generateObjectId(),
992
+ x,
993
+ y,
994
+ signer: duplicatedSigner,
995
+ }
996
+
997
+ doc.allObjects[pageIndex].push(duplicatedObject)
998
+ this.objectIndexCache[`${docIndex}-${duplicatedObject.id}`] = pageIndex
999
+
1000
+ this.$nextTick(() => {
1001
+ const refKey = `draggable${docIndex}-${pageIndex}-${duplicatedObject.id}`
1002
+ const draggableRefs = this.$refs[refKey]
1003
+ if (draggableRefs && Array.isArray(draggableRefs) && draggableRefs[0]) {
1004
+ draggableRefs[0].isSelected = true
1005
+ }
1006
+ })
1007
+ },
792
1008
 
793
1009
  checkAndMoveObjectPage(docIndex, objectId, mouseX, mouseY) {
794
1010
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return undefined
@@ -798,12 +1014,10 @@ export default {
798
1014
  let currentPageIndex = this.objectIndexCache[cacheKey]
799
1015
 
800
1016
  if (currentPageIndex === undefined) {
801
- doc.allObjects.forEach((objects, pIndex) => {
802
- if (objects.find(o => o.id === objectId)) {
803
- currentPageIndex = pIndex
804
- this.objectIndexCache[cacheKey] = pIndex
805
- }
806
- })
1017
+ currentPageIndex = findObjectPageIndex(doc, objectId)
1018
+ if (currentPageIndex !== undefined) {
1019
+ this.objectIndexCache[cacheKey] = currentPageIndex
1020
+ }
807
1021
  }
808
1022
 
809
1023
  if (currentPageIndex === undefined) return undefined
@@ -812,8 +1026,9 @@ export default {
812
1026
  if (!targetObject) return currentPageIndex
813
1027
 
814
1028
  let targetPageIndex = currentPageIndex
815
- for (const key in this.pagesBoundingRects) {
816
- const { docIndex: rectDocIndex, pageIndex, rect } = this.pagesBoundingRects[key]
1029
+ const pageBoundsMap = this.getPageBoundsMap()
1030
+ for (const key in pageBoundsMap) {
1031
+ const { docIndex: rectDocIndex, pageIndex, rect } = pageBoundsMap[key]
817
1032
  if (rectDocIndex === docIndex &&
818
1033
  mouseX >= rect.left && mouseX <= rect.right &&
819
1034
  mouseY >= rect.top && mouseY <= rect.bottom) {
@@ -822,23 +1037,25 @@ export default {
822
1037
  }
823
1038
  }
824
1039
 
825
- const targetPageRect = this.pagesBoundingRects[`${docIndex}-${targetPageIndex}`]?.rect
1040
+ const targetPageRect = this.getPageRect(docIndex, targetPageIndex)
826
1041
  if (!targetPageRect) return currentPageIndex
827
1042
 
828
1043
  const pagesScale = this.getDisplayedPageScale(docIndex, targetPageIndex)
829
1044
  const relX = (mouseX - targetPageRect.left - this.draggingElementShift.x) / pagesScale - (this.draggingInitialMouseOffset.x / pagesScale)
830
1045
  const relY = (mouseY - targetPageRect.top - this.draggingElementShift.y) / pagesScale - (this.draggingInitialMouseOffset.y / pagesScale)
831
1046
 
832
- const pageWidth = this.getPageWidth(docIndex, targetPageIndex)
833
- const pageHeight = this.getPageHeight(docIndex, targetPageIndex)
834
-
835
- const clampedX = Math.max(0, Math.min(relX, pageWidth - targetObject.width))
836
- const clampedY = Math.max(0, Math.min(relY, pageHeight - targetObject.height))
1047
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, targetPageIndex)
1048
+ const { x: clampedX, y: clampedY } = clampPosition(
1049
+ relX,
1050
+ relY,
1051
+ targetObject.width,
1052
+ targetObject.height,
1053
+ pageWidth,
1054
+ pageHeight,
1055
+ )
837
1056
 
838
1057
  if (targetPageIndex !== currentPageIndex) {
839
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].filter(
840
- obj => obj.id !== objectId
841
- )
1058
+ this.removeObjectFromPage(docIndex, currentPageIndex, objectId)
842
1059
  doc.allObjects[targetPageIndex].push({
843
1060
  ...targetObject,
844
1061
  x: clampedX,
@@ -846,9 +1063,7 @@ export default {
846
1063
  })
847
1064
  this.objectIndexCache[cacheKey] = targetPageIndex
848
1065
  } else if (clampedX !== targetObject.x || clampedY !== targetObject.y) {
849
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].map(obj =>
850
- obj.id === objectId ? { ...obj, x: clampedX, y: clampedY } : obj
851
- )
1066
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, { x: clampedX, y: clampedY })
852
1067
  }
853
1068
 
854
1069
  return targetPageIndex
@@ -856,8 +1071,9 @@ export default {
856
1071
 
857
1072
  onMeasure(e, docIndex, pageIndex) {
858
1073
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
859
- this.pdfDocuments[docIndex].pagesScale[pageIndex] = e.scale
860
- this.cachePageBounds()
1074
+ this.pdfDocuments[docIndex].pagesScale.splice(pageIndex, 1, e.scale)
1075
+ this._pageMeasurementCache[`${docIndex}-${pageIndex}`] = null
1076
+ this.cachePageBoundsForPage(docIndex, pageIndex)
861
1077
  if (this.autoFitZoom) {
862
1078
  this.scheduleAutoFitZoom()
863
1079
  }
@@ -871,16 +1087,26 @@ export default {
871
1087
  getPageWidth(docIndex, pageIndex) {
872
1088
  const pageRef = this.getPageComponent(docIndex, pageIndex)
873
1089
  if (!pageRef) return 0
874
- const doc = this.pdfDocuments[docIndex]
875
- const pagesScale = doc.pagesScale[pageIndex] || 1
876
- return pageRef.getCanvasMeasurement().canvasWidth / pagesScale
1090
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
1091
+ return measurement.width
877
1092
  },
878
1093
  getPageHeight(docIndex, pageIndex) {
879
1094
  const pageRef = this.getPageComponent(docIndex, pageIndex)
880
1095
  if (!pageRef) return 0
1096
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
1097
+ return measurement.height
1098
+ },
1099
+ getPageSize(docIndex, pageIndex) {
1100
+ return {
1101
+ width: this.getPageWidth(docIndex, pageIndex),
1102
+ height: this.getPageHeight(docIndex, pageIndex),
1103
+ }
1104
+ },
1105
+ getCachedMeasurement(docIndex, pageIndex, pageRef) {
1106
+ const cacheKey = `${docIndex}-${pageIndex}`
881
1107
  const doc = this.pdfDocuments[docIndex]
882
1108
  const pagesScale = doc.pagesScale[pageIndex] || 1
883
- return pageRef.getCanvasMeasurement().canvasHeight / pagesScale
1109
+ return getCachedMeasurement(this._pageMeasurementCache, cacheKey, pageRef, pagesScale)
884
1110
  },
885
1111
  calculateOptimalScale(maxPageWidth) {
886
1112
  const containerWidth = this.$el?.clientWidth || 0
@@ -924,9 +1150,8 @@ export default {
924
1150
  if (Math.abs(optimalScale - this.scale) > 0.01) {
925
1151
  this.scale = optimalScale
926
1152
  this.visualScale = optimalScale
927
- this.pdfDocuments.forEach((doc) => {
928
- doc.pagesScale = doc.pagesScale.map(() => this.scale)
929
- })
1153
+ applyScaleToDocs(this.pdfDocuments, this.scale)
1154
+ this._pageMeasurementCache = {}
930
1155
  this.cachePageBounds()
931
1156
  }
932
1157
  },
@@ -968,15 +1193,6 @@ export default {
968
1193
  opacity: 0.7;
969
1194
  pointer-events: none;
970
1195
  }
971
- .drag-ghost {
972
- position: fixed;
973
- opacity: 0.9;
974
- pointer-events: none;
975
- z-index: 10000;
976
- transform-origin: top left;
977
- transition: none;
978
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
979
- }
980
1196
  .overlay {
981
1197
  position: absolute;
982
1198
  top: 0;