@libresign/pdf-elements 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later
8
8
  <div
9
9
  v-if="pdfDocuments.length"
10
10
  class="pages-container"
11
- :style="{ transform: `scale(${visualScale / scale})`, transformOrigin: 'top center' }"
12
11
  >
13
12
  <div v-for="(pdfDoc, docIndex) in pdfDocuments" :key="docIndex">
14
13
  <div v-for="(page, pIndex) in pdfDoc.pages" :key="`${docIndex}-${pIndex}`" class="page-slot">
@@ -24,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
24
23
  />
25
24
  <div
26
25
  class="overlay"
26
+ @mousemove="handleMouseMove"
27
+ @touchmove="handleMouseMove"
27
28
  >
28
29
  <div
29
30
  v-if="isAddingMode && previewPageDocIndex === docIndex && previewPageIndex === pIndex && previewElement && previewVisible"
@@ -36,10 +37,16 @@ SPDX-License-Identifier: AGPL-3.0-or-later
36
37
  }"
37
38
  >
38
39
  <slot
39
- name="custom"
40
+ :name="previewElement.type ? `element-${previewElement.type}` : 'custom'"
40
41
  :object="previewElement"
41
42
  :isSelected="false"
42
- />
43
+ >
44
+ <slot
45
+ name="custom"
46
+ :object="previewElement"
47
+ :isSelected="false"
48
+ />
49
+ </slot>
43
50
  </div>
44
51
 
45
52
  <DraggableElement
@@ -47,9 +54,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
47
54
  :key="object.id"
48
55
  :ref="`draggable${docIndex}-${pIndex}-${object.id}`"
49
56
  :object="object"
50
- :pages-scale="getRenderPageScale(docIndex, pIndex)"
57
+ :pages-scale="getDisplayedPageScale(docIndex, pIndex)"
51
58
  :page-width="getPageWidth(docIndex, pIndex)"
52
59
  :page-height="getPageHeight(docIndex, pIndex)"
60
+ :read-only="readOnly"
53
61
  :on-update="(payload) => updateObject(docIndex, object.id, payload)"
54
62
  :on-delete="() => deleteObject(docIndex, object.id)"
55
63
  :on-drag-start="(mouseX, mouseY, pointerOffset, dragShift) => startDraggingElement(docIndex, pIndex, object, mouseX, mouseY, pointerOffset, dragShift)"
@@ -127,10 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later
127
135
  </template>
128
136
 
129
137
  <script>
130
- import debounce from 'debounce'
131
138
  import PDFPage from './PDFPage.vue'
132
139
  import DraggableElement from './DraggableElement.vue'
133
140
  import { readAsPDF, readAsArrayBuffer } from '../utils/asyncReader.js'
141
+ import { clampPosition, getVisibleArea } from '../utils/geometry.js'
142
+ import { getViewportWindow, isPageInViewport } from '../utils/pageBounds.js'
143
+ import { applyScaleToDocs } from '../utils/zoom.js'
144
+ import { objectIdExistsInDoc, findObjectPageIndex, updateObjectInDoc, removeObjectFromDoc } from '../utils/objectStore.js'
145
+ import { getCachedMeasurement } from '../utils/measurements.js'
134
146
 
