I created a website with jQueryMobile for iOS and Android.
I don\'t want the document itself to scroll. Instead, just an area (a
Here is a solution that uses jQuery for the events.
var stuff = {};
$('#scroller').on('touchstart',stuff,function(e){
e.data.max = this.scrollHeight - this.offsetHeight;
e.data.y = e.originalEvent.pageY;
}).on('touchmove',stuff,function(e){
var dy = e.data.y - e.originalEvent.pageY;
// if scrolling up and at the top, or down and at the bottom
if((dy < 0 && this.scrollTop < 1)||(dy > 0 && this.scrollTop >= e.data.max)){
e.preventDefault();
};
});
Maybe I misunderstood the question, but if I'm correct:
You want not to be able to scroll except a certain element so you:
$(document).bind("touchmove", function(e){
e.preventDefault();
});
Prevent everything within the document.
Why don't you just stop the event bubbling on the element where you wish to scroll? (PS: you don't have to prevent touchstart -> if you use touch start for selecting elements instead of clicks that is prevented as well, touch move is only needed because then it is actually tracing the movement)
$('#element').on('touchmove', function (e) {
e.stopPropagation();
});
Now on the element CSS
#element {
overflow-y: scroll; // (vertical)
overflow-x: hidden; // (horizontal)
}
If you are on a mobile device, you can even go a step further. You can force hardware accelerated scrolling (though not all mobile browsers support this);
Browser Overflow scroll:
Android Browser Yes
Blackberry Browser Yes
Chrome for Mobile Yes
Firefox Mobile Yes
IE Mobile Yes
Opera Mini No
Opera Mobile Kinda
Safari Yes
#element.nativescroll {
-webkit-overflow-scrolling: touch;
}
normal:
<div id="element"></div>
native feel:
<div id="element" class="nativescroll"></div>
In my case, I have a scrollable body and a scrollable floating menu over it. Both have to be scrollable, but I had to prevent body scrolling when "floating menu" (position:fixed) received touch events and was scrolling and it reached top or bottom. By default browser then started to scroll the body.
I really liked jimmont's answer, but unfortunatelly it did not work well on all devices and browsers, especially with a fast and long swipe.
I ended up using MOMENTUM SCROLLING USING JQUERY (hnldesign.nl) on floating menu, which prevents default browser scrolling and then animates scrolling itself. I include that code here for completeness:
/**
* jQuery inertial Scroller v1.5
* (c)2013 hnldesign.nl
* This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/.
**/
/*jslint browser: true*/
/*global $, jQuery*/
/* SETTINGS */
var i_v = {
i_touchlistener : '.inertialScroll', // element to monitor for touches, set to null to use document. Otherwise use quotes. Eg. '.myElement'. Note: if the finger leaves this listener while still touching, movement is stopped.
i_scrollElement : '.inertialScroll', // element (class) to be scrolled on touch movement
i_duration : window.innerHeight * 1.5, // (ms) duration of the inertial scrolling simulation. Devices with larger screens take longer durations (phone vs tablet is around 500ms vs 1500ms). This is a fixed value and does not influence speed and amount of momentum.
i_speedLimit : 1.2, // set maximum speed. Higher values will allow faster scroll (which comes down to a bigger offset for the duration of the momentum scroll) note: touch motion determines actual speed, this is just a limit.
i_handleY : true, // should scroller handle vertical movement on element?
i_handleX : true, // should scroller handle horizontal movement on element?
i_moveThreshold : 100, // (ms) determines if a swipe occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger inertial scrolling
i_offsetThreshold : 30, // (px) determines, together with i_offsetThreshold if a swipe occurred: if calculated offset is above this threshold
i_startThreshold : 5, // (px) how many pixels finger needs to move before a direction (horizontal or vertical) is chosen. This will make the direction detection more accurate, but can introduce a delay when starting the swipe if set too high
i_acceleration : 0.5, // increase the multiplier by this value, each time the user swipes again when still scrolling. The multiplier is used to multiply the offset. Set to 0 to disable.
i_accelerationT : 250 // (ms) time between successive swipes that determines if the multiplier is increased (if lower than this value)
};
/* stop editing here */
//set some required vars
i_v.i_time = {};
i_v.i_elem = null;
i_v.i_elemH = null;
i_v.i_elemW = null;
i_v.multiplier = 1;
// Define easing function. This is based on a quartic 'out' curve. You can generate your own at http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
if ($.easing.hnlinertial === undefined) {
$.easing.hnlinertial = function (x, t, b, c, d) {
"use strict";
var ts = (t /= d) * t, tc = ts * t;
return b + c * (-1 * ts * ts + 4 * tc + -6 * ts + 4 * t);
};
}
$(i_v.i_touchlistener || document)
.on('touchstart touchmove touchend', function (e) {
"use strict";
//prevent default scrolling
e.preventDefault();
//store timeStamp for this event
i_v.i_time[e.type] = e.timeStamp;
})
.on('touchstart', function (e) {
"use strict";
this.tarElem = $(e.target);
this.elemNew = this.tarElem.closest(i_v.i_scrollElement).length > 0 ? this.tarElem.closest(i_v.i_scrollElement) : $(i_v.i_scrollElement).eq(0);
//dupecheck, optimizes code a bit for when the element selected is still the same as last time
this.sameElement = i_v.i_elem ? i_v.i_elem[0] == this.elemNew[0] : false;
//no need to redo these if element is unchanged
if (!this.sameElement) {
//set the element to scroll
i_v.i_elem = this.elemNew;
//get dimensions
i_v.i_elemH = i_v.i_elem.innerHeight();
i_v.i_elemW = i_v.i_elem.innerWidth();
//check element for applicable overflows and reevaluate settings
this.i_scrollableY = !!((i_v.i_elemH < i_v.i_elem.prop('scrollHeight') && i_v.i_handleY));
this.i_scrollableX = !!((i_v.i_elemW < i_v.i_elem.prop('scrollWidth') && i_v.i_handleX));
}
//get coordinates of touch event
this.pageY = e.originalEvent.touches[0].pageY;
this.pageX = e.originalEvent.touches[0].pageX;
if (i_v.i_elem.is(':animated') && (i_v.i_time.touchstart - i_v.i_time.touchend) < i_v.i_accelerationT) {
//user swiped while still animating, increase the multiplier for the offset
i_v.multiplier += i_v.i_acceleration;
} else {
//else reset multiplier
i_v.multiplier = 1;
}
i_v.i_elem
//stop any animations still running on element (this enables 'tap to stop')
.stop(true, false)
//store current scroll positions of element
.data('scrollTop', i_v.i_elem.scrollTop())
.data('scrollLeft', i_v.i_elem.scrollLeft());
})
.on('touchmove', function (e) {
"use strict";
//check if startThreshold is met
this.go = (Math.abs(this.pageX - e.originalEvent.touches[0].pageX) > i_v.i_startThreshold || Math.abs(this.pageY - e.originalEvent.touches[0].pageY) > i_v.i_startThreshold);
})
.on('touchmove touchend', function (e) {
"use strict";
//check if startThreshold is met
if (this.go) {
//set animpar1 to be array
this.animPar1 = {};
//handle events
switch (e.type) {
case 'touchmove':
this.vertical = Math.abs(this.pageX - e.originalEvent.touches[0].pageX) < Math.abs(this.pageY - e.originalEvent.touches[0].pageY); //find out in which direction we are scrolling
this.distance = this.vertical ? this.pageY - e.originalEvent.touches[0].pageY : this.pageX - e.originalEvent.touches[0].pageX; //determine distance between touches
this.acc = Math.abs(this.distance / (i_v.i_time.touchmove - i_v.i_time.touchstart)); //calculate acceleration during movement (crucial)
//determine which property to animate, reset animProp first for when no criteria is matched
this.animProp = null;
if (this.vertical && this.i_scrollableY) { this.animProp = 'scrollTop'; } else if (!this.vertical && this.i_scrollableX) { this.animProp = 'scrollLeft'; }
//set animation parameters
if (this.animProp) { this.animPar1[this.animProp] = i_v.i_elem.data(this.animProp) + this.distance; }
this.animPar2 = { duration: 0 };
break;
case 'touchend':
this.touchTime = i_v.i_time.touchend - i_v.i_time.touchmove; //calculate touchtime: the time between release and last movement
this.i_maxOffset = (this.vertical ? i_v.i_elemH : i_v.i_elemW) * i_v.i_speedLimit; //(re)calculate max offset
//calculate the offset (the extra pixels for the momentum effect
this.offset = Math.pow(this.acc, 2) * (this.vertical ? i_v.i_elemH : i_v.i_elemW);
this.offset = (this.offset > this.i_maxOffset) ? this.i_maxOffset : this.offset;
this.offset = (this.distance < 0) ? -i_v.multiplier * this.offset : i_v.multiplier * this.offset;
//if the touchtime is low enough, the offset is not null and the offset is above the offsetThreshold, (re)set the animation parameters to include momentum
if ((this.touchTime < i_v.i_moveThreshold) && this.offset !== 0 && Math.abs(this.offset) > (i_v.i_offsetThreshold)) {
if (this.animProp) { this.animPar1[this.animProp] = i_v.i_elem.data(this.animProp) + this.distance + this.offset; }
this.animPar2 = { duration: i_v.i_duration, easing : 'hnlinertial', complete: function () {
//reset multiplier
i_v.multiplier = 1;
}};
}
break;
}
// run the animation on the element
if ((this.i_scrollableY || this.i_scrollableX) && this.animProp) {
i_v.i_elem.stop(true, false).animate(this.animPar1, this.animPar2);
}
}
});
Another observation: I also tried various combinations of e.stopPropagation() on menu div and e.preventDefault() on window/body at touchmove event, but without success, I only managed to prevent scrolling I wanted and not scrolling I did not want. I also tried to have a div over whole document, with z-index between document and menu, visible only between touchstart and touchend, but it did not receive touchmove event (because it was under menu div).
Here is my implementation which works on touch devices and laptops.
function ScrollManager() {
let startYCoord;
function getScrollDiff(event) {
let delta = 0;
switch (event.type) {
case 'mousewheel':
delta = event.wheelDelta ? event.wheelDelta : -1 * event.deltaY;
break;
case 'touchstart':
startYCoord = event.touches[0].clientY;
break;
case 'touchmove': {
const yCoord = event.touches[0].clientY;
delta = yCoord - startYCoord;
startYCoord = yCoord;
break;
}
}
return delta;
}
function getScrollDirection(event) {
return getScrollDiff(event) >= 0 ? 'UP' : 'DOWN';
}
function blockScrollOutside(targetElement, event) {
const { target } = event;
const isScrollAllowed = targetElement.contains(target);
const isTouchStart = event.type === 'touchstart';
let doScrollBlock = !isTouchStart;
if (isScrollAllowed) {
const isScrollingUp = getScrollDirection(event) === 'UP';
const elementHeight = targetElement.scrollHeight - targetElement.offsetHeight;
doScrollBlock =
doScrollBlock &&
((isScrollingUp && targetElement.scrollTop <= 0) ||
(!isScrollingUp && targetElement.scrollTop >= elementHeight));
}
if (doScrollBlock) {
event.preventDefault();
}
}
return {
blockScrollOutside,
getScrollDirection,
};
}
const scrollManager = ScrollManager();
const testBlock = document.body.querySelector('.test');
function handleScroll(event) {
scrollManager.blockScrollOutside(testBlock, event);
}
window.addEventListener('scroll', handleScroll);
window.addEventListener('mousewheel', handleScroll);
window.addEventListener('touchstart', handleScroll);
window.addEventListener('touchmove', handleScroll);
.main {
border: 1px solid red;
height: 200vh;
}
.test {
border: 1px solid green;
height: 300px;
width: 300px;
overflow-y: auto;
position: absolute;
top: 100px;
left: 50%;
}
.content {
height: 100vh;
}
<div class="main">
<div class="test">
<div class="content"></div>
</div>
</div>
I was able to disable scrolling of the main document by adding css "overflow-y: hidden" on HTML.
It did not mess with positioning at all.
Here is a solution I am using:
$scrollElement is the scroll element, $scrollMask is a div with style position: fixed; top: 0; bottom: 0;
.
The z-index
of $scrollMask is smaller than $scrollElement.
$scrollElement.on('touchmove touchstart', function (e) {
e.stopPropagation();
});
$scrollMask.on('touchmove', function(e) {
e.stopPropagation();
e.preventDefault();
});