@@ -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