Reputation: 333
I need to add a simple left/right swipe gesture so that the 'selected' image cycles when swiped on mobile, similar to clicking the buttons in the hero component, also similar to pressing the left/right arrow keys on a keyboard
I don't have the most experience with JavaScript so if anyone could tell me what exactly to write and where so that I can completely wrap up this project.
Here is a demo: http://nufaith.ca/justinatkins/
Code:
Vue.component('hero-bg', {
template: `
<div class="hero-bg">
<div class="hero">
<img id="pushed" :src="selected"/>
</div>
</div>
`,
props: ['selected']
});
Vue.component('hero-bg-empty', {
template: `
<div class="hero-bg">
<div class="hero">
<span style="display:block;height:100px;"></span>
</div>
</div>
`
});
Vue.component('hero', {
template: `
<div>
<topbar v-if="!gridEnabled"></topbar>
<topbar2 v-if="gridEnabled"></topbar2>
<hero-bg :selected="selectedItem.img" v-if="!gridEnabled"></hero-bg>
<hero-bg-empty v-if="gridEnabled"></hero-bg-empty>
<div class="hero-container" v-if="!gridEnabled">
<div class="hero">
<img :src="selectedItem.img" v-if="thing" alt=""/>
</div>
<div class="hero-desc">
<button class="control left" @click="previous">
<i class="zmdi zmdi-chevron-left"></i>
</button>
<span class="hero-desc-title" v-html="title"></span>
<button class="control right" @click="next">
<i class="zmdi zmdi-chevron-right"></i>
</button>
<br/>
<button class="view-all-button" @click="enableGrid">OVERVIEW</button>
</div>
</div>
</div>
`,
data() {
return {
gridEnabled: false,
selected: 0,
thing: true
};
},
computed: {
selectedItem() {
return info[this.selected];
},
title() {
const comma = this.selectedItem.title.indexOf(',');
const len = this.selectedItem.title.length;
const strBeginning = this.selectedItem.title.substring(comma, 0);
const strEnd = this.selectedItem.title.substring(comma, len);
if (this.selectedItem.title.includes(',')) {
return `<span>${strBeginning}<span class="font-regular font-muted">${strEnd}</span></span>`;
}
return this.selectedItem.title;
},
maxImages() {
return info.length - 1;
}
},
created() {
window.addEventListener('keydown', e => {
if (e.keyCode === 37) {
this.previous();
return;
}
if (e.keyCode === 39) {
this.next();
return;
}
});
Event.$on('updateImg', index => {
this.selected = index;
this.gridEnabled = !this.gridEnabled;
});
},
methods: {
next() {
this.selected === this.maxImages ? (this.selected = 0) : (this.selected += 1);
},
previous() {
this.selected === 0 ? (this.selected = this.maxImages) : (this.selected -= 1);
},
enableGrid() {
this.gridEnabled = !this.gridEnabled;
window.scroll(0, 0);
Event.$emit('enableGrid');
}
}
});
Upvotes: 22
Views: 29497
Reputation: 19305
A reworking of phoenix answer:
function registerTouchEvent(element, callback) {
const THRESHOLD = 50 // Minimum difference in pixels at which a swipe gesture is detected
let startEvent
element.addEventListener('touchstart', ev => startEvent = ev)
element.addEventListener('touchend', endEvent => {
if (!startEvent.changedTouches || !endEvent.changedTouches) return
const start = startEvent.changedTouches[0]
const end = endEvent.changedTouches[0]
if (!start || !end) return
const horizontalDifference = start.screenX - end.screenX
const verticalDifference = start.screenY - end.screenY
const horizontal = Math.abs(horizontalDifference) > Math.abs(verticalDifference) && Math.abs(verticalDifference) < THRESHOLD
const vertical = !horizontal && Math.abs(horizontalDifference) < THRESHOLD
let direction = 'diagonal';
if (horizontal) direction = horizontalDifference >= THRESHOLD ? 'left' : (horizontalDifference <= -THRESHOLD ? 'right' : 'click')
if (vertical) direction = verticalDifference >= THRESHOLD ? 'up' : (verticalDifference <= -THRESHOLD ? 'down' : 'click')
callback(direction, startEvent, endEvent)
})
}
To use:
registerTouchEvent(document, callback)
callback
will receive a string left/right/up/down/click/diagonal and the startEvent and endEvent
Upvotes: 0
Reputation: 1328
I took smmehrab’s answer, added some thresholds to avoid accidental swipes, and turned it into a little library. Might come in handy, so here it is:
export default class TouchEvent {
static SWIPE_THRESHOLD = 50; // Minimum difference in pixels at which a swipe gesture is detected
static SWIPE_LEFT = 1;
static SWIPE_RIGHT = 2;
static SWIPE_UP = 3;
static SWIPE_DOWN = 4;
constructor(startEvent, endEvent) {
this.startEvent = startEvent;
this.endEvent = endEvent || null;
}
isSwipeLeft() {
return this.getSwipeDirection() == TouchEvent.SWIPE_LEFT;
}
isSwipeRight() {
return this.getSwipeDirection() == TouchEvent.SWIPE_RIGHT;
}
isSwipeUp() {
return this.getSwipeDirection() == TouchEvent.SWIPE_UP;
}
isSwipeDown() {
return this.getSwipeDirection() == TouchEvent.SWIPE_DOWN;
}
getSwipeDirection() {
if (!this.startEvent.changedTouches || !this.endEvent.changedTouches) {
return null;
}
let start = this.startEvent.changedTouches[0];
let end = this.endEvent.changedTouches[0];
if (!start || !end) {
return null;
}
let horizontalDifference = start.screenX - end.screenX;
let verticalDifference = start.screenY - end.screenY;
// Horizontal difference dominates
if (Math.abs(horizontalDifference) > Math.abs(verticalDifference)) {
if (horizontalDifference >= TouchEvent.SWIPE_THRESHOLD) {
return TouchEvent.SWIPE_LEFT;
} else if (horizontalDifference <= -TouchEvent.SWIPE_THRESHOLD) {
return TouchEvent.SWIPE_RIGHT;
}
// Vertical or no difference dominates
} else {
if (verticalDifference >= TouchEvent.SWIPE_THRESHOLD) {
return TouchEvent.SWIPE_UP;
} else if (verticalDifference <= -TouchEvent.SWIPE_THRESHOLD) {
return TouchEvent.SWIPE_DOWN;
}
}
return null;
}
setEndEvent(endEvent) {
this.endEvent = endEvent;
}
}
Simply feed it the events from touchstart
and touchend
:
import TouchEvent from '@/TouchEvent'
let touchEvent = null;
document.addEventListener('touchstart', (event) => {
touchEvent = new TouchEvent(event);
});
document.addEventListener('touchend', handleSwipe);
function handleSwipe(event) {
if (!touchEvent) {
return;
}
touchEvent.setEndEvent(event);
if (touchEvent.isSwipeRight()) {
// Do something
} else if (touchEvent.isSwipeLeft()) {
// Do something different
}
// Reset event for next touch
touchEvent = null;
}
Upvotes: 11
Reputation: 1066
Using @smmehrab answer, I created a Vue 3 composable that also works for SSR builds.
import { onMounted, Ref } from 'vue'
export type SwipeCallback = (event: TouchEvent) => void;
export type SwipeOptions = {
directinoal_threshold?: number; // Pixels offset to trigger swipe
};
export const useSwipe = (touchableElement: HTMLElement = null, options: Ref<SwipeOptions> = ref({
directinoal_threshold: 10
})) => {
const touchStartX = ref(0);
const touchEndX = ref(0);
const touchStartY = ref(0);
const touchEndY = ref(0);
onMounted(() => {
if (!touchableElement)
touchableElement = document.body;
touchableElement.addEventListener('touchstart', (event) => {
touchStartX.value = event.changedTouches[0].screenX;
touchStartY.value = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', (event) => {
touchEndX.value = event.changedTouches[0].screenX;
touchEndY.value = event.changedTouches[0].screenY;
handleGesture(event);
}, false);
});
const onSwipeLeft: Array<SwipeCallback> = [];
const onSwipeRight: Array<SwipeCallback> = [];
const onSwipeUp: Array<SwipeCallback> = [];
const onSwipeDown: Array<SwipeCallback> = [];
const onTap: Array<SwipeCallback> = [];
const addEventListener = (arr: Array<SwipeCallback>, callback: SwipeCallback) => {
arr.push(callback);
};
const handleGesture = (event: TouchEvent) => {
if (touchEndX.value < touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeLeft.forEach(callback => callback(event));
}
if (touchEndX.value > touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeRight.forEach(callback => callback(event));
}
if (touchEndY.value < touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeUp.forEach(callback => callback(event));
}
if (touchEndY.value > touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeDown.forEach(callback => callback(event));
}
if (touchEndY.value === touchStartY.value) {
onTap.forEach(callback => callback(event));
}
}
return {
onSwipeLeft: (callback: SwipeCallback) => addEventListener(onSwipeLeft, callback),
onSwipeRight: (callback: SwipeCallback) => addEventListener(onSwipeRight, callback),
onSwipeUp: (callback: SwipeCallback) => addEventListener(onSwipeUp, callback),
onSwipeDown: (callback: SwipeCallback) => addEventListener(onSwipeDown, callback),
onTap: (callback: SwipeCallback) => addEventListener(onTap, callback)
}
}
Example usage:
const { onSwipeLeft, onSwipeRight } = useSwipe(document.body);
onSwipeLeft((e:TouchEvent) => {
//logic
});
Upvotes: 1
Reputation: 876
This is how I implemented a simple swipe gesture in one of my projects. You may check this out.
Code:
touchableElement.addEventListener('touchstart', function (event) {
touchstartX = event.changedTouches[0].screenX;
touchstartY = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', function (event) {
touchendX = event.changedTouches[0].screenX;
touchendY = event.changedTouches[0].screenY;
handleGesture();
}, false);
function handleGesture() {
if (touchendX < touchstartX) {
console.log('Swiped Left');
}
if (touchendX > touchstartX) {
console.log('Swiped Right');
}
if (touchendY < touchstartY) {
console.log('Swiped Up');
}
if (touchendY > touchstartY) {
console.log('Swiped Down');
}
if (touchendY === touchstartY) {
console.log('Tap');
}
}
Basically, touchableElement
mentioned here, refers to the DOM Element
that will receive the touch event. If you want to activate swipe options on your entire screen, then you may use your body
tag as the touchable element. Or you may configure any specific div
element as the touchable element, in case you just want the swipe gesture on that specific div
.
On that touchableElement
, we are adding 2 event-listeners here:
touchstart
:
this is when user starts swiping. We take that initial coordinates (x,y) and
store them into touchstartX, touchstartY respectively.touchend
: this is when user stops swiping. We take that final coordinates (x, y) and store them into touchendX, touchendY respectively.Keep in mind that, the origin of these coordinates is the top left corner of the screen. x-coordinate increases as you go from left to right and y-coordinate increases as you go from top to bottom.
Then, in handleGesture()
, we just compare those 2 pair of coordinates (touchstartX, touchstartY) and (touchendX, touchendY), to detect different types of swipe gesture (up, down, left, right):
touchendX < touchstartX
: says that, user started swiping at a higher X value & stopped swiping at a lower X value. That means, swiped from right to left (Swiped Left).
touchendX > touchstartX
: says that, user started swiping at a lower X value & stopped swiping at a higher X value. That means, swiped from left to right (Swiped Right).
touchendY < touchstartY
: says that, user started swiping at a higher Y value & stopped swiping at a lower Y value. That means, swiped from bottom to top (Swiped Up).
touchendY > touchstartY
: says that, user started swiping at a lower Y value & stopped swiping at a higher Y value. That means, swiped from top to bottom (Swiped Down).
You may add the code for these 4 different events (Swipe Up/Down/Left/Right), on the corresponding if
blocks, as shown on the code.
Upvotes: 32
Reputation: 141
This sounds like a job for Hammer.JS, unless you're trying to avoid dependencies. They have good documentation and examples for getting started
My Vue knowledge is next to nothing, so I'm wary of this becoming a blind leading the blind scenario, but the first thing you'll have to do is add the dependency using either npm or yarn - then add it to the top of your file using
import Hammer from 'hammerjs'
Try adding the below code right above this line: Event.$on('updateImg', index => {
const swipeableEl = document.getElementsByClassName('.hero')[0];
this.hammer = Hammer(swipeableEl)
this.hammer.on('swipeleft', () => this.next())
this.hammer.on('swiperight', () => this.previous())
If it doesn't work you'll have to check your developer tools / console log to see if it's logged any useful errors.
This codepen might be a useful resource too:
Good luck.
Upvotes: 4