@libresign/pdf-elements 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@libresign/pdf-elements",
3
3
  "description": "PDF viewer with draggable and resizable element overlays for Vue 2",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "author": "LibreCode <contact@librecode.coop>",
6
6
  "private": false,
7
7
  "main": "dist/pdf-elements.umd.js",
@@ -120,6 +120,10 @@ export default {
120
120
  showDefaultActions: {
121
121
  type: Boolean,
122
122
  default: true,
123
+ },
124
+ readOnly: {
125
+ type: Boolean,
126
+ default: false,
123
127
  }
124
128
  },
125
129
  data() {
@@ -153,25 +157,41 @@ export default {
153
157
  },
154
158
  elementStyle() {
155
159
  const scale = this.pagesScale || 1
156
- const currentX = this.object.x + this.offsetX + this.resizeOffsetX
157
- const currentY = this.object.y + this.offsetY + this.resizeOffsetY
158
- const currentWidth = this.object.width + this.resizeOffsetW
159
- const currentHeight = this.object.height + this.resizeOffsetH
160
+ const isDragging = this.mode === 'drag'
161
+ const isResizing = this.mode === 'resize'
162
+ const offsetX = isDragging ? this.offsetX : 0
163
+ const offsetY = isDragging ? this.offsetY : 0
164
+ const resizeOffsetX = isResizing ? this.resizeOffsetX : 0
165
+ const resizeOffsetY = isResizing ? this.resizeOffsetY : 0
166
+ const resizeOffsetW = isResizing ? this.resizeOffsetW : 0
167
+ const resizeOffsetH = isResizing ? this.resizeOffsetH : 0
168
+ const currentX = this.object.x + offsetX + resizeOffsetX
169
+ const currentY = this.object.y + offsetY + resizeOffsetY
170
+ const currentWidth = this.object.width + resizeOffsetW
171
+ const currentHeight = this.object.height + resizeOffsetH
160
172
  return {
161
173
  left: `${currentX * scale}px`,
162
174
  top: `${currentY * scale}px`,
163
175
  width: `${currentWidth * scale}px`,
164
176
  height: `${currentHeight * scale}px`,
177
+ pointerEvents: this.readOnly ? 'none' : 'auto',
165
178
  }
166
179
  },
167
180
  toolbarStyle() {
168
181
  const scale = this.pagesScale || 1
169
- const x = this.object.x + this.offsetX + this.resizeOffsetX
170
- const y = this.object.y + this.offsetY + this.resizeOffsetY
171
- const width = this.object.width + this.resizeOffsetW
182
+ const isDragging = this.mode === 'drag'
183
+ const isResizing = this.mode === 'resize'
184
+ const offsetX = isDragging ? this.offsetX : 0
185
+ const offsetY = isDragging ? this.offsetY : 0
186
+ const resizeOffsetX = isResizing ? this.resizeOffsetX : 0
187
+ const resizeOffsetY = isResizing ? this.resizeOffsetY : 0
188
+ const resizeOffsetW = isResizing ? this.resizeOffsetW : 0
189
+ const x = this.object.x + offsetX + resizeOffsetX
190
+ const y = this.object.y + offsetY + resizeOffsetY
191
+ const width = this.object.width + resizeOffsetW
172
192
  return {
173
193
  left: `${(x + width / 2) * scale}px`,
174
- top: `${(y - 48) * scale}px`,
194
+ top: `${(y - 60) * scale}px`,
175
195
  transform: 'translateX(-50%)',
176
196
  }
177
197
  },
@@ -203,6 +223,9 @@ export default {
203
223
  },
204
224
  methods: {
205
225
  handleElementClick(event) {
226
+ if (this.readOnly) {
227
+ return
228
+ }
206
229
  if (event.target.closest('.delete-handle') || event.target.closest('[data-stop-drag]') || event.target.closest('.actions-toolbar')) {
207
230
  return
208
231
  }
@@ -220,6 +243,7 @@ export default {
220
243
  this.startResize(direction, event)
221
244
  },
222
245
  startDrag(event) {
246
+ if (this.readOnly) return
223
247
  if (event.target.classList.contains('delete')) return
224
248
  if (event.target.classList.contains('resize-handle')) return
225
249
  this.mode = 'drag'
@@ -256,6 +280,7 @@ export default {
256
280
  window.addEventListener('touchend', this.boundStopInteraction)
257
281
  },
258
282
  startResize(direction, event) {
283
+ if (this.readOnly) return
259
284
  this.mode = 'resize'
260
285
  this.direction = direction
261
286
  this.startX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX
@@ -318,7 +343,7 @@ export default {
318
343
  return
319
344
  }
320
345
 
321
- const minSize = 16
346
+ const minSize = 16 / (this.pagesScale || 1)
322
347
  let newWidth = this.startWidth
323
348
  let newHeight = this.startHeight
324
349
  let newLeft = this.startLeft
@@ -471,35 +496,43 @@ export default {
471
496
  box-shadow: inset 0 0 0 2px #2563eb;
472
497
  }
473
498
  .resize-handle {
499
+ all: unset;
474
500
  position: absolute;
475
- width: 10px;
476
- height: 10px;
501
+ width: 12px;
502
+ height: 12px;
503
+ min-width: 0;
504
+ min-height: 0;
477
505
  background: #2563eb;
478
506
  border: 1px solid #ffffff;
479
- border-radius: 2px;
507
+ border-radius: 3px;
480
508
  padding: 0;
481
509
  margin: 0;
510
+ line-height: 0;
511
+ font-size: 0;
512
+ box-sizing: border-box;
513
+ display: block;
514
+ appearance: none;
482
515
  cursor: pointer;
483
516
  z-index: 200;
484
517
  }
485
518
  .handle-top-left {
486
- top: -6px;
487
- left: -6px;
519
+ top: -7px;
520
+ left: -7px;
488
521
  cursor: nwse-resize;
489
522
  }
490
523
  .handle-top-right {
491
- top: -6px;
492
- right: -6px;
524
+ top: -7px;
525
+ right: -7px;
493
526
  cursor: nesw-resize;
494
527
  }
495
528
  .handle-bottom-left {
496
- bottom: -6px;
497
- left: -6px;
529
+ bottom: -7px;
530
+ left: -7px;
498
531
  cursor: nesw-resize;
499
532
  }
500
533
  .handle-bottom-right {
501
- bottom: -6px;
502
- right: -6px;
534
+ bottom: -7px;
535
+ right: -7px;
503
536
  cursor: nwse-resize;
504
537
  }
505
538
  </style>
@@ -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,7 +135,6 @@ 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'
@@ -175,6 +182,10 @@ export default {
175
182
  type: Boolean,
176
183
  default: true,
177
184
  },
185
+ readOnly: {
186
+ type: Boolean,
187
+ default: false,
188
+ },
178
189
  pageCountFormat: {
179
190
  type: String,
180
191
  default: '{currentPage} of {totalPages}',
@@ -210,15 +221,15 @@ export default {
210
221
  viewportRafId: 0,
211
222
  objectIndexCache: {},
212
223
  zoomRafId: null,
224
+ wheelZoomRafId: null,
213
225
  boundHandleWheel: null,
214
- debouncedApplyZoom: null,
215
226
  visualScale: this.initialScale,
216
- resizeObserver: null,
227
+ autoFitApplied: false,
228
+ lastContainerWidth: 0,
217
229
  }
218
230
  },
219
231
  mounted() {
220
232
  this.boundHandleWheel = this.handleWheel.bind(this)
221
- this.debouncedApplyZoom = debounce(this.commitZoom, 150)
222
233
  this.init()
223
234
  document.addEventListener('mousemove', this.handleMouseMove)
224
235
  document.addEventListener('touchmove', this.handleMouseMove, { passive: true })
@@ -229,23 +240,15 @@ export default {
229
240
  window.addEventListener('resize', this.onViewportScroll)
230
241
  this.$el?.addEventListener('scroll', this.onViewportScroll, { passive: true })
231
242
  this.$el?.addEventListener('wheel', this.boundHandleWheel, { passive: false })
232
- if (this.autoFitZoom) {
233
- window.addEventListener('resize', this.adjustZoomToFit)
234
- if (typeof ResizeObserver !== 'undefined') {
235
- this.resizeObserver = new ResizeObserver(() => {
236
- this.scheduleAutoFitZoom()
237
- })
238
- if (this.$el) {
239
- this.resizeObserver.observe(this.$el)
240
- }
241
- }
242
- }
243
243
  },
244
244
  beforeUnmount() {
245
245
  if (this.zoomRafId) {
246
246
  cancelAnimationFrame(this.zoomRafId)
247
247
  }
248
- this.debouncedApplyZoom?.clear?.()
248
+ if (this.wheelZoomRafId) {
249
+ cancelAnimationFrame(this.wheelZoomRafId)
250
+ this.wheelZoomRafId = null
251
+ }
249
252
  if (this.boundHandleWheel) {
250
253
  this.$el?.removeEventListener('wheel', this.boundHandleWheel)
251
254
  }
@@ -257,13 +260,6 @@ export default {
257
260
  window.removeEventListener('scroll', this.onViewportScroll)
258
261
  window.removeEventListener('resize', this.onViewportScroll)
259
262
  this.$el?.removeEventListener('scroll', this.onViewportScroll)
260
- if (this.autoFitZoom) {
261
- window.removeEventListener('resize', this.adjustZoomToFit)
262
- if (this.resizeObserver) {
263
- this.resizeObserver.disconnect()
264
- this.resizeObserver = null
265
- }
266
- }
267
263
  if (this.viewportRafId) {
268
264
  window.cancelAnimationFrame(this.viewportRafId)
269
265
  this.viewportRafId = 0
@@ -273,6 +269,7 @@ export default {
273
269
  async init() {
274
270
  if (!this.initFiles || this.initFiles.length === 0) return
275
271
  const docs = []
272
+ this.autoFitApplied = false
276
273
 
277
274
  for (let i = 0; i < this.initFiles.length; i++) {
278
275
  const file = this.initFiles[i]
@@ -417,24 +414,35 @@ export default {
417
414
  },
418
415
 
419
416
  cachePageBounds() {
420
- this.pagesBoundingRects = {}
417
+ const nextRects = {}
421
418
  for (let docIdx = 0; docIdx < this.pdfDocuments.length; docIdx++) {
422
419
  for (let pageIdx = 0; pageIdx < this.pdfDocuments[docIdx].pages.length; pageIdx++) {
423
420
  const canvas = this.getPageCanvasElement(docIdx, pageIdx)
424
421
  if (!canvas) continue
425
422
  const rect = canvas.getBoundingClientRect()
426
- this.pagesBoundingRects[`${docIdx}-${pageIdx}`] = {
423
+ nextRects[`${docIdx}-${pageIdx}`] = {
427
424
  docIndex: docIdx,
428
425
  pageIndex: pageIdx,
429
426
  rect,
430
427
  }
431
428
  }
432
429
  }
430
+ this.pagesBoundingRects = nextRects
433
431
  },
434
432
 
435
433
  getDisplayedPageScale(docIndex, pageIndex) {
436
434
  const doc = this.pdfDocuments[docIndex]
437
435
  if (!doc) return 1
436
+ const baseWidth = doc.pageWidths?.[pageIndex] || 0
437
+ const rectWidth = this.pagesBoundingRects[`${docIndex}-${pageIndex}`]?.rect?.width || 0
438
+ if (rectWidth && baseWidth) {
439
+ return rectWidth / baseWidth
440
+ }
441
+ const canvas = this.getPageCanvasElement(docIndex, pageIndex)
442
+ const fallbackRectWidth = canvas?.getBoundingClientRect?.().width || 0
443
+ if (fallbackRectWidth && baseWidth) {
444
+ return fallbackRectWidth / baseWidth
445
+ }
438
446
  const base = doc.pagesScale[pageIndex] || 1
439
447
  const factor = this.visualScale && this.scale ? (this.visualScale / this.scale) : 1
440
448
  return base * factor
@@ -459,6 +467,14 @@ export default {
459
467
  if (this.isAddingMode || this.isDraggingElement) {
460
468
  this.cachePageBounds()
461
469
  }
470
+ if (this.autoFitZoom && !this.isAddingMode && !this.isDraggingElement) {
471
+ const containerWidth = this.$el?.clientWidth || 0
472
+ if (containerWidth && containerWidth !== this.lastContainerWidth) {
473
+ this.lastContainerWidth = containerWidth
474
+ this.autoFitApplied = false
475
+ this.scheduleAutoFitZoom()
476
+ }
477
+ }
462
478
  this.viewportRafId = 0
463
479
  })
464
480
  },
@@ -480,17 +496,17 @@ export default {
480
496
 
481
497
  const canvasEl = this.getPageCanvasElement(docIndex, pageIndex)
482
498
  const pagesScale = this.pdfDocuments[docIndex]?.pagesScale?.[pageIndex] || 1
483
- const layoutWidth = canvasEl?.offsetWidth || rect.width
484
- const layoutHeight = canvasEl?.offsetHeight || rect.height
485
- const layoutScaleX = layoutWidth ? rect.width / layoutWidth : 1
486
- const layoutScaleY = layoutHeight ? rect.height / layoutHeight : 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
487
503
  const relX = (clientX - rect.left) / layoutScaleX / pagesScale
488
504
  const relY = (clientY - rect.top) / layoutScaleY / pagesScale
489
505
 
490
- const pageWidth = layoutWidth / pagesScale
491
- const pageHeight = layoutHeight / pagesScale
492
- this.previewScale.x = pagesScale * layoutScaleX
493
- this.previewScale.y = pagesScale * layoutScaleY
506
+ const pageWidth = renderWidth / pagesScale
507
+ const pageHeight = renderHeight / pagesScale
508
+ this.previewScale.x = pagesScale
509
+ this.previewScale.y = pagesScale
494
510
  let x = relX - this.previewElement.width / 2
495
511
  let y = relY - this.previewElement.height / 2
496
512
 
@@ -523,7 +539,11 @@ export default {
523
539
  const factor = 1 - (event.deltaY * 0.002)
524
540
  const nextVisual = Math.max(0.5, Math.min(3.0, this.visualScale * factor))
525
541
  this.visualScale = nextVisual
526
- this.debouncedApplyZoom()
542
+ if (this.wheelZoomRafId) return
543
+ this.wheelZoomRafId = window.requestAnimationFrame(() => {
544
+ this.wheelZoomRafId = null
545
+ this.commitZoom()
546
+ })
527
547
  },
528
548
 
529
549
  commitZoom() {
@@ -610,26 +630,27 @@ export default {
610
630
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return []
611
631
 
612
632
  const doc = this.pdfDocuments[docIndex]
613
- const scale = this.scale || 1
614
633
  const result = []
615
634
 
616
635
  doc.allObjects.forEach((pageObjects, pageIndex) => {
617
636
  const pageRef = this.getPageComponent(docIndex, pageIndex)
618
637
  if (!pageRef) return
619
-
620
638
  const measurement = pageRef.getCanvasMeasurement()
621
- const normalizedCanvasHeight = measurement.canvasHeight / scale
639
+ const pagesScale = doc.pagesScale[pageIndex] || 1
640
+ const normalizedCanvasHeight = measurement.canvasHeight / pagesScale
622
641
 
623
642
  pageObjects.forEach(object => {
624
643
  result.push({
625
644
  ...object,
626
645
  pageIndex,
627
646
  pageNumber: pageIndex + 1,
628
- scale,
647
+ scale: pagesScale,
629
648
  normalizedCoordinates: {
630
649
  llx: parseInt(object.x, 10),
631
650
  lly: parseInt(normalizedCanvasHeight - object.y, 10),
632
651
  ury: parseInt(normalizedCanvasHeight - object.y - object.height, 10),
652
+ width: parseInt(object.width, 10),
653
+ height: parseInt(object.height, 10),
633
654
  },
634
655
  })
635
656
  })
@@ -746,10 +767,27 @@ export default {
746
767
  deleteObject(docIndex, objectId) {
747
768
  if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
748
769
  const doc = this.pdfDocuments[docIndex]
749
- doc.allObjects = doc.allObjects.map(objects =>
750
- objects.filter(object => object.id !== objectId),
770
+ let deletedObject = null
771
+ let deletedPageIndex = -1
772
+
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
+ }),
751
782
  )
752
783
  delete this.objectIndexCache[`${docIndex}-${objectId}`]
784
+ if (deletedObject) {
785
+ this.$emit('pdf-elements:delete-object', {
786
+ object: deletedObject,
787
+ docIndex,
788
+ pageIndex: deletedPageIndex,
789
+ })
790
+ }
753
791
  },
754
792
 
755
793
  checkAndMoveObjectPage(docIndex, objectId, mouseX, mouseY) {
@@ -852,6 +890,7 @@ export default {
852
890
  return Math.max(0.1, Math.min(2, availableWidth / maxPageWidth))
853
891
  },
854
892
  scheduleAutoFitZoom() {
893
+ if (this.autoFitApplied) return
855
894
  if (this.zoomRafId) return
856
895
  this.zoomRafId = window.requestAnimationFrame(() => {
857
896
  this.zoomRafId = 0
@@ -859,7 +898,7 @@ export default {
859
898
  })
860
899
  },
861
900
  adjustZoomToFit() {
862
- if (!this.autoFitZoom || !this.pdfDocuments.length) return
901
+ if (!this.autoFitZoom || this.autoFitApplied || !this.pdfDocuments.length) return
863
902
 
864
903
  const widths = this.pdfDocuments
865
904
  .flatMap(doc => doc.pageWidths || [])
@@ -881,6 +920,7 @@ export default {
881
920
  }
882
921
 
883
922
  const optimalScale = this.calculateOptimalScale(maxCanvasWidth)
923
+ this.autoFitApplied = true
884
924
  if (Math.abs(optimalScale - this.scale) > 0.01) {
885
925
  this.scale = optimalScale
886
926
  this.visualScale = optimalScale