Skip to content

Commit 76310ad

Browse files
committed
fix: handle pinch zoom inside the PDF viewer
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 04d8d81 commit 76310ad

3 files changed

Lines changed: 122 additions & 5 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@libresign/pdf-elements",
33
"description": "PDF viewer with draggable and resizable element overlays for Vue 3",
4-
"version": "1.2.3",
4+
"version": "1.2.4",
55
"author": "LibreCode <contact@librecode.coop>",
66
"private": false,
77
"main": "dist/index.mjs",

src/components/PDFElements.vue

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ export default defineComponent({
243243
pendingHoverClientPos: null as { x: number; y: number } | null,
244244
lastHoverRect: null as DOMRect | null,
245245
addingListenersAttached: false,
246+
isPinching: false,
247+
pinchStartDistance: 0,
248+
pinchStartScale: this.initialScale,
249+
pinchAnchor: { x: 0, y: 0 },
250+
pinchCenter: { x: 0, y: 0 },
251+
suppressTouchClickUntil: 0,
246252
dragRafId: 0,
247253
pendingDragClientPos: null as { x: number; y: number } | null,
248254
pageBoundsVersion: 0,
@@ -263,6 +269,9 @@ export default defineComponent({
263269
zoomRafId: null as number | null,
264270
wheelZoomRafId: null as number | null,
265271
boundHandleWheel: null as ((event: WheelEvent) => void) | null,
272+
boundHandleTouchStart: null as ((event: TouchEvent) => void) | null,
273+
boundHandleTouchMove: null as ((event: TouchEvent) => void) | null,
274+
boundHandleTouchEnd: null as ((event: TouchEvent) => void) | null,
266275
visualScale: this.initialScale,
267276
autoFitApplied: false,
268277
lastContainerWidth: 0,
@@ -275,11 +284,18 @@ export default defineComponent({
275284
},
276285
mounted() {
277286
this.boundHandleWheel = this.handleWheel.bind(this)
287+
this.boundHandleTouchStart = this.handleTouchStart.bind(this)
288+
this.boundHandleTouchMove = this.handleTouchMove.bind(this)
289+
this.boundHandleTouchEnd = this.handleTouchEnd.bind(this)
278290
this.init()
279291
window.addEventListener('scroll', this.onViewportScroll, { passive: true })
280292
window.addEventListener('resize', this.onViewportScroll)
281293
this.$el?.addEventListener('scroll', this.onViewportScroll, { passive: true })
282294
this.$el?.addEventListener('wheel', this.boundHandleWheel, { passive: false })
295+
this.$el?.addEventListener('touchstart', this.boundHandleTouchStart, { passive: false })
296+
this.$el?.addEventListener('touchmove', this.boundHandleTouchMove, { passive: false })
297+
this.$el?.addEventListener('touchend', this.boundHandleTouchEnd)
298+
this.$el?.addEventListener('touchcancel', this.boundHandleTouchEnd)
283299
},
284300
beforeUnmount() {
285301
if (this.zoomRafId) {
@@ -292,6 +308,16 @@ export default defineComponent({
292308
if (this.boundHandleWheel) {
293309
this.$el?.removeEventListener('wheel', this.boundHandleWheel)
294310
}
311+
if (this.boundHandleTouchStart) {
312+
this.$el?.removeEventListener('touchstart', this.boundHandleTouchStart)
313+
}
314+
if (this.boundHandleTouchMove) {
315+
this.$el?.removeEventListener('touchmove', this.boundHandleTouchMove)
316+
}
317+
if (this.boundHandleTouchEnd) {
318+
this.$el?.removeEventListener('touchend', this.boundHandleTouchEnd)
319+
this.$el?.removeEventListener('touchcancel', this.boundHandleTouchEnd)
320+
}
295321
this.detachAddingListeners()
296322
window.removeEventListener('scroll', this.onViewportScroll)
297323
window.removeEventListener('resize', this.onViewportScroll)
@@ -547,6 +573,95 @@ export default defineComponent({
547573
y: event?.clientY,
548574
}
549575
},
576+
getTouchDistance(touches) {
577+
if (!touches || touches.length < 2) return 0
578+
const first = touches[0]
579+
const second = touches[1]
580+
return Math.hypot(second.clientX - first.clientX, second.clientY - first.clientY)
581+
},
582+
getTouchCenter(touches) {
583+
if (!touches || touches.length < 2) return null
584+
const first = touches[0]
585+
const second = touches[1]
586+
return {
587+
x: (first.clientX + second.clientX) / 2,
588+
y: (first.clientY + second.clientY) / 2,
589+
}
590+
},
591+
clampZoomScale(scale) {
592+
return Math.max(0.5, Math.min(3.0, scale))
593+
},
594+
startPinchZoom(event) {
595+
const container = this.$el
596+
const center = this.getTouchCenter(event.touches)
597+
const distance = this.getTouchDistance(event.touches)
598+
if (!container || !center || !distance) return
599+
600+
const containerRect = container.getBoundingClientRect()
601+
const localCenterX = center.x - containerRect.left
602+
const localCenterY = center.y - containerRect.top
603+
const currentScale = this.scale || 1
604+
605+
this.isPinching = true
606+
this.pinchStartDistance = distance
607+
this.pinchStartScale = this.visualScale || currentScale
608+
this.pinchCenter = { x: localCenterX, y: localCenterY }
609+
this.pinchAnchor = {
610+
x: (container.scrollLeft + localCenterX) / currentScale,
611+
y: (container.scrollTop + localCenterY) / currentScale,
612+
}
613+
this.suppressTouchClickUntil = Date.now() + 300
614+
},
615+
applyPinchZoom(nextScale, center) {
616+
const container = this.$el
617+
if (!container) return
618+
619+
this.visualScale = nextScale
620+
this.commitZoom()
621+
container.scrollLeft = Math.max(0, (this.pinchAnchor.x * nextScale) - center.x)
622+
container.scrollTop = Math.max(0, (this.pinchAnchor.y * nextScale) - center.y)
623+
this.cachePageBounds()
624+
},
625+
handleTouchStart(event) {
626+
if (event.touches.length !== 2 || this.isAddingMode || this.isDraggingElement) return
627+
if (event.cancelable) {
628+
event.preventDefault()
629+
}
630+
this.startPinchZoom(event)
631+
},
632+
handleTouchMove(event) {
633+
if (event.touches.length !== 2) return
634+
if (!this.isPinching) {
635+
this.startPinchZoom(event)
636+
}
637+
if (!this.isPinching) return
638+
if (event.cancelable) {
639+
event.preventDefault()
640+
}
641+
642+
const container = this.$el
643+
const center = this.getTouchCenter(event.touches)
644+
const distance = this.getTouchDistance(event.touches)
645+
if (!container || !center || !distance || !this.pinchStartDistance) return
646+
647+
const containerRect = container.getBoundingClientRect()
648+
const localCenter = {
649+
x: center.x - containerRect.left,
650+
y: center.y - containerRect.top,
651+
}
652+
const nextScale = this.clampZoomScale(this.pinchStartScale * (distance / this.pinchStartDistance))
653+
654+
this.pinchCenter = localCenter
655+
this.applyPinchZoom(nextScale, localCenter)
656+
},
657+
handleTouchEnd(event) {
658+
if (event.touches.length >= 2) {
659+
this.startPinchZoom(event)
660+
return
661+
}
662+
this.isPinching = false
663+
this.pinchStartDistance = 0
664+
},
550665
updatePreviewFromClientPoint(cursorX, cursorY) {
551666
let target = null
552667
@@ -669,6 +784,7 @@ export default defineComponent({
669784
},
670785
671786
handleMouseMove(event) {
787+
if (event?.touches?.length > 1 || this.isPinching) return
672788
if (!this.isAddingMode || !this.previewElement) return
673789
const { x, y } = this.getPointerPosition(event)
674790
if (x === undefined || y === undefined) return
@@ -688,6 +804,7 @@ export default defineComponent({
688804
})
689805
},
690806
handleOverlayClick(docIndex, pageIndex, event) {
807+
if (event?.type?.includes?.('touch') && (this.isPinching || Date.now() < this.suppressTouchClickUntil)) return
691808
if (!this.emitObjectClick) return
692809
693810
const { x: clientX, y: clientY } = this.getPointerPosition(event)
@@ -782,7 +899,7 @@ export default defineComponent({
782899
event.preventDefault()
783900
784901
const factor = 1 - (event.deltaY * 0.002)
785-
const nextVisual = Math.max(0.5, Math.min(3.0, this.visualScale * factor))
902+
const nextVisual = this.clampZoomScale(this.visualScale * factor)
786903
this.visualScale = nextVisual
787904
if (this.wheelZoomRafId) return
788905
this.wheelZoomRafId = window.requestAnimationFrame(() => {
@@ -1305,7 +1422,7 @@ export default defineComponent({
13051422
overflow-y: auto;
13061423
overflow-x: auto;
13071424
box-sizing: border-box;
1308-
touch-action: pan-x pan-y pinch-zoom;
1425+
touch-action: pan-x pan-y;
13091426
-webkit-overflow-scrolling: touch;
13101427
}
13111428
.pages-container {

0 commit comments

Comments
 (0)