James Bameron
James Bameron

Reputation: 333

Adding a simple left/right swipe gesture

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

Answers (5)

kofifus
kofifus

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

Rob
Rob

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;
    }
}

How to use

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

Bodokh
Bodokh

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

smmehrab
smmehrab

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:

  1. touchstart: this is when user starts swiping. We take that initial coordinates (x,y) and store them into touchstartX, touchstartY respectively.
  2. 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

treatycity
treatycity

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

Related Questions