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');
}
}
});
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.
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 SWPIE_THRESHOLD = 50 // Minumum 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()
{
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.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_LEFT
} else if (horizontalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_RIGHT
}
// Verical or no difference dominates
} else {
if (verticalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_UP
} else if (verticalDifference <= -TouchEvent.SWPIE_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;
}
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.
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
});
I am trying to offset an announcement bar when scrolling down, taking into consideration that the height of the bar is more important on smaller devices.
Here's an example of what I want to achieve : https://8kflexwarm.com/
So I ended up with this piece of code, which is working, but I feel like it is not optimized, not clean code. I figure there must be a way to offset $('.announcement-bar') instead of doing it manually with window size.
Also, why is this code not working when I refresh the screen and I'm not on top of the page ?
Is there a way to improve this code without using a library ?
if($(window).width() >= 686){
var a = $(".site-header").offset().top;
function scrollListener(){
if($(document).scrollTop() > a)
{
$('.site-header').css({"margin-top":"-40px"});
$('.site-header').css({"transition":"0.4s"});
} else {
$('.site-header').css({"margin-top":"0px"});
$('.site-header').css({"transition":"0.4s"});
}
};
$(document).scroll(scrollListener);
scrollListener();
} else if($(window).width() >= 370) {
var a = $(".site-header").offset().top;
function scrollListener(){
if($(document).scrollTop() > a)
{
$('.site-header').css({"margin-top":"-65px"});
$('.site-header').css({"transition":"0.4s"});
} else {
$('.site-header').css({"margin-top":"0px"});
$('.site-header').css({"transition":"0.4s"});
}
};
$(document).scroll(scrollListener);
scrollListener();
} else {
var a = $(".site-header").offset().top;
function scrollListener(){
if($(document).scrollTop() > a)
{
$('.site-header').css({"margin-top":"-90px"});
$('.site-header').css({"transition":"0.4s"});
} else {
$('.site-header').css({"margin-top":"0px"});
$('.site-header').css({"transition":"0.4s"});
}
};
$(document).scroll(scrollListener);
scrollListener();
};
Please provide a codePen, it makes it easier to help you with your question.
I came up with this untested piece of javascript:
var myApp = (function(app) {
const $elements = {
siteHeader = null,
}
function setPosition() {
const scrollTop = $(document).scrollTop()
const offsetTop = $elements.siteHeader.offset().top
if(scrollTop > offsetTop){
$elements.siteHeader.css({'margin-top':`${$elements.siteHeader.height() * -1}px`})
} else {
$elements.siteHeader.css({'margin-top':'0px'})
}
}
function initialize() {
// Wait for all elements to be created
$(function() {
$elements.siteHeader = $('.site-header')
setPosition()
$(document).scroll(setPosition)
$(document).resize(setPosition)
})
}
initialize()
return app;
})(myApp || {})
I wanna detect when the user scrolls. I saw there are some questions about this but those answers didn't help me.
Here is my code:
var up = document.getElementById('up');
var down = document.getElementById('down');
function onScroll() {
if (view.scrollTop > 0) {
console.log("up");
}
if (view.scrollTop < 0) {
console.log("down");
}
}
var view = document.getElementById('container');
view.addEventListener("wheel", onScroll);
EDIT: "scroll" instead "wheel" isn't working for me... I must have the "wheel". When I make:
if (view.scrolltop <=0) {
console.log("down");
}
that I get in my console "down" but it appears when I scrolling up too! I have my page in 100% of screen and I have no scrollbars (and I don't want to have).
EDIT2: Here is the code that solved my problem!
window.addEventListener('wheel', function (e) {
if (e.deltaY < 0) {
console.log("scrolling up");
}
if (e.deltaY > 0) {
console.log("scrolling down");
}
});
you can use the
window.onscroll
to get the scrolling event, then just use
.scrollTop
to determine your offset from the top of page or an element.
window.onscroll = function() { scrollFunction() };
scrollFunction() {
//code to check if it is scrolling up or down
}
Hopefully this was a helpful start.
I did this with the jquery method
var last = 0;
$(window).scroll(function(event){
var Pos = $(this).scrollTop();
if (Pos > last ){
// down
} else {
// up
}
last = Pos;
});
Supporting #Belmin Bedak's comment.
Use .scroll instead of .wheel:
var up = document.getElementById('up');
var down = document.getElementById('down');
function onScroll() {
if (view.scrollTop > 0) {
alert("up");
}
if (view.scrollTop <= 0) {
alert("down");
}
}
var view = document.getElementById('container');
view.addEventListener("scroll", onScroll);
div {
border: 1px solid black;
width: 200px;
height: 100px;
overflow: scroll;
}
<div id='container'>a<br>
b<br>
c<br>
d<br>
e<br>
f<br>
g<br>
h<br>
i<br>
k<br>
l<br>
</div>
I was looking for a function that would scroll a given element into view with some smart behavior:
if an element is descendant of a scrollable element - that ancestor is scrolled rather than body.
if an element is descendant of a positioned element - body won't be scrolled.
I didn't find any suitable function, so I made one and wanted some expert opinion on it. Please check the plunkr http://plnkr.co/edit/DNGWLh5cH1Cr1coZbwpa?p=preview . There are problems with animated scroll in FF, so please use Chrome to check the logic.
To illustrate, what I'm looking for - here is the first update that came to mind - if we reached an element that can scroll, lets call it SC (Scroll Parent), we should not only scroll SC to make the target visible inside it, but also recursively scroll SC itself into view, since it may outside of the currently visible are of the page. Here is the update plunkr http://plnkr.co/edit/DNGWLh5cH1Cr1coZbwpa?p=preview (also applied fix for FF scrolling problem).
And here is the code of the function
function scrollTo(target){
//Position delta is used for scrollable elements other than BODY
var combinedPositionDelta = 0;
var previousParent = $(target);
var parent = $(target).parent();
while(parent){
combinedPositionDelta += previousParent.position().top - parent.position().top;
//If we reached body
if(parent.prop("tagName").toUpperCase() == "BODY"){
scrollBody(target.offset().top);
break;
}
//if we reached an element that can scroll
if(parent[0].scrollHeight > parent.outerHeight()){
scrollElementByDelta(parent,combinedPositionDelta);
//Recursively scroll parent into view, since it itself might not be visible
scrollTo(parent);
break;
}
//if we reached a apositioned element - break
if(parent.css('position').toUpperCase() != 'STATIC'){
console.log("Stopping due to positioned parent " + parent[0].outerHTML);
break;
}
previousParent = parent;
parent = parent.parent();
}
}
var offsetSkin = 20;
function scrollElementByDelta(element,offsetDelta){
$(element).animate({
scrollTop: element.scrollTop() + (offsetDelta - offsetSkin)
}, 1000);
}
function scrollBody(offset){
$('body,html').animate({
scrollTop: offset - offsetSkin
}, 1000);
}
Well I'm Using this one which works very well for me:
function scrollIntoView (element, alignTop) {
var document = element.ownerDocument;
var origin = element, originRect = origin.getBoundingClientRect();
var hasScroll = false;
var documentScroll = this.getDocumentScrollElement(document);
while (element) {
if (element == document.body) {
element = documentScroll;
} else {
element = element.parentNode;
}
if (element) {
var hasScrollbar = (!element.clientHeight) ? false : element.scrollHeight > element.clientHeight;
if (!hasScrollbar) {
if (element == documentScroll) {
element = null;
}
continue;
}
var rects;
if (element == documentScroll) {
rects = {
left : 0,
top : 0
};
} else {
rects = element.getBoundingClientRect();
}
// check that elementRect is in rects
var deltaLeft = originRect.left - (rects.left + (parseInt(element.style.borderLeftWidth, 10) | 0));
var deltaRight = originRect.right
- (rects.left + element.clientWidth + (parseInt(element.style.borderLeftWidth, 10) | 0));
var deltaTop = originRect.top - (rects.top + (parseInt(element.style.borderTopWidth, 10) | 0));
var deltaBottom = originRect.bottom
- (rects.top + element.clientHeight + (parseInt(element.style.borderTopWidth, 10) | 0));
// adjust display depending on deltas
if (deltaLeft < 0) {
element.scrollLeft += deltaLeft;
} else if (deltaRight > 0) {
element.scrollLeft += deltaRight;
}
if (alignTop === true && !hasScroll) {
element.scrollTop += deltaTop;
} else if (alignTop === false && !hasScroll) {
element.scrollTop += deltaBottom;
} else {
if (deltaTop < 0) {
element.scrollTop += deltaTop;
} else if (deltaBottom > 0) {
element.scrollTop += deltaBottom;
}
}
if (element == documentScroll) {
element = null;
} else {
// readjust element position after scrolls, and check if vertical scroll has changed.
// this is required to perform only one alignment
var nextRect = origin.getBoundingClientRect();
if (nextRect.top != originRect.top) {
hasScroll = true;
}
originRect = nextRect;
}
}
}
}
I hope this helps.
If you do not mind venturing into jQuery, the scrollTo plugin is the best bet. It handles most needs and gives a very refined smooth trasition.
Hope it helps.
I've this function:
$(function() {
$(window).scroll(function(e) {
var sl = $(this).scrollLeft();
var lbl = $("#waypoints");
if (sl > 0) {
lbl.show('');
}
else {
lbl.hide();
}
if (sl > 750) {
lbl.html("<p>now passed 750</p>");
}
if (sl > 1000) {
lbl.html("<p>now passed 1000</p>");
}
});
});
Which work when i scroll the browser window. But I need to adjust it, so that it is set for a div (#main) which scrolls inside another div (.content) which has a fixed width and css overflow enabled.
I've tried $(#main).scroll(function(e) { no joy ...
thanks for reading, any help would be awesome.
Try to change to class:
$(function() {
$('.content').scroll(function(e) {
var sl = $(this).scrollLeft();
var lbl = $("#waypoints");
if (sl > 0) {
lbl.show();
}
else {
lbl.hide();
}
if (sl > 750) {
lbl.html("<p>now passed 750</p>");
}
if (sl > 1000) {
lbl.html("<p>now passed 1000</p>");
}
});
});
Here's a JSFiddle
I don't see what's the problem here - it's working (I mean the scroll events are fired, as you can see by scrollLeft value that is changing) http://jsfiddle.net/DTKeX/2/