Reputation: 321
I found a simple Codepen which allows me to drag and scroll a gallery with images. It is working fine, but I need a way to add "smooth grabbing/scrolling" to this function. Basically I want to emulate the scroll for example on an iPhone.
Can someone please help me out. I am a total beginner in Javascript. Here is the link to the code: Horizontal Click and Drag Scrolling with JS
const slider = document.querySelector('.items');
let isDown = false;
let startX;
let scrollLeft;
slider.addEventListener('mousedown', (e) => {
isDown = true;
slider.classList.add('active');
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => {
isDown = false;
slider.classList.remove('active');
});
slider.addEventListener('mouseup', () => {
isDown = false;
slider.classList.remove('active');
});
slider.addEventListener('mousemove', (e) => {
if(!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 3; //scroll-fast
slider.scrollLeft = scrollLeft - walk;
console.log(walk);
});
Thanks in advance.
Upvotes: 6
Views: 8256
Reputation: 881
You can try this https://utsb-fmm.github.io/MobileLikeScroller/
More natural scrolling I found.
Here is the code:
class MobileLikeScroller {
constructor(elem,direction='xy') {
this.previousTouchX=[0,0,0];
this.previousTouchY=[0,0,0];
this.previousTouchTime=[0,0,0];
this.direction=direction;
this.scrollAtT0=[0,0];
this.inertialTimerInterval = null;
this.target = elem;
this.childrenEventListeners = [];
this.childEventObject=null;
this.blockChildrenTimeout = null;
this.$BlockedInputs=[];
$(elem).on('mousedown', (e) => this.touchstart(e));
}
touchstart(e) {
if (e.button === 0) { // Check for left click
e.preventDefault();
$(this.target).css('cursor', 'grabbing');
this.previousTouchX = [e.pageX, e.pageX, e.pageX];
this.previousTouchY = [e.pageY, e.pageY, e.pageY];
this.previousTouchTime = [Date.now() - 2, Date.now() - 1, Date.now()];
$(document).on('mousemove.scroller', (e) => this.touchmove(e));
$(document).on('mouseup.scroller', (e) => this.touchend(e));
$(document).on('click.scroller', (e) => this.click(e));
if (this.inertialTimerInterval) {
clearInterval(this.inertialTimerInterval);
this.inertialTimerInterval=null;
}
//The two following lines are for blocking clicks on child items after 300 ms of long press (i.e swiping)
this.childEventObject=null;
this.blockChildrenTimeout = setTimeout(() => { this.preventChildClicks(); },300); // Prevent children from being clicked after 300ms when we are sure that the user is grabbing the parent to scroll
}
}
touchmove(e) {
this.previousTouchX = [this.previousTouchX[1], this.previousTouchX[2], e.pageX];
this.previousTouchY = [this.previousTouchY[1], this.previousTouchY[2], e.pageY];
this.previousTouchTime = [this.previousTouchTime[1], this.previousTouchTime[2], Date.now()];
if(this.direction!='y') this.target.scrollLeft -= this.previousTouchX[2] - this.previousTouchX[1];
if(this.direction!='x') this.target.scrollTop -= this.previousTouchY[2] - this.previousTouchY[1];
if(this.blockChildrenTimeout && (this.previousTouchX[2] - this.previousTouchX[1])**2+(this.previousTouchY[2] - this.previousTouchY[1])**2>25) {
// If fast mouse movement, this is not a click on children, do not wait 300ms
this.preventChildClicks();
}
$(this.target).trigger('scroll');
}
touchend(e) {
$(document).off('mousemove.scroller mouseup.scroller');
$(this.target).css('cursor', '');
this.scrollAtT0 = [$(this.target).scrollLeft(), $(this.target).scrollTop()];
this.inertialTimerInterval = setInterval(() => this.inertialmove(), 16);
$(this.target).trigger('initiateinertial');
}
click(e) { // Click is trigger after mouseup. Parent click is trigger after child click so we cannot remove child click before
$(document).off('click.scroller');
if(this.blockChildrenTimeout===null) {
this.childrenEventListeners.forEach((t) => {
t[0].removeEventListener('click', t[1], true);
}); //,true make this event prioritary
this.childrenEventListeners=[];
setTimeout(() => { // The event for the change is done after the click event, so we need to wait for the click event to be done before re-enabling the inputs
this.$BlockedInputs.prop('disabled',false);
this.$BlockedInputs=[];
},0);
}
else {
clearTimeout(this.blockChildrenTimeout);
this.blockChildrenTimeout=null;
}
}
preventChildClicks() {
$(this.target).find('*').each((i,elem) => {
let listener = (e) => this.childclick(e);
elem.addEventListener('click', listener, true)
this.childrenEventListeners.push([elem,listener]);
});
this.$BlockedInputs=$(this.target).find('input:not(:disabled)').prop('disabled',true);
clearInterval(this.blockChildrenTimeout);
this.blockChildrenTimeout=null;
}
childclick(e) {
e.stopPropagation();
this.click(e);
}
inertialmove() {
var v0X = 0, v0Y = 0;
if(this.direction!='y') v0X = (this.previousTouchX[2] - this.previousTouchX[0]) / (this.previousTouchTime[2] - this.previousTouchTime[0])*1000/$(this.target).width(); // page per second
if(this.direction!='x') v0Y = (this.previousTouchY[2] - this.previousTouchY[0]) / (this.previousTouchTime[2] - this.previousTouchTime[0])*1000/$(this.target).height(); // page per second
var av0 = this.direction=='xy'?Math.sqrt(v0X*v0X+v0Y*v0Y):(this.direction=='y'?Math.abs(v0Y):Math.abs(v0X));
var unitVector = [v0X / av0, v0Y / av0];
av0 = Math.min(12, Math.max(-12, 1.2*av0));
var t = (Date.now() - this.previousTouchTime[2])/1000;
var v = av0 - 14.278 * t + 75.24 * t * t / av0 - 149.72 * t * t * t / av0 / av0; //This is the equation of inertia determined by reverse engineering on chrome. A Clear better experience.
if (av0 == 0 || v <= 0 || isNaN(av0)) {
clearInterval(this.inertialTimerInterval);
this.inertialTimerInterval = null;
$(this.target).trigger('scrollend');
} else {
var deltaX = $(this.target).width()*unitVector[0] * (av0 * t - 7.1397 * t * t + 25.08 * t * t * t / av0 - 37.43 * t * t * t * t / av0 / av0);
var deltaY = $(this.target).height()*unitVector[1] * (av0 * t - 7.1397 * t * t + 25.08 * t * t * t / av0 - 37.43 * t * t * t * t / av0 / av0);
let maxScroll = [this.target.scrollWidth - $(this.target).width(), this.target.scrollHeight - $(this.target).height()];
let newScroll = [Math.min(maxScroll[0],Math.max(0,this.scrollAtT0[0] - deltaX)), Math.min(maxScroll[1],Math.max(0,this.scrollAtT0[1] - deltaY))];
if ((newScroll[0]==0 || newScroll[0]==maxScroll[0]) && (newScroll[1]==0 || newScroll[1]==maxScroll[1])) {
clearInterval(this.inertialTimerInterval);
this.inertialTimerInterval = null;
}
if(this.direction!='y')
this.target.scrollLeft = newScroll[0];
if(this.direction!='x')
this.target.scrollTop = newScroll[1];
$(this.target).trigger('scroll');
}
}
}
Upvotes: -2
Reputation: 263
If I understand your question correctly you'd like to emulate the way that a scrolling item slows to a stop after you release it on iOS.
1) First we need to track it's speed when it's being dragged by adding the following 2 lines to the mousemove
event listener:
var velX;
slider.addEventListener('mousemove', (e) => {
if(!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 3;
// Store the previous scroll position
var prevScrollLeft = slider.scrollLeft;
slider.scrollLeft = scrollLeft - walk;
// Compare change in position to work out drag speed
velX = slider.scrollLeft - prevScrollLeft;
});
2) When the drag is complete we create a frame loop that keeps scrolling at the drag velocity, slowing it down each iteration until it comes to a stop.
slider.addEventListener('mouseup', () => {
isDown = false;
slider.classList.remove('active');
beginMomentumTracking(); // Start a frame loop to continue drag momentum
});
// Momentum
var momentumID;
function beginMomentumTracking(){
cancelMomentumTracking();
momentumID = requestAnimationFrame(momentumLoop);
}
function cancelMomentumTracking(){
cancelAnimationFrame(momentumID);
}
function momentumLoop(){
slider.scrollLeft += velX; // Apply the velocity to the scroll position
velX *= 0.95; // Slow the velocity slightly
if (Math.abs(velX) > 0.5){ // Still moving?
momentumID = requestAnimationFrame(momentumLoop); // Keep looping
}
}
3) Finally cancel the momentum loop when the user begins to interact with the scroll item
slider.addEventListener('mousedown', (e) => {
isDown = true;
slider.classList.add('active');
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
cancelMomentumTracking(); // Stop the drag momentum loop
});
// Listen for mouse wheel events
slider.addEventListener('wheel', (e) => {
cancelMomentumTracking(); // Stop the drag momentum loop
});
See it working here:
https://codepen.io/loxks/details/KKpVvVW
Upvotes: 17