The code below is a javascript-only solution distilled from a couple of sources, which are towards the bottom.
If you are willing to change the structure of your pages, a CSS/HTML only option may work for you.
Additionally, the draft CSS property scroll-boundary-behavior
is in the process of being standardized and added to Chrome provide this capability among a few others. As the implementation is very, very new, I provide links at the bottom of my answer.
although jsfiddle's iframe structure prevents pull-to-refresh from working at all, I also tested the same script within a flat HTML document on Chrome Android 60.0.3112.116.
Full jsfiddle
event.preventDefault()
can keep browser default behaviors such as pull-to-refresh from taking place. We want the usual browser behavior most of the time, just not when it would lead to a pull-to-refresh. Since a pull-to-refresh happens when touches are moving down the screen and we're scrolled to the top of the document, we'll only call preventDefault
under those conditions.
//We're going to make a closure that will handle events
//so as to prevent the pull-to-refresh behavior.
var pullToRefreshPreventer = (function() {
//To determine the direction in which a touch is moving,
//we hold on to a map from touch identifier to touches
//from the previous event.
var previousTouches = {};
return function(event) {
//First we get all touches in this event and set up
//the value which will replace `previousTouches`
//before this event handler exits.
var touches = Array.prototype.slice.call(event.touches);
nextTouches = {}
touches.forEach(function(touch){
nextTouches[touch.identifier] = touch;
});
//Pull-to-refresh behavior only happens if we are
//scrolled to the top of the document, so we can
//exit early if we are somewhere in the middle.
if(document.scrollingElement.scrollTop > 0) {
previousTouches = nextTouches;
return;
}
//Now we know that we are scrolled to the top of
//the document;
//look through the current set of touches and see
//if any of them have moved down the page.
for(var ix = 0; ix < touches.length; ix++) {
var touch = touches[ix],
id = touch.identifier;
//If this touch was captured in a previous event
//and it has moved downwards, we call preventDefault
//to prevent the pull-to-refresh behavior.
if(id in previousTouches && previousTouches[id].screenY < touch.screenY) {
event.preventDefault();
console.log("event.preventDefault() called")
break;
}
}
//lastly, we update previousTouches
previousTouches = nextTouches;
};
}());
//Since touch events which may call `preventDefault` can be
//much more expensive to handle, Chrome disallows such calls
//by default. We must add the options argument `{passive: false}`
//here to make it work.
document.body.addEventListener('touchmove', pullToRefreshPreventer, {passive: false});
document.body.addEventListener('touchend', pullToRefreshPreventer, {passive: false});
References:
StackOverflow answer linking to chromestatus.com page
"Treat Document Level Touch Event Listeners as Passive", chromestatus
"Making touch scrolling fast by default"
"Touch events"
scroll-boundary-behavior
links:
chromestatus
chromium bug
github issue proposing the standard
draft css module, last publish date 2017-09-07