135
147
  export default {
136
148
  name: 'PDFElements',
@@ -175,6 +187,10 @@ export default {
175
187
  type: Boolean,
176
188
  default: true,
177
189
  },
190
+ readOnly: {
191
+ type: Boolean,
192
+ default: false,
193
+ },
178
194
  pageCountFormat: {
179
195
  type: String,
180
196
  default: '{currentPage} of {totalPages}',
@@ -197,7 +213,16 @@ export default {
197
213
  previewPageDocIndex: -1,
198
214
  previewPageIndex: -1,
199
215
  previewVisible: false,
200
- pagesBoundingRects: {},
216
+ hoverRafId: 0,
217
+ pendingHoverClientPos: null,
218
+ lastHoverRect: null,
219
+ addingListenersAttached: false,
220
+ dragRafId: 0,
221
+ pendingDragClientPos: null,
222
+ pageBoundsVersion: 0,
223
+ lastScrollTop: 0,
224
+ lastClientWidth: 0,
225
+ nextObjectCounter: 0,
201
226
  isDraggingElement: false,
202
227
  draggingObject: null,
203
228
  draggingDocIndex: -1,
@@ -210,21 +235,23 @@ export default {
210
235
  viewportRafId: 0,
211
236
  objectIndexCache: {},
212
237
  zoomRafId: null,
238
+ wheelZoomRafId: null,
213
239
  boundHandleWheel: null,
214
- debouncedApplyZoom: null,
215
240
  visualScale: this.initialScale,
216
241
  autoFitApplied: false,
242
+ lastContainerWidth: 0,
217
243
  }
218
244
  },
245
+ created() {
246
+ this._pagesBoundingRects = {}
247
+ this._pagesBoundingRectsList = []
248
+ this._pageMeasurementCache = {}
249
+ this._lastPageBoundsScrollTop = 0
250
+ this._lastPageBoundsClientHeight = 0
251
+ },
219
252
  mounted() {
220
253
  this.boundHandleWheel = this.handleWheel.bind(this)
221
- this.debouncedApplyZoom = debounce(this.commitZoom, 150)
222
254
  this.init()
223
- document.addEventListener('mousemove', this.handleMouseMove)
224
- document.addEventListener('touchmove', this.handleMouseMove, { passive: true })
225
- document.addEventListener('mouseup', this.finishAdding)
226
- document.addEventListener('touchend', this.finishAdding)
227
- document.addEventListener('keydown', this.handleKeyDown)
228
255
  window.addEventListener('scroll', this.onViewportScroll, { passive: true })
229
256
  window.addEventListener('resize', this.onViewportScroll)
230
257
  this.$el?.addEventListener('scroll', this.onViewportScroll, { passive: true })
@@ -234,15 +261,14 @@ export default {
234
261
  if (this.zoomRafId) {
235
262
  cancelAnimationFrame(this.zoomRafId)
236
263
  }
237
- this.debouncedApplyZoom?.clear?.()
264
+ if (this.wheelZoomRafId) {
265
+ cancelAnimationFrame(this.wheelZoomRafId)
266
+ this.wheelZoomRafId = null
267
+ }
238
268
  if (this.boundHandleWheel) {
239
269
  this.$el?.removeEventListener('wheel', this.boundHandleWheel)
240
270
  }
241
- document.removeEventListener('mousemove', this.handleMouseMove)
242
- document.removeEventListener('touchmove', this.handleMouseMove)
243
- document.removeEventListener('mouseup', this.finishAdding)
244
- document.removeEventListener('touchend', this.finishAdding)
245
- document.removeEventListener('keydown', this.handleKeyDown)
271
+ this.detachAddingListeners()
246
272
  window.removeEventListener('scroll', this.onViewportScroll)
247
273
  window.removeEventListener('resize', this.onViewportScroll)
248
274
  this.$el?.removeEventListener('scroll', this.onViewportScroll)
@@ -250,6 +276,14 @@ export default {
250
276
  window.cancelAnimationFrame(this.viewportRafId)
251
277
  this.viewportRafId = 0
252
278
  }
279
+ if (this.hoverRafId) {
280
+ window.cancelAnimationFrame(this.hoverRafId)
281
+ this.hoverRafId = 0
282
+ }
283
+ if (this.dragRafId) {
284
+ window.cancelAnimationFrame(this.dragRafId)
285
+ this.dragRafId = 0
286
+ }
253
287
  },
254
288
  methods: {
255
289
  async init() {
@@ -274,7 +308,7 @@ export default {
274
308
  for (let p = 1; p <= pdfDoc.numPages; p++) {
275
309
  const pagePromise = pdfDoc.getPage(p)
276
310
  pagePromise.then((page) => {
277
- pageWidths[p - 1] = page.getViewport({ scale: 1 }).width
311
+ pageWidths.splice(p - 1, 1, page.getViewport({ scale: 1 }).width)
278
312
  if (this.autoFitZoom) {
279
313
  this.scheduleAutoFitZoom()
280
314
  }
@@ -295,6 +329,7 @@ export default {
295
329
  }
296
330
 
297
331
  this.pdfDocuments = docs
332
+ this._pageMeasurementCache = {}
298
333
  if (docs.length) {
299
334
  this.selectedDocIndex = 0
300
335
  this.selectedPageIndex = 0
@@ -324,11 +359,8 @@ export default {
324
359
  ? dragShift
325
360
  : { x: 0, y: 0 }
326
361
 
327
- const pageKey = `${docIndex}-${pageIndex}`
328
- if (!this.pagesBoundingRects[pageKey]) {
329
- this.cachePageBounds()
330
- }
331
- const pageRect = this.pagesBoundingRects[pageKey]?.rect
362
+ this.cachePageBounds()
363
+ const pageRect = this.getPageRect(docIndex, pageIndex)
332
364
  if (pointerOffset && typeof pointerOffset.x === 'number' && typeof pointerOffset.y === 'number') {
333
365
  this.draggingInitialMouseOffset.x = pointerOffset.x
334
366
  this.draggingInitialMouseOffset.y = pointerOffset.y
@@ -344,17 +376,22 @@ export default {
344
376
  this.draggingClientPosition.y = mouseY - this.draggingInitialMouseOffset.y
345
377
  }
346
378
 
347
- this.cachePageBounds()
348
379
  },
349
380
 
350
381
  updateDraggingPosition(clientX, clientY) {
351
382
  if (!this.isDraggingElement) return
352
383
 
353
- this.lastMouseClientPos.x = clientX
354
- this.lastMouseClientPos.y = clientY
355
-
356
- this.draggingClientPosition.x = clientX - this.draggingInitialMouseOffset.x
357
- this.draggingClientPosition.y = clientY - this.draggingInitialMouseOffset.y
384
+ this.pendingDragClientPos = { x: clientX, y: clientY }
385
+ if (this.dragRafId) return
386
+ this.dragRafId = window.requestAnimationFrame(() => {
387
+ this.dragRafId = 0
388
+ const pending = this.pendingDragClientPos
389
+ if (!pending) return
390
+ this.lastMouseClientPos.x = pending.x
391
+ this.lastMouseClientPos.y = pending.y
392
+ this.draggingClientPosition.x = pending.x - this.draggingInitialMouseOffset.x
393
+ this.draggingClientPosition.y = pending.y - this.draggingInitialMouseOffset.y
394
+ })
358
395
  },
359
396
 
360
397
  stopDraggingElement() {
@@ -386,10 +423,12 @@ export default {
386
423
  this.draggingDocIndex = -1
387
424
  this.draggingPageIndex = -1
388
425
  this.draggingElementShift = { x: 0, y: 0 }
426
+ this.pendingDragClientPos = null
389
427
  },
390
428
 
391
429
  startAddingElement(templateObject) {
392
430
  if (!this.pdfDocuments.length) return
431
+ this.attachAddingListeners()
393
432
  this.isAddingMode = true
394
433
  this.previewElement = { ...templateObject }
395
434
  this.previewPageDocIndex = 0
@@ -400,33 +439,103 @@ export default {
400
439
  },
401
440
 
402
441
  cachePageBounds() {
403
- this.pagesBoundingRects = {}
442
+ const nextRects = {}
443
+ const container = this.$el
444
+ const scrollTop = container?.scrollTop || 0
445
+ const viewHeight = container?.clientHeight || 0
446
+ if (!this.isAddingMode && !this.isDraggingElement &&
447
+ scrollTop === this._lastPageBoundsScrollTop &&
448
+ viewHeight === this._lastPageBoundsClientHeight) {
449
+ return
450
+ }
451
+ this._lastPageBoundsScrollTop = scrollTop
452
+ this._lastPageBoundsClientHeight = viewHeight
453
+ const { minY, maxY } = getViewportWindow(scrollTop, viewHeight)
404
454
  for (let docIdx = 0; docIdx < this.pdfDocuments.length; docIdx++) {
405
455
  for (let pageIdx = 0; pageIdx < this.pdfDocuments[docIdx].pages.length; pageIdx++) {
406
456
  const canvas = this.getPageCanvasElement(docIdx, pageIdx)
407
457
  if (!canvas) continue
458
+ if (viewHeight) {
459
+ const wrapper = canvas.closest('.page-wrapper') || canvas
460
+ const offsetTop = wrapper.offsetTop || 0
461
+ const offsetHeight = wrapper.offsetHeight || 0
462
+ if (!isPageInViewport(offsetTop, offsetHeight, minY, maxY)) {
463
+ continue
464
+ }
465
+ }
408
466
  const rect = canvas.getBoundingClientRect()
409
- this.pagesBoundingRects[`${docIdx}-${pageIdx}`] = {
467
+ nextRects[`${docIdx}-${pageIdx}`] = {
410
468
  docIndex: docIdx,
411
469
  pageIndex: pageIdx,
412
470
  rect,
413
471
  }
414
472
  }
415
473
  }
474
+ this._pagesBoundingRects = nextRects
475
+ this._pagesBoundingRectsList = Object.values(nextRects)
476
+ this.pageBoundsVersion++
477
+ },
478
+ cachePageBoundsForPage(docIndex, pageIndex) {
479
+ const canvas = this.getPageCanvasElement(docIndex, pageIndex)
480
+ if (!canvas) return
481
+ const rect = canvas.getBoundingClientRect()
482
+ this._pagesBoundingRects = {
483
+ ...this._pagesBoundingRects,
484
+ [`${docIndex}-${pageIndex}`]: {
485
+ docIndex,
486
+ pageIndex,
487
+ rect,
488
+ },
489
+ }
490
+ this._pagesBoundingRectsList = Object.values(this._pagesBoundingRects)
491
+ this.pageBoundsVersion++
492
+ },
493
+ getPageBoundsMap() {
494
+ return this._pagesBoundingRects || {}
495
+ },
496
+ getPageBoundsList() {
497
+ return this._pagesBoundingRectsList || []
498
+ },
499
+ getPageRect(docIndex, pageIndex) {
500
+ return this.getPageBoundsMap()[`${docIndex}-${pageIndex}`]?.rect || null
501
+ },
502
+ getPointerPosition(event) {
503
+ if (event?.type?.includes?.('touch')) {
504
+ return {
505
+ x: event.touches?.[0]?.clientX,
506
+ y: event.touches?.[0]?.clientY,
507
+ }
508
+ }
509
+ return {
510
+ x: event?.clientX,
511
+ y: event?.clientY,
512
+ }
416
513
  },
417
514
 
418
515
  getDisplayedPageScale(docIndex, pageIndex) {
516
+ this.pageBoundsVersion
419
517
  const doc = this.pdfDocuments[docIndex]
420
518
  if (!doc) return 1
519
+ const baseWidth = doc.pageWidths?.[pageIndex] || 0
520
+ const pageBoundsMap = this.getPageBoundsMap()
521
+ if (!pageBoundsMap[`${docIndex}-${pageIndex}`]) {
522
+ this.cachePageBoundsForPage(docIndex, pageIndex)
523
+ }
524
+ const rectWidth = this.getPageBoundsMap()[`${docIndex}-${pageIndex}`]?.rect?.width || 0
525
+ if (rectWidth && baseWidth) {
526
+ return rectWidth / baseWidth
527
+ }
528
+ if (this.isAddingMode || this.isDraggingElement) {
529
+ const canvas = this.getPageCanvasElement(docIndex, pageIndex)
530
+ const fallbackRectWidth = canvas?.getBoundingClientRect?.().width || 0
531
+ if (fallbackRectWidth && baseWidth) {
532
+ return fallbackRectWidth / baseWidth
533
+ }
534
+ }
421
535
  const base = doc.pagesScale[pageIndex] || 1
422
536
  const factor = this.visualScale && this.scale ? (this.visualScale / this.scale) : 1
423
537
  return base * factor
424
538
  },
425
- getRenderPageScale(docIndex, pageIndex) {
426
- const doc = this.pdfDocuments[docIndex]
427
- if (!doc) return 1
428
- return doc.pagesScale[pageIndex] || 1
429
- },
430
539
  getPageComponent(docIndex, pageIndex) {
431
540
  const pageRef = this.$refs[`page${docIndex}-${pageIndex}`]
432
541
  return pageRef && Array.isArray(pageRef) && pageRef[0] ? pageRef[0] : null
@@ -439,8 +548,25 @@ export default {
439
548
  onViewportScroll() {
440
549
  if (this.viewportRafId) return
441
550
  this.viewportRafId = window.requestAnimationFrame(() => {
551
+ const container = this.$el
552
+ const scrollTop = container?.scrollTop || 0
553
+ const clientWidth = container?.clientWidth || 0
554
+ const scrollChanged = scrollTop !== this.lastScrollTop
555
+ const widthChanged = clientWidth !== this.lastClientWidth
556
+ this.lastScrollTop = scrollTop
557
+ this.lastClientWidth = clientWidth
558
+
442
559
  if (this.isAddingMode || this.isDraggingElement) {
443
- this.cachePageBounds()
560
+ if (scrollChanged || widthChanged) {
561
+ this.cachePageBounds()
562
+ }
563
+ }
564
+ if (this.autoFitZoom && !this.isAddingMode && !this.isDraggingElement) {
565
+ if (clientWidth && widthChanged) {
566
+ this.lastContainerWidth = clientWidth
567
+ this.autoFitApplied = false
568
+ this.scheduleAutoFitZoom()
569
+ }
444
570
  }
445
571
  this.viewportRafId = 0
446
572
  })
@@ -448,49 +574,75 @@ export default {
448
574
 
449
575
  handleMouseMove(event) {
450
576
  if (!this.isAddingMode || !this.previewElement) return
577
+ const { x, y } = this.getPointerPosition(event)
578
+ if (x === undefined || y === undefined) return
579
+ this.pendingHoverClientPos = { x, y }
580
+ if (this.hoverRafId) return
581
+ this.hoverRafId = window.requestAnimationFrame(() => {
582
+ this.hoverRafId = 0
583
+ const pending = this.pendingHoverClientPos
584
+ if (!pending) return
585
+
586
+ const cursorX = pending.x
587
+ const cursorY = pending.y
588
+ let target = null
589
+
590
+ if (this.lastHoverRect &&
591
+ cursorX >= this.lastHoverRect.left && cursorX <= this.lastHoverRect.right &&
592
+ cursorY >= this.lastHoverRect.top && cursorY <= this.lastHoverRect.bottom) {
593
+ target = {
594
+ docIndex: this.previewPageDocIndex,
595
+ pageIndex: this.previewPageIndex,
596
+ rect: this.lastHoverRect,
597
+ }
598
+ } else {
599
+ const rects = this.getPageBoundsList().length
600
+ ? this.getPageBoundsList()
601
+ : Object.values(this.getPageBoundsMap())
602
+ for (let i = 0; i < rects.length; i++) {
603
+ const entry = rects[i]
604
+ const rect = entry.rect
605
+ if (cursorX >= rect.left && cursorX <= rect.right &&
606
+ cursorY >= rect.top && cursorY <= rect.bottom) {
607
+ target = entry
608
+ break
609
+ }
610
+ }
611
+ }
451
612
 
452
- const clientX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX
453
- const clientY = event.type.includes('touch') ? event.touches[0].clientY : event.clientY
454
-
455
- let foundPage = false
456
- for (const key in this.pagesBoundingRects) {
457
- const { docIndex, pageIndex, rect } = this.pagesBoundingRects[key]
458
- if (clientX >= rect.left && clientX <= rect.right &&
459
- clientY >= rect.top && clientY <= rect.bottom) {
460
- this.previewPageDocIndex = docIndex
461
- this.previewPageIndex = pageIndex
462
- foundPage = true
463
-
464
- const canvasEl = this.getPageCanvasElement(docIndex, pageIndex)
465
- const pagesScale = this.pdfDocuments[docIndex]?.pagesScale?.[pageIndex] || 1
466
- const layoutWidth = canvasEl?.offsetWidth || rect.width
467
- const layoutHeight = canvasEl?.offsetHeight || rect.height
468
- const layoutScaleX = layoutWidth ? rect.width / layoutWidth : 1
469
- const layoutScaleY = layoutHeight ? rect.height / layoutHeight : 1
470
- const relX = (clientX - rect.left) / layoutScaleX / pagesScale
471
- const relY = (clientY - rect.top) / layoutScaleY / pagesScale
472
-
473
- const pageWidth = layoutWidth / pagesScale
474
- const pageHeight = layoutHeight / pagesScale
475
- this.previewScale.x = pagesScale * layoutScaleX
476
- this.previewScale.y = pagesScale * layoutScaleY
477
- let x = relX - this.previewElement.width / 2
478
- let y = relY - this.previewElement.height / 2
479
-
480
- x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width))
481
- y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height))
482
-
483
- this.previewPosition.x = x
484
- this.previewPosition.y = y
485
- this.previewVisible = true
486
- break
613
+ if (!target) {
614
+ this.previewVisible = false
615
+ this.previewScale = { x: 1, y: 1 }
616
+ this.lastHoverRect = null
617
+ return
487
618
  }
488
- }
489
619
 
490
- if (!foundPage) {
491
- this.previewVisible = false
492
- this.previewScale = { x: 1, y: 1 }
493
- }
620
+ this.previewPageDocIndex = target.docIndex
621
+ this.previewPageIndex = target.pageIndex
622
+ this.lastHoverRect = target.rect
623
+ const canvasEl = this.getPageCanvasElement(target.docIndex, target.pageIndex)
624
+ const pagesScale = this.pdfDocuments[target.docIndex]?.pagesScale?.[target.pageIndex] || 1
625
+ const renderWidth = canvasEl?.width || target.rect.width
626
+ const renderHeight = canvasEl?.height || target.rect.height
627
+ const layoutScaleX = renderWidth ? target.rect.width / renderWidth : 1
628
+ const layoutScaleY = renderHeight ? target.rect.height / renderHeight : 1
629
+ const relX = (cursorX - target.rect.left) / layoutScaleX / pagesScale
630
+ const relY = (cursorY - target.rect.top) / layoutScaleY / pagesScale
631
+
632
+ const pageWidth = renderWidth / pagesScale
633
+ const pageHeight = renderHeight / pagesScale
634
+ this.previewScale.x = pagesScale
635
+ this.previewScale.y = pagesScale
636
+ let x = relX - this.previewElement.width / 2
637
+ let y = relY - this.previewElement.height / 2
638
+
639
+ x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width))
640
+ y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height))
641
+
642
+ this.previewPosition.x = x
643
+ this.previewPosition.y = y
644
+ this.previewVisible = true
645
+ })
494
646
  },
495
647
 
496
648
  handleKeyDown(event) {
@@ -506,7 +658,11 @@ export default {
506
658
  const factor = 1 - (event.deltaY * 0.002)
507
659
  const nextVisual = Math.max(0.5, Math.min(3.0, this.visualScale * factor))
508
660
  this.visualScale = nextVisual
509
- this.debouncedApplyZoom()
661
+ if (this.wheelZoomRafId) return
662
+ this.wheelZoomRafId = window.requestAnimationFrame(() => {
663
+ this.wheelZoomRafId = null
664
+ this.commitZoom()
665
+ })
510
666
  },
511
667
 
512
668
  commitZoom() {
@@ -514,10 +670,9 @@ export default {
514
670
 
515
671
  this.scale = newScale
516
672
 
517
- this.pdfDocuments.forEach((doc) => {
518
- doc.pagesScale = doc.pagesScale.map(() => this.scale)
519
- })
673
+ applyScaleToDocs(this.pdfDocuments, this.scale)
520
674
 
675
+ this._pageMeasurementCache = {}
521
676
  this.cachePageBounds()
522
677
  },
523
678
 
@@ -527,7 +682,7 @@ export default {
527
682
 
528
683
  const objectToAdd = {
529
684
  ...this.previewElement,
530
- id: `obj-${Date.now()}`,
685
+ id: this.generateObjectId(),
531
686
  x: Math.round(this.previewPosition.x),
532
687
  y: Math.round(this.previewPosition.y),
533
688
  }
@@ -564,6 +719,30 @@ export default {
564
719
  this.isAddingMode = false
565
720
  this.previewElement = null
566
721
  this.previewVisible = false
722
+ this.detachAddingListeners()
723
+ },
724
+ generateObjectId() {
725
+ const counter = this.nextObjectCounter++
726
+ const rand = Math.random().toString(36).slice(2, 8)
727
+ return `obj-${Date.now()}-${counter}-${rand}`
728
+ },
729
+ attachAddingListeners() {
730
+ if (this.addingListenersAttached) return
731
+ this.addingListenersAttached = true
732
+ document.addEventListener('mousemove', this.handleMouseMove)
733
+ document.addEventListener('touchmove', this.handleMouseMove, { passive: true })
734
+ document.addEventListener('mouseup', this.finishAdding)
735
+ document.addEventListener('touchend', this.finishAdding)
736
+ document.addEventListener('keydown', this.handleKeyDown)
737
+ },
738
+ detachAddingListeners() {
739
+ if (!this.addingListenersAttached) return
740
+ this.addingListenersAttached = false
741
+ document.removeEventListener('mousemove', this.handleMouseMove)
742
+ document.removeEventListener('touchmove', this.handleMouseMove, { passive: true })
743
+ document.removeEventListener('mouseup', this.finishAdding)
744
+ document.removeEventListener('touchend', this.finishAdding)
745
+ document.removeEventListener('keydown', this.handleKeyDown)
567
746
  },
568
747
 
569
748
  addObjectToPage(object, pageIndex = this.selectedPageIndex, docIndex = this.selectedDocIndex) {
@@ -574,45 +753,65 @@ export default {
574
753
  const pageRef = this.getPageComponent(docIndex, pageIndex)
575
754
  if (!pageRef) return false
576
755
 
756
+ let objectToAdd = object
757
+ if (!objectToAdd.id || this.objectIdExists(docIndex, objectToAdd.id)) {
758
+ objectToAdd = { ...objectToAdd, id: this.generateObjectId() }
759
+ }
760
+
577
761
  const pageWidth = this.getPageWidth(docIndex, pageIndex)
578
762
  const pageHeight = this.getPageHeight(docIndex, pageIndex)
579
763
 
580
- if (object.x < 0 || object.y < 0 ||
581
- object.x + object.width > pageWidth ||
582
- object.y + object.height > pageHeight) {
764
+ if (objectToAdd.x < 0 || objectToAdd.y < 0 ||
765
+ objectToAdd.x + objectToAdd.width > pageWidth ||
766
+ objectToAdd.y + objectToAdd.height > pageHeight) {
583
767
  return false
584
768
  }
585
769
 
586
- doc.allObjects = doc.allObjects.map((objects, pIndex) =>
587
- pIndex === pageIndex ? [...objects, object] : objects,
588
- )
770
+ doc.allObjects[pageIndex].push(objectToAdd)
771
+ this.objectIndexCache[`${docIndex}-${objectToAdd.id}`] = pageIndex
589
772
  return true
590
773
  },
774
+ objectIdExists(docIndex, objectId) {
775
+ if (!objectId) return false
776
+ const cacheKey = `${docIndex}-${objectId}`
777
+ if (this.objectIndexCache[cacheKey] !== undefined) return true
778
+ const doc = this.pdfDocuments[docIndex]
779
+ return objectIdExistsInDoc(doc, objectId)
780
+ },
781
+ updateObjectInPage(docIndex, pageIndex, objectId, payload) {
782
+ const doc = this.pdfDocuments[docIndex]
783
+ updateObjectInDoc(doc, pageIndex, objectId, payload)
784
+ },
785
+ removeObjectFromPage(docIndex, pageIndex, objectId) {
786
+ const doc = this.pdfDocuments[docIndex]
787
+ removeObjectFromDoc(doc, pageIndex, objectId)
788
+ },
591
789
 
592
790
  getAllObjects(docIndex = this.selectedDocIndex) {
593
791
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return []
594
792
 
595
793
  const doc = this.pdfDocuments[docIndex]
596
- const scale = this.scale || 1
597
794
  const result = []
598
795
 
599
796
  doc.allObjects.forEach((pageObjects, pageIndex) => {
600
797
  const pageRef = this.getPageComponent(docIndex, pageIndex)
601
798
  if (!pageRef) return
602
-
603
- const measurement = pageRef.getCanvasMeasurement()
604
- const normalizedCanvasHeight = measurement.canvasHeight / scale
799
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
800
+ const normalizedCanvasHeight = measurement.height
801
+ const pagesScale = doc.pagesScale[pageIndex] || 1
605
802
 
606
803
  pageObjects.forEach(object => {
607
804
  result.push({
608
805
  ...object,
609
806
  pageIndex,
610
807
  pageNumber: pageIndex + 1,
611
- scale,
808
+ scale: pagesScale,
612
809
  normalizedCoordinates: {
613
810
  llx: parseInt(object.x, 10),
614
811
  lly: parseInt(normalizedCanvasHeight - object.y, 10),
615
812
  ury: parseInt(normalizedCanvasHeight - object.y - object.height, 10),
813
+ width: parseInt(object.width, 10),
814
+ height: parseInt(object.height, 10),
616
815
  },
617
816
  })
618
817
  })
@@ -629,12 +828,10 @@ export default {
629
828
  let currentPageIndex = this.objectIndexCache[cacheKey]
630
829
 
631
830
  if (currentPageIndex === undefined) {
632
- doc.allObjects.forEach((objects, pIndex) => {
633
- if (objects.find(o => o.id === objectId)) {
634
- currentPageIndex = pIndex
635
- this.objectIndexCache[cacheKey] = pIndex
636
- }
637
- })
831
+ currentPageIndex = findObjectPageIndex(doc, objectId)
832
+ if (currentPageIndex !== undefined) {
833
+ this.objectIndexCache[cacheKey] = currentPageIndex
834
+ }
638
835
  }
639
836
 
640
837
  if (currentPageIndex === undefined) return
@@ -646,19 +843,18 @@ export default {
646
843
  const mouseX = payload._mouseX
647
844
  const mouseY = payload._mouseY
648
845
 
649
- if (!this.pagesBoundingRects || Object.keys(this.pagesBoundingRects).length === 0) {
846
+ const pageBoundsMap = this.getPageBoundsMap()
847
+ if (!pageBoundsMap || Object.keys(pageBoundsMap).length === 0) {
650
848
  this.cachePageBounds()
651
849
  }
652
850
 
653
- const currentPageRect = this.pagesBoundingRects[`${docIndex}-${currentPageIndex}`]?.rect
851
+ const currentPageRect = this.getPageRect(docIndex, currentPageIndex)
654
852
  if (currentPageRect) {
655
853
  const pagesScale = this.getDisplayedPageScale(docIndex, currentPageIndex)
656
854
  const relX = (mouseX - currentPageRect.left - this.draggingElementShift.x) / pagesScale - (this.draggingInitialMouseOffset.x / pagesScale)
657
855
  const relY = (mouseY - currentPageRect.top - this.draggingElementShift.y) / pagesScale - (this.draggingInitialMouseOffset.y / pagesScale)
658
856
 
659
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].map(obj =>
660
- obj.id === objectId ? { ...obj, x: relX, y: relY } : obj
661
- )
857
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, { x: relX, y: relY })
662
858
  }
663
859
  return
664
860
  }
@@ -669,6 +865,14 @@ export default {
669
865
  const objWidth = payload.width !== undefined ? payload.width : targetObject.width
670
866
  const objHeight = payload.height !== undefined ? payload.height : targetObject.height
671
867
 
868
+ const { width: currentPageWidth, height: currentPageHeight } = this.getPageSize(docIndex, currentPageIndex)
869
+ if (newX >= 0 && newY >= 0 &&
870
+ newX + objWidth <= currentPageWidth &&
871
+ newY + objHeight <= currentPageHeight) {
872
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, payload)
873
+ return
874
+ }
875
+
672
876
  let bestPageIndex = currentPageIndex
673
877
  let maxVisibleArea = 0
674
878
 
@@ -676,30 +880,18 @@ export default {
676
880
  const pageWidth = this.getPageWidth(docIndex, pIndex)
677
881
  const pageHeight = this.getPageHeight(docIndex, pIndex)
678
882
 
679
- const visibleLeft = Math.max(0, newX)
680
- const visibleTop = Math.max(0, newY)
681
- const visibleRight = Math.min(pageWidth, newX + objWidth)
682
- const visibleBottom = Math.min(pageHeight, newY + objHeight)
683
-
684
- if (visibleRight > visibleLeft && visibleBottom > visibleTop) {
685
- const visibleArea = (visibleRight - visibleLeft) * (visibleBottom - visibleTop)
686
- if (visibleArea > maxVisibleArea) {
687
- maxVisibleArea = visibleArea
688
- bestPageIndex = pIndex
689
- }
883
+ const visibleArea = getVisibleArea(newX, newY, objWidth, objHeight, pageWidth, pageHeight)
884
+ if (visibleArea > maxVisibleArea) {
885
+ maxVisibleArea = visibleArea
886
+ bestPageIndex = pIndex
690
887
  }
691
888
  }
692
889
 
693
890
  if (bestPageIndex !== currentPageIndex) {
694
- const pageWidth = this.getPageWidth(docIndex, bestPageIndex)
695
- const pageHeight = this.getPageHeight(docIndex, bestPageIndex)
891
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, bestPageIndex)
892
+ const { x: adjustedX, y: adjustedY } = clampPosition(newX, newY, objWidth, objHeight, pageWidth, pageHeight)
696
893
 
697
- const adjustedX = Math.max(0, Math.min(newX, pageWidth - objWidth))
698
- const adjustedY = Math.max(0, Math.min(newY, pageHeight - objHeight))
699
-
700
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].filter(
701
- obj => obj.id !== objectId
702
- )
894
+ this.removeObjectFromPage(docIndex, currentPageIndex, objectId)
703
895
  const updatedObject = {
704
896
  ...targetObject,
705
897
  ...payload,
@@ -711,8 +903,7 @@ export default {
711
903
  return
712
904
  }
713
905
 
714
- const pageWidth = this.getPageWidth(docIndex, currentPageIndex)
715
- const pageHeight = this.getPageHeight(docIndex, currentPageIndex)
906
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, currentPageIndex)
716
907
 
717
908
  if (newX < 0 || newY < 0 ||
718
909
  newX + objWidth > pageWidth ||
@@ -721,18 +912,33 @@ export default {
721
912
  }
722
913
  }
723
914
 
724
- doc.allObjects = doc.allObjects.map(objects =>
725
- objects.map(object => (object.id === objectId ? { ...object, ...payload } : object)),
726
- )
915
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, payload)
727
916
  },
728
917
 
729
918
  deleteObject(docIndex, objectId) {
730
919
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
731
920
  const doc = this.pdfDocuments[docIndex]
732
- doc.allObjects = doc.allObjects.map(objects =>
733
- objects.filter(object => object.id !== objectId),
734
- )
921
+ let deletedObject = null
922
+ let deletedPageIndex = -1
923
+
924
+ doc.allObjects.some((objects, pageIndex) => {
925
+ const objectIndex = objects.findIndex(object => object.id === objectId)
926
+ if (objectIndex === -1) {
927
+ return false
928
+ }
929
+ deletedObject = objects[objectIndex]
930
+ deletedPageIndex = pageIndex
931
+ objects.splice(objectIndex, 1)
932
+ return true
933
+ })
735
934
  delete this.objectIndexCache[`${docIndex}-${objectId}`]
935
+ if (deletedObject) {
936
+ this.$emit('pdf-elements:delete-object', {
937
+ object: deletedObject,
938
+ docIndex,
939
+ pageIndex: deletedPageIndex,
940
+ })
941
+ }
736
942
  },
737
943
 
738
944
  checkAndMoveObjectPage(docIndex, objectId, mouseX, mouseY) {
@@ -743,12 +949,10 @@ export default {
743
949
  let currentPageIndex = this.objectIndexCache[cacheKey]
744
950
 
745
951
  if (currentPageIndex === undefined) {
746
- doc.allObjects.forEach((objects, pIndex) => {
747
- if (objects.find(o => o.id === objectId)) {
748
- currentPageIndex = pIndex
749
- this.objectIndexCache[cacheKey] = pIndex
750
- }
751
- })
952
+ currentPageIndex = findObjectPageIndex(doc, objectId)
953
+ if (currentPageIndex !== undefined) {
954
+ this.objectIndexCache[cacheKey] = currentPageIndex
955
+ }
752
956
  }
753
957
 
754
958
  if (currentPageIndex === undefined) return undefined
@@ -757,8 +961,9 @@ export default {
757
961
  if (!targetObject) return currentPageIndex
758
962
 
759
963
  let targetPageIndex = currentPageIndex
760
- for (const key in this.pagesBoundingRects) {
761
- const { docIndex: rectDocIndex, pageIndex, rect } = this.pagesBoundingRects[key]
964
+ const pageBoundsMap = this.getPageBoundsMap()
965
+ for (const key in pageBoundsMap) {
966
+ const { docIndex: rectDocIndex, pageIndex, rect } = pageBoundsMap[key]
762
967
  if (rectDocIndex === docIndex &&
763
968
  mouseX >= rect.left && mouseX <= rect.right &&
764
969
  mouseY >= rect.top && mouseY <= rect.bottom) {
@@ -767,23 +972,25 @@ export default {
767
972
  }
768
973
  }
769
974
 
770
- const targetPageRect = this.pagesBoundingRects[`${docIndex}-${targetPageIndex}`]?.rect
975
+ const targetPageRect = this.getPageRect(docIndex, targetPageIndex)
771
976
  if (!targetPageRect) return currentPageIndex
772
977
 
773
978
  const pagesScale = this.getDisplayedPageScale(docIndex, targetPageIndex)
774
979
  const relX = (mouseX - targetPageRect.left - this.draggingElementShift.x) / pagesScale - (this.draggingInitialMouseOffset.x / pagesScale)
775
980
  const relY = (mouseY - targetPageRect.top - this.draggingElementShift.y) / pagesScale - (this.draggingInitialMouseOffset.y / pagesScale)
776
981
 
777
- const pageWidth = this.getPageWidth(docIndex, targetPageIndex)
778
- const pageHeight = this.getPageHeight(docIndex, targetPageIndex)
779
-
780
- const clampedX = Math.max(0, Math.min(relX, pageWidth - targetObject.width))
781
- const clampedY = Math.max(0, Math.min(relY, pageHeight - targetObject.height))
982
+ const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, targetPageIndex)
983
+ const { x: clampedX, y: clampedY } = clampPosition(
984
+ relX,
985
+ relY,
986
+ targetObject.width,
987
+ targetObject.height,
988
+ pageWidth,
989
+ pageHeight,
990
+ )
782
991
 
783
992
  if (targetPageIndex !== currentPageIndex) {
784
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].filter(
785
- obj => obj.id !== objectId
786
- )
993
+ this.removeObjectFromPage(docIndex, currentPageIndex, objectId)
787
994
  doc.allObjects[targetPageIndex].push({
788
995
  ...targetObject,
789
996
  x: clampedX,
@@ -791,9 +998,7 @@ export default {
791
998
  })
792
999
  this.objectIndexCache[cacheKey] = targetPageIndex
793
1000
  } else if (clampedX !== targetObject.x || clampedY !== targetObject.y) {
794
- doc.allObjects[currentPageIndex] = doc.allObjects[currentPageIndex].map(obj =>
795
- obj.id === objectId ? { ...obj, x: clampedX, y: clampedY } : obj
796
- )
1001
+ this.updateObjectInPage(docIndex, currentPageIndex, objectId, { x: clampedX, y: clampedY })
797
1002
  }
798
1003
 
799
1004
  return targetPageIndex
@@ -801,8 +1006,9 @@ export default {
801
1006
 
802
1007
  onMeasure(e, docIndex, pageIndex) {
803
1008
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
804
- this.pdfDocuments[docIndex].pagesScale[pageIndex] = e.scale
805
- this.cachePageBounds()
1009
+ this.pdfDocuments[docIndex].pagesScale.splice(pageIndex, 1, e.scale)
1010
+ this._pageMeasurementCache[`${docIndex}-${pageIndex}`] = null
1011
+ this.cachePageBoundsForPage(docIndex, pageIndex)
806
1012
  if (this.autoFitZoom) {
807
1013
  this.scheduleAutoFitZoom()
808
1014
  }
@@ -816,16 +1022,26 @@ export default {
816
1022
  getPageWidth(docIndex, pageIndex) {
817
1023
  const pageRef = this.getPageComponent(docIndex, pageIndex)
818
1024
  if (!pageRef) return 0
819
- const doc = this.pdfDocuments[docIndex]
820
- const pagesScale = doc.pagesScale[pageIndex] || 1
821
- return pageRef.getCanvasMeasurement().canvasWidth / pagesScale
1025
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
1026
+ return measurement.width
822
1027
  },
823
1028
  getPageHeight(docIndex, pageIndex) {
824
1029
  const pageRef = this.getPageComponent(docIndex, pageIndex)
825
1030
  if (!pageRef) return 0
1031
+ const measurement = this.getCachedMeasurement(docIndex, pageIndex, pageRef)
1032
+ return measurement.height
1033
+ },
1034
+ getPageSize(docIndex, pageIndex) {
1035
+ return {
1036
+ width: this.getPageWidth(docIndex, pageIndex),
1037
+ height: this.getPageHeight(docIndex, pageIndex),
1038
+ }
1039
+ },
1040
+ getCachedMeasurement(docIndex, pageIndex, pageRef) {
1041
+ const cacheKey = `${docIndex}-${pageIndex}`
826
1042
  const doc = this.pdfDocuments[docIndex]
827
1043
  const pagesScale = doc.pagesScale[pageIndex] || 1
828
- return pageRef.getCanvasMeasurement().canvasHeight / pagesScale
1044
+ return getCachedMeasurement(this._pageMeasurementCache, cacheKey, pageRef, pagesScale)
829
1045
  },
830
1046
  calculateOptimalScale(maxPageWidth) {
831
1047
  const containerWidth = this.$el?.clientWidth || 0
@@ -869,9 +1085,8 @@ export default {
869
1085
  if (Math.abs(optimalScale - this.scale) > 0.01) {
870
1086
  this.scale = optimalScale
871
1087
  this.visualScale = optimalScale
872
- this.pdfDocuments.forEach((doc) => {
873
- doc.pagesScale = doc.pagesScale.map(() => this.scale)
874
- })
1088
+ applyScaleToDocs(this.pdfDocuments, this.scale)
1089
+ this._pageMeasurementCache = {}
875
1090
  this.cachePageBounds()
876
1091
  }
877
1092
  },
@@ -913,15 +1128,6 @@ export default {
913
1128
  opacity: 0.7;
914
1129
  pointer-events: none;
915
1130
  }
916
- .drag-ghost {
917
- position: fixed;
918
- opacity: 0.9;
919
- pointer-events: none;
920
- z-index: 10000;
921
- transform-origin: top left;
922
- transition: none;
923
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
924
- }
925
1131
  .overlay {
926
1132
  position: absolute;
927
1133
  top: 0;