coddicat / vue-pinch-scroll-zoom Goto Github PK
View Code? Open in Web Editor NEWVue component that provides content scrolling and zooming using mouse events or two fingers pinch on a mobile devices
Vue component that provides content scrolling and zooming using mouse events or two fingers pinch on a mobile devices
I could not figure out how to use this with Nuxt - the decorators with typescript just didn't want to work. So I rewrote everything into a standard Vue component. Maybe that helps someone else. (I would love to see this plugin without the need for TypeScript support /
without the need for vue-property-decorator. It's exactly what I needed and the source code did save me a ton of time! :)
Parent:
<Zoom
ref="zoomer"
width="1920"
height="1080"
:minScale="1"
@scaling="zoomHandler"
@dragging="zoomHandler"
@startDrag="zoomHandler"
@stopDrag="zoomHandler"
>
...
</Zoom>
methods: {
zoomHandler(zoomData) {
console.log(zoomData)
},
}
Child:
Still requires pinch-scroll-zoom-axis.ts
script (and TypeScript support)
<template>
<div
class="pinch-scroll-zoom"
:class="componentClass"
@mousedown="startDrag"
@mousemove="doDrag"
:style="componentStyle"
>
<div
ref="content"
class="pinch-scroll-zoom__content"
:style="containerStyle"
>
<slot></slot>
</div>
</div>
</template>
<script>
import PinchScrollZoomAxis from "../plugins/custom/zoom/pinch-scroll-zoom-axis";
import _ from 'lodash'
export default {
name: "Zoom",
data() {
return {
touch1: false,
touch2: false,
currentScale: this.scale,
startScale: this.scale,
axisX: new PinchScrollZoomAxis(
this.width,
this.originX,
this.translateX,
this.contentWidth
),
axisY: new PinchScrollZoomAxis(
this.height,
this.originY,
this.translateY,
this.contentHeight
),
throttleDoDrag: _.throttle(this.doDragEvent.bind(this), this.throttleDelay),
stopScaling: _.debounce(this.doStopScalingEvent.bind(this), 200),
zoomIn: false,
zoomOut: false,
stopDragListener: false,
startDragListener: false,
draggingListener: false,
scalingListener: false,
};
},
props: {
contentWidth: {
type: Number | undefined
},
contentHeight: {
type: Number | undefined
},
width: {
required: true,
type: Number
},
height: {
required: true,
type: Number
},
originX: {
type: Number | undefined
},
originY: {
type: Number | undefined
},
translateX: {
type: Number,
default: 0
},
translateY: {
type: Number,
default: 0
},
scale: {
type: Number,
default: 1
},
throttleDelay: {
type: Number,
default: 25
},
within: {
type: Boolean,
default: true
},
minScale: {
type: Number,
default: 0.3
},
maxScale: {
type: Number,
default: 5
},
wheelVelocity: {
type: Number,
default: 0.001
},
draggable: {
type: Boolean,
default: true
},
},
computed: {
componentStyle() {
return {
width: `${this.width}px`,
height: `${this.height}px`,
}
},
componentClass() {
return {
"pinch-scroll-zoom--zoom-out": this.zoomOut,
"pinch-scroll-zoom--zoom-in": this.zoomIn,
}
},
containerStyle() {
const x = `${this.axisX.point}px`;
const y = `${this.axisY.point}px`;
const translate = `translate(${x}, ${y}) scale(${this.currentScale})`;
const transformOrigin = `${this.axisX.origin}px ${this.axisY.origin}px`;
return {
transform: translate,
"transform-origin": transformOrigin,
}
},
},
watch: {
scale(val) {
this.submitScale(val)
},
translateX(val) {
this.axisX.setPoint(val)
},
translateY(val) {
this.axisY.setPoint(val)
},
originX(val) {
this.axisX.setOrigin(val)
},
originY(val) {
this.axisY.setOrigin(val)
},
within(val) {
this.checkWithin()
},
},
methods: {
setData(data) {
this.currentScale = data.scale;
this.axisX.setPoint(data.translateX);
this.axisY.setPoint(data.translateY);
this.axisX.setOrigin(data.originX);
this.axisY.setOrigin(data.originY);
},
getEmitData() {
return {
x: this.axisX.point,
y: this.axisY.point,
scale: this.currentScale,
originX: this.axisX.origin,
originY: this.axisY.origin,
translateX: this.axisX.point,
translateY: this.axisY.point,
}
},
stopDrag() {
this.touch1 = false;
this.touch2 = false;
this.zoomIn = false;
this.zoomOut = false;
if (this.stopDragListener) {
this.$emit("stopDrag", this.getEmitData());
}
},
startDrag(touchEvent) {
if (!this.draggable) return;
if (!touchEvent.touches) {
touchEvent.touches = [
{
clientX: touchEvent.clientX,
clientY: touchEvent.clientY,
},
];
}
if (touchEvent.touches.length == 0) {
this.stopDrag();
return;
}
const clientX1 = this.getBoundingTouchClientX(touchEvent.touches[0]);
const clientY1 = this.getBoundingTouchClientY(touchEvent.touches[0]);
if (touchEvent.touches.length > 1) {
this.touch1 = true;
this.touch2 = true;
this.startScale = this.currentScale;
const clientX2 = this.getBoundingTouchClientX(touchEvent.touches[1]);
const clientY2 = this.getBoundingTouchClientY(touchEvent.touches[1]);
this.axisX.pinch(clientX1, clientX2, this.currentScale);
this.axisY.pinch(clientY1, clientY2, this.currentScale);
} else {
this.touch1 = true;
this.touch2 = false;
this.axisX.touch(clientX1);
this.axisY.touch(clientY1);
}
if (this.startDragListener) {
this.$emit("startDrag", this.getEmitData());
}
},
doDrag(touchEvent) {
if (!this.draggable) return;
this.throttleDoDrag(touchEvent);
},
doStopScalingEvent() {
this.zoomIn = false;
this.zoomOut = false;
},
doDragEvent(touchEvent) {
if (!this.touch1 && !this.touch2) return;
if (!touchEvent.touches) {
touchEvent.touches = [
{
clientX: touchEvent.clientX,
clientY: touchEvent.clientY,
},
];
}
if (touchEvent.touches.length === 0) return;
if (this.touch1 && this.touch2 && touchEvent.touches.length === 1)
this.startDrag(touchEvent);
if (!this.touch1 || (!this.touch2 && touchEvent.touches.length === 2))
this.startDrag(touchEvent);
if (this.touch1 && this.touch2) {
this.axisX.dragPinch(
this.getBoundingTouchClientX(touchEvent.touches[0]),
this.getBoundingTouchClientX(touchEvent.touches[1])
);
this.axisY.dragPinch(
this.getBoundingTouchClientY(touchEvent.touches[0]),
this.getBoundingTouchClientY(touchEvent.touches[1])
);
} else {
this.axisX.dragTouch(this.getBoundingTouchClientX(touchEvent.touches[0]));
this.axisY.dragTouch(this.getBoundingTouchClientY(touchEvent.touches[0]));
}
this.doScale(touchEvent);
this.submitDrag();
},
getBoundingTouchClientX(touch) {
return touch.clientX - this.$el.getBoundingClientRect().left;
},
getBoundingTouchClientY(touch) {
return touch.clientY - this.$el.getBoundingClientRect().top;
},
submitDrag() {
if (this.draggingListener) {
this.$emit("dragging", this.getEmitData());
}
},
getDistance(x1, y1, x2, y2) {
const sqrDistance = (x1 - x2) ** 2 + (y1 - y2) ** 2;
const distance = Math.sqrt(sqrDistance);
return distance;
},
doScale(touchEvent) {
if (touchEvent.touches.length < 2 || !this.touch1 || !this.touch2) {
this.checkWithin();
return;
}
const touch1 = touchEvent.touches[0];
const touch2 = touchEvent.touches[1];
const distance = this.getDistance(
this.getBoundingTouchClientX(touch1),
this.getBoundingTouchClientY(touch1),
this.getBoundingTouchClientX(touch2),
this.getBoundingTouchClientY(touch2)
);
const startDistance = this.getDistance(
this.axisX.start1,
this.axisY.start1,
this.axisX.start2,
this.axisY.start2
);
const scale = this.startScale * (distance / startDistance);
this.submitScale(scale);
},
submitScale(scale) {
if (
(scale >= this.minScale || this.currentScale < scale) &&
(scale <= this.maxScale || this.currentScale > scale)
) {
if (this.currentScale != scale) {
this.zoomIn = this.currentScale < scale;
this.zoomOut = this.currentScale > scale;
this.currentScale = scale;
this.stopScaling();
}
}
this.checkWithin();
if (this.scalingListener) {
this.$emit("scaling", this.getEmitData());
}
},
doWheelScale(event) {
event.preventDefault();
const clientX = this.getBoundingTouchClientX(event);
const clientY = this.getBoundingTouchClientY(event);
this.axisX.pinch(clientX, clientX, this.currentScale);
this.axisY.pinch(clientY, clientY, this.currentScale);
const factor = 1 - event.deltaY * this.wheelVelocity;
const scale = this.currentScale * factor;
this.submitScale(scale);
},
checkWithin(event) {
if (!this.within) {
return;
}
this.axisY.checkAndResetToWithin(this.currentScale);
this.axisX.checkAndResetToWithin(this.currentScale);
},
},
mounted () {
window.addEventListener("mouseup", this.stopDrag);
this.$el.addEventListener("touchstart", this.startDrag);
this.$el.addEventListener("touchmove", this.doDrag);
this.$el.addEventListener("wheel", this.doWheelScale);
this.stopDragListener = !!this.$listeners.stopDrag;
this.startDragListener = !!this.$listeners.startDrag;
this.draggingListener = !!this.$listeners.dragging;
this.scalingListener = !!this.$listeners.scaling;
},
beforeDestroy() {
window.removeEventListener("mouseup", this.stopDrag);
this.$el.removeEventListener("touchstart", this.startDrag);
this.$el.removeEventListener("touchmove", this.doDrag);
this.$el.removeEventListener("wheel", this.doWheelScale);
},
}
</script>
<style lang="scss">
.pinch-scroll-zoom {
position: relative;
touch-action: none;
user-select: none;
user-zoom: none;
overflow: hidden;
:active {
cursor: all-scroll;
}
&--zoom-in {
cursor: zoom-in;
}
&--zoom-out {
cursor: zoom-out;
}
&__content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
}
}
}
</style>
Hi,
First of all, thanks for the neat plugin. You did a great job!
I noticed an annoying bug when the component is used in the right side of the window and you start scrolling to zoom which results in off positioned originX and originY.
The possible solution could be using a ref for div.pinch-scroll-zoom
and calculating the refs' absolute position with .getBoundingClientRect().left
for horizontal axis and .getBoundingClientRect().top
for vertical axis, then extracting these distances from originX and originY accordingly.
Hi,
I got this error while trying to use your package:
This dependency was not found:
* vue-property-decorator in ./node_modules/@coddicat/vue-pinch-scroll-zoom/lib/index.esm.js
I'm adapting the package so it doesn't use vue-property-decorator
but I think it's worth the mentioning in the documentation :)
Thanks !
Hi, thanks for making this awesome library.
I've been struggling to use the setData
method with within
props.
If setData
method gets translateX
or translateY
value that larger than container size, within
feature does not work.
if u press the test
button, u will see what I mean
./node_modules/.pnpm/@[email protected]/node_modules/@coddicat/vue-pinch-scroll-zoom/dist/pinch-scroll-zoom.js 2094:22
Module parse failed: Unexpected token (2094:22)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| function jn(n, t) {
| var e = typeof n;
> return t = t ?? v , !!t && (e == "number" || e != "symbol" && ts.test(n)) && n > -1 && n % 1 == 0 && n < t;
| }
| function an(n, t, e) {
I used "vue": "^3.0.0" with webpack,
import "@coddicat/vue-pinch-scroll-zoom/dist/style.css";
import PinchScrollZoom, {
PinchScrollZoomExposed,
} from "@coddicat/vue-pinch-scroll-zoom";
when I running my project, I got error above
I try to add
plugins: [
["@babel/plugin-proposal-nullish-coalescing-operator"],
["@babel/plugin-proposal-optional-chaining"],
],
in babel.config.js but still not work
Hi,
I'm desperate in trying to find a way to zoom in and zoom out (while respecting the max and min scale set) by pushing on a + ou - button.
Right now I'm using this:
zoomer.value?.setData({
scale: newScale,
originX: state.originX,
originY: state.originY,
translateX: state.translateX,
translateY: state.translateY,
})
newScale is set with a value withing my min and max scale and it works but not in a logic way.
I'm trying to zoom in the middle and unzoom from the middle, just like if I mousewheeled from the center but on the click on a + or - button. Also, with the method I use now, if I unzoom but I was watching the upper part of content, the content gets out of bound as the scale reduce, until I drag the content, then the "within" context is respected and the content gets back to a good position.
One solution could be to add accessible method to allow setting the scale (and scale from middle), but it would then process like a mousewheel event for content positions?
As last time, I'm ok to contribute or sponsor this plugin, with buymeacoffee or anything else. Let me know! ๐
I can also provide video if needed.
Thank you!
When vue3 support will be available :)
When the translate-x or translate-y props are updated I get the following error in the console:
Here's my code:
<template>
<PinchScrollZoom
ref="zoom"
:width="screenWidth"
:height="screenHeight"
:contentWidth="contentWidth"
:contentHeight="contentHeight"
:scale="currentScale"
:minScale="initScale"
:maxScale="initScale * 10"
:origin-x="originX"
:origin-y="originY"
:translate-x="translateX"
:translate-y="translateY"
@scaling="onScale"
>
<div ref="scrollContent" :style="{ width: contentWidth, height: contentHeight }" @click="myEvent"><img src="/lanetest.png" /></div>
</PinchScrollZoom>
</template>
<script>
import PinchScrollZoom from '@coddicat/vue-pinch-scroll-zoom'
export default {
components: {
PinchScrollZoom
},
data () {
return {
contentWidth: 1102,
contentHeight: 1966,
currentScale: 0,
originX: 0,
originY: 0,
translateX: 0,
translateY: 0
}
},
computed: {
screenHeight () {
return this.$q.screen.height - 50
},
screenWidth () {
return this.$q.screen.width
},
initScale () {
// Calculate the initial scale to fit the image in the screen
return Math.max(
this.screenWidth / this.contentWidth,
this.screenHeight / this.contentHeight
)
}
},
methods: {
myEvent (e) {
console.log(e.offsetX, e.offsetY)
this.translateX = 39
this.translateY = -1009
this.currentScale = this.initScale * 4
},
onScale (e) {
console.log(e)
this.currentScale = e.scale
}
},
mounted () {
this.currentScale = this.initScale
}
}
</script>
<style>
</style>
Tested in a brand new Quasar/Vite project with the above in App.vue.
I couldn't install this with my vue-cli 3 project. Below is the error code.
npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: [email protected] npm ERR! Found: [email protected] npm ERR! node_modules/vue npm ERR! vue@"^3.2.13" from the root project npm ERR! npm ERR! Could not resolve dependency: npm ERR! peer vue@"^2.6.11" from @coddicat/[email protected] npm ERR! node_modules/@coddicat/vue-pinch-scroll-zoom npm ERR! @coddicat/vue-pinch-scroll-zoom@"*" from the root project npm ERR! npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force, or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
Is it possible to set the width
and height
props to match the viewport width and height and have within
enabled?
My use case is a responsive fullscreen modal, with a image.
I get the following error when I try to use this in my app:
Failed to resolve entry for package "@coddicat/vue-pinch-scroll-zoom". The package may have incorrect main/module/exports specified in its package.json: Failed to resolve entry for package "@coddicat/vue-pinch-scroll-zoom". The package may have incorrect main/module/exports specified in its package.json.
Am I missing something obvious? It's a vue 3 / Quasar 2 app and the component I'm importing into used the options API if that's relevant?
This occurs even in this simple component:
<template>
<PinchScrollZoom>
<img src="/test.png"/>
</PinchScrollZoom>
</template>
<script setup>
import PinchScrollZoom from '@coddicat/vue-pinch-scroll-zoom'
</script>
Any help greatly appreciated as this looks like just what I've been looking for to replace PinchZoom.js! :)
Hi,
I'm trying to find a way how to disable all scrolling and panning, while keeping the current transformation. Is that possible. And if so, can you tell me how to do that?
Hi,
I have set min-scale to 1 and max-scale to 3. I can zoom with the mouse wheel and it works well, but then when I unzoom with the mouse wheel, it can't get back precisely to the 1.0 scale (min-value), it remains a little bit bigger.
If I zoom and unzoom many times with mouse wheel I can get close to the 1.0 scale but never exactly it. Playing with the wheel-velocity helps a little and allows me to get closer ton min-scale, but does not fix the issue.
It's problematic because it leaves room for a vertical scroll even when the content is same height as the viewer.
Thank you for the great package, it's very useful. If you have a buymeacoffeelink I'd send a lil something for sure.
I have other less important issues that I might post here soon, I try to gather more informations.
Have a good day!
First of all, great component!
I wondering if it is possible to use values for width and height, other than pixels. For example %, vh etc.
<PinchScrollZoom
ref="zoomer"
within
centred
key-actions
width="100%"
height="100vh"
:min-scale="0.5"
:max-scale="15"
@scaling="(e) => onEvent('scaling', e)"
@startDrag="(e) => onEvent('startDrag', e)"
@stopDrag="(e) => onEvent('stopDrag', e)"
@dragging="(e) => onEvent('dragging', e)"
>
<img src="..." />
</PinchScrollZoom>
I'm using the Vue3 version and it looks like the width and height values are not reactive, so for example, I can't dynamically resize the zoomer in a responsive design app. I guess I can understand this isn't an easy undertaking, but thought I'd save somebody a few hours trying to make it work.
<PinchScrollZoom
:width="item.slideSizeWidth" // dynamically changing item.slideSizeWidth has no effect !!!
:height="item.slideSizeHeight"
If one were to implement this feature, forcing a reset whenever the width or height changed would be a fine simplification.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.