iScroll 4 not scrolling on input elements - javascript

I'm building a mobile app that targets Android 2.2+, Blackberry 9+ and iOS 4+. In our stack we are using Phonegap, jQuery and iScroll (amongst others).
One of our app screens looks like this (text redacted for anonymity) - running in the iOS 5 simulator's Safari.
As you can see, this is a typical input screen with a fixed header, and multiple block-level form elements stretching the full with of the device screen, less padding.
As I mentioned, our app uses iScroll. We're initialising it on this page using the following code (taken from the iScroll 'forms' sample).
// ...
window.scroller = new iScroll(id, {
useTransform: false,
onBeforeScrollStart: function(e) {
var target = e.target;
while (target.nodeType != 1) target = target.parentNode;
if(target.tagName != 'select'
&& target.tagName != 'input'
&& target.tagName != 'textarea') {
e.preventDefault();
}
}
});
// Disable touch scrolling (Req'd for iScroll)
window.document.addEventListener('touchmove', function(e) {
e.preventDefault();
}, false);
// ...
On this screen, I've noticed that the content scrolls fine when the user touches any part of the background, but doesn't scroll when you begin the scroll gesture (swiping up or down) by touching one of the input elements. As you can understand this essentially makes this screen impossible to use, hence me looking for a fix.
I've since found the culprit line in iScroll;
if (nodeName == "TEXTAREA" || nodeName == "INPUT" || nodeName == "SELECT" ) return;
(179 in iscroll.js), an open issue for this bug with a claimed fix, and one pull request that claims to fix it, however the author of the bug seems to have incorrect line numbers, preventing me from attempting that fix, and the mentioned pull request doesn't work for me (tested on iOS 5.1, Android 4.0.4).
My question - is there some way to allow a user to scroll (using iScroll) when touching an input element? If not, iScroll is completely useless in cases like this. At the moment, I'm looking at either
Using the Overthrow shim that claims to have identical functionality to iScroll - however this isn't a great option due to various issues with Android - one of our key platforms.
Doing away with iScroll and loosing my fixed header.
It's 2012 - do we still not have a way to do this on Mobile browsers?!?

I realize this ticket is a bit old but I ran into it while dealing with the same issue.
I am using iScroll v4 on iOS.
I found this solution (somewhere) to add the following when setting up the iScroll object:
myScroll = new iScroll(id, {
useTransform: false,
onBeforeScrollStart: function (e) {
var target = e.target;
while (target.nodeType != 1) target = target.parentNode;
if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA') {
e.preventDefault();
}
}
});
However I found 2 issues with this code:
a) The useTransform broke layout for a form where I was auto hiding radio buttons in order to show "pretty graphic" ones as per this page (http://webdesign.tutsplus.com/tutorials/htmlcss-tutorials/quick-tip-easy-css3-checkboxes-and-radio-buttons/). I don't know why this would break that layout and possibly it was only a partial relationship I could have fixed another way but...I commented out the useTransform and it fixed it so...
b) For inputs that pulled up the virtual keyboard, the page stayed "scrolled up" (header off screen and footer about 1/4 up from bottom ) after the keyboard was hidden, so I added and "onBlur" event to "rescroll" the window and that seemed to work so... here is my final solution.
myScroll = new iScroll(id, {
//useTransform: false,
onBeforeScrollStart: function (e) {
var target = e.target;
while (target.nodeType != 1) target = target.parentNode;
if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA') {
e.preventDefault();
} else {
$(target).bind('blur', function(){
window.scrollTo(0,0);
myScroll.refresh();
});
}
}
});
Hope this helps!!!

I had exactly the same problem and used the following when I set up my iScroll:
var ISCROLL_MOVE;
var ISCROLL_MOVE_LIMIT=10;
// ... functions to include when you set up your iScroll
onScrollStart: function(e) {
ISCROLL_MOVE=0;
},
onScrollMove: function(e) {
ISCROLL_MOVE_LIMIT++;
}
Then when you have any form elements in your iScroll, e.g.:
var selectField = document.getElementById('mySelectField');
selectField.addEventListener('touchend' /*'mousedown'*/, function(e) {
if (SCROLL_MOVE<SCROLL_MOVE_LIMIT)
this.focus();
}, false);
Note the action is on a touch end event, and it allows for scrolling iScroll pages when the user touches on a form element - basically it measures the amount of scrolling ( or not ) which the user is making ( the amount of SCROLL_MOVE ). If its more than 10 ( the SCROLL_MOVE_LIMIT seems like a good number to me ) then the field won't grab focus , otherwise it will.
Let me know if you need more detail.

Remove the preventDefault():
window.scroller = new iScroll(id, {
useTransform: false,
onBeforeScrollStart: function(e) {
var target = e.target;
while (target.nodeType != 1) target = target.parentNode;
if(target.tagName != 'select'
&& target.tagName != 'input'
&& target.tagName != 'textarea') {
// remove this code right here:
e.preventDefault();
}
}
});

i'd answer about it here, without java-script and iscroll hacks
https://stackoverflow.com/a/17390516/1458628

I solved it perfectly. If you are using iscroll.js, then you can solve it easily by editing the iscroll.js file.
I edited the _move(e) function definition.
You will see the below code in the function definition of _move(e):
enter code here
that.moved = true;
that._pos(newX, newY);
that.dirX = deltaX > 0 ? -1 : deltaX < 0 ? 1 : 0;
that.dirY = deltaY > 0 ? -1 : deltaY < 0 ? 1 : 0;
if (timestamp - that.startTime > 300) {
that.startTime = timestamp;
that.startX = that.x;
that.startY = that.y;
}
if (that.options.onScrollMove) that.options.onScrollMove.call(that, e);
//Enclose the above code in the else condition. And the if condition should be below:
enter code here
if (e.srcElement.localName == 'textarea') {
var scrollHeight = e.srcElement.scrollHeight;
var scrollTop = e.srcElement.scrollTop;
if (scrollTop >= 0 && screenTop <= scrollHeight) {
that.dirY = that.dirY * 5;
e.srcElement.scrollTop = scrollTop + that.dirY;
}
}
//Tested in IPad. Works fine for me.

Related

Force Vertical Scroll User Action to Manifest as Horizontal Scroll

I am working on a portfolio site that is designed to scroll horizontally when the user scrolls vertically. This CSS (https://css-tricks.com/examples/HorzScrolling/) does NOT work for me. I can only find answers or fiddles that create a horizontal scrollbar, which is not what I am looking for.
My css rule looks like this:
.container-scroll {
width: auto;
min-width: 100px;
overflow-x: auto;
position: relative;
min-height: 100%;
}
I am looking to emulate the horizontal scroll functionality of this site: http://yearinreview.hshtags.com/
Made this a long time ago, it translates your mouse scrolling into a horizontal scroll and includes kinetic scrolling and adaptable to most broswers..
Just add what scrolling element you want into the bottom:
var hs = new HorizontalScroll(document.body);
instead of document.body
study it, learn from it ;)
Codepin
To explain a little more:
There are many different mouse scroll events based on which browser is currently being used:
mousewheel (chrome)
wheel (IE)
DOMMouseScroll (firefox)
so to detect which one exists an attachment check is made with the element in question and then the function for the scroll event or scrollEv is attached, we will call this element elq:
var elq = document.getElementById('elementinquestion');
switch('object')
{
case typeof elq.onmousewheel:
elq.onmousewheel = scrollEv;
break;
case typeof elq.onwheel:
elq.onwheel = scrollEv;
break;
case typeof elq.onDOMMouseScroll:
elq.onDOMMouseScroll = scrollEv;
break;
case typeof elq.DOMMouseScroll:
elq.DOMMouseScroll = scrollEv;
break;
}
Once we have attached to the correct mousewheel event we can start our logic by reading the event object passed into the function:
var scrollEv = function(eventObject)
{
eventObject.preventDefault(); //prevent default scrolling action
}
the properties in the event object we are looking for will also depend on the browser, also the values will differ between firefox and chrome/IE/Safari.
eventObject.wheelDelta (IE/chrome/Safari)
eventObject.deltaY (Firefox)
eventObject.detail (old IE)
to balance all of these so they come around similiar values for the scrolling to be fluid requires some math.. with wheelDelta we need to devide by 60 as this value will be high, with deltaY this value will be at the opposite spectrum.. so must do a inverse on it by multiplying by -1, detail just needs to be inversed and divided by 2, EX:
var delta = 0;
if (eventObject == null)
{
eventObject = window.event;
}
if (typeof eventObject.wheelDelta != 'undefined')
{
if(eventObject.wheelDelta < -50)
{
eventObject.wheelDelta = -30;
}
else if(eventObject.wheelDelta > 50)
{
eventObject.wheelDelta = 30;
}
delta = eventObject.wheelDelta/60;
}
else if(typeof eventObject.deltaY != 'undefined')
{
if(eventObject.deltaY < -50)
{
eventObject.deltaY = -30;
}
else if(eventObject.deltaY > 50)
{
eventObject.deltaY = 30;
}
delta = eventObject.deltaY*(-1);
}
else if (typeof eventObject.detail != 'undefined')
{
delta = -eventObject.detail/2;
}
now we can use this value to set our scrolling. our scrolling comes from 2 different properties in the element, also based on browser:
elq.scrollLeft
elq.offsetLeft
to move the element in question we will simply take our ending value of delta and add it to the scrollLeft:
if(typeof elq.scrollLeft != 'undefined')
{
elq.scrollLeft += delta;
}
else if(elq.offsetLeft)
{
elq.scrollLeft += delta;
}
and thats it, a cross browser scroll system that overwrites the default to horizontal :)

Disable web page navigation on swipe(back and forward)

On a Windows phone, in IE users can go back and forward by swiping on the screen if the swipe is coming from the edge. This OS level functionality is hampering my webpage's UX.
Is there any js or css which can disable that? Some hack would also do.
A snapshot from windowsphone's website:
Here is the link to the reference page: http://www.windowsphone.com/en-in/how-to/wp8/basics/gestures-swipe-pan-and-stretch
Please note that I still need touchaction enabled for horizontal scrolling.
You Should Try this solution in two way :
1) CSS only fix for Chrome/Firefox
html, body {
overscroll-behavior-x: none;
}
2) JavaScript fix for Safari
if (window.safari) {
history.pushState(null, null, location.href);
window.onpopstate = function(event) {
history.go(1);
};
}
Over time, Safari will implement overscroll-behavior-x and we'll be able to remove the JS hack
Possible duplicate of iOS 7 - is there a way to disable the swipe back and forward functionality in Safari?
Copy and paste this JavaScript:
var xPos = null;
var yPos = null;
window.addEventListener( "touchmove", function ( event ) {
var touch = event.originalEvent.touches[ 0 ];
oldX = xPos;
oldY = yPos;
xPos = touch.pageX;
yPos = touch.pageY;
if ( oldX == null && oldY == null ) {
return false;
}
else {
if ( Math.abs( oldX-xPos ) > Math.abs( oldY-yPos ) ) {
event.preventDefault();
return false;
}
}
} );
If you want it minified, copy and paste this:
var xPos=null;var yPos=null;window.addEventListener("touchmove",function(event){var touch=event.originalEvent.touches[0];oldX=xPos;oldY=yPos;xPos=touch.pageX;yPos=touch.pageY;if(oldX==null && oldY==null){return false;}else{if(Math.abs(oldX-xPos)>Math.abs(oldY-yPos)){event.preventDefault();return false;}}});
How about preventing the default action of the swipe event. Somewhere in your document.ready add (note I've included the document.ready in this example, only the function needs to be added):
$(document).ready(function(){
$(window).on('touchmove',function(e){e.preventDefault();});
});
In this case I believe the event is called 'touchmove'you may need to extend this to also ignore default behavior of touchstart/touchend but I'm not 100% sure.
Check out this Codepen by David Hill.
var elem = document.getElementById("container"); //area we don't want touch drag
var defaultPrevent=function(e){e.preventDefault();}
elem.addEventListener("touchstart", defaultPrevent);
elem.addEventListener("touchmove" , defaultPrevent);
I actually think this is a cool way to build objects too.
This is also a problem for Mac users who have configured swipe-left to navigate backwards. You can't disable this setting, but you can prompt the user to confirm that they intended to navigate away https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload.
*{
-webkit-touch-callout: none;
}
The result is to completely deactivate any touch events

JQuery / JS : Detect user's scroll attempt without any window overflow to scroll to

I'm working on a transitioning website, and while I want to use the user's scroll attempt as the transition initiator, I don't want there to be a window scroll bar.
Right now, I'm simply detecting that the user scrolls (I've made my window size 1px taller that the user's screen for a scrollbar, although this is what I'm trying to avoid) with jquery's
.scroll(function)
method, and using that to transition my page, but I'd like to detect the user's scroll attempt without having to make my page overflow by a pixel, and thus showing the scrollbar
How can this be done?
Messy patch possibility that I know of:
Positioning the window inside an outer wrapper and hiding the scrollbar in the overflow of the wrapper. This is a patch up job and not a solution. It results in the page content being off-center since not all browsers use the same width for their scrollbars.
Have a look at this question. I used it as a reference to make this fiddle.
Works only in Firefox:
$('html').on ('DOMMouseScroll', function (e) {
var delta = e.originalEvent.detail;
if (delta < 0) {
$('p').text ('You scrolled up');
} else if (delta > 0) {
$('p').text ('You scrolled down');
}
});
Works in Chrome, IE, Opera and Safari:
$('html').on ('mousewheel', function (e) {
var delta = e.originalEvent.wheelDelta;
if (delta < 0) {
$('p').text ('You scrolled down');
} else if (delta > 0) {
$('p').text ('You scrolled up');
}
});
You'd have to bind it on an element that spans your entire browser screen.
The answer from TreeTree can be simplified into one function that supports all browsers. Combine the mousewheel and DOMMouseScroll events since jQuery supports one or more event parameter(s). However, my testing showed on() did not work in the latest Firefox. Use bind() instead. You can also combine the var delta declaration to support all browsers:
$('html').bind('mousewheel DOMMouseScroll', function (e) {
var delta = (e.originalEvent.wheelDelta || -e.originalEvent.detail);
if (delta < 0) {
console.log('You scrolled down');
} else if (delta > 0) {
console.log('You scrolled up');
}
});
The modern solution relies on the wheel event (IE9+)
$('selector').on('wheel', function(event) {
var delta = {
x: event.originalEvent.deltaX,
y: event.originalEvent.deltaY
};
if (delta.x != 0 || delta.y != 0) {
//user scrolled
}
});

Integrating a Java based page key scrolling control in a page

I've created a site with a fixed header. I've discovered this causes one issue
when someone hits the page down/up key, the length of that scroll is too long
due to it not remving the height of the header (and a very small bit of padding below it) from the scroll length. So (for example), if you're at the beginning of the page and hit "page down", you'd have to manually scroll back up a bit to match where you previously left off and not miss any content.
I found what I thought was the solution to this problem in this Java based page
scroll control:
https://stackoverflow.com/a/6395433/1858759
http://jsfiddle.net/bpMCE/
It worked well enough in the demo page. However, no matter what I do (which given my beginner skill level in this sort of thing), I can't get it to control my pages. I had one other person take a look at it and offer suggestions, but none of them solved the problem. One thing he did do was adjust the "bar" content vs the original Javascript code. I've pasted this revised code below, to compare to the original linked above.
My pages with actual content are not hosted yet. A friend has hosted a "dummy" page I made with generic content but the same code as some of my other pages (I'm not quite ready to have the content public). Here's the link:
http://www.11fifty.com/Site_108/before.html
I'm totally stumped with this. I've found some great advice here from reading the archives as needed, so I hope someone can make sense of this. In addition, I hope it will help others that may want to correct for this in their own fixed header sites.
Thanks in advance...
(function(){
var content, header
function adjustScroll(event) {
var e, key, remainingSpace;
content = content || document.getElementById('content');
header = header || document.getElementById('header');
e = event || window.event;
key = e.which || e.keyCode;
if ( key === 33 ) { // Page up
remainingSpace = content.scrollHeight - content.scrollTop;
setTimeout(function () {
content.scrollTop = (remainingSpace >= content.scrollHeight -
header.offsetHeight) ? 0 : (content.scrollTop + header.offsetHeight);
}, 10);
}
if ( key === 34 ) { // Page down
remainingSpace = content.scrollHeight - content.scrollTop -
content.offsetHeight;
setTimeout(function () {
content.scrollTop = (remainingSpace <= header.offsetHeight) ?
content.scrollHeight : (content.scrollTop - header.offsetHeight);
}, 10);
}
}
document.onkeydown = adjustScroll;
}());
What you need to do is to add a class tag (let's name it class="new") to every item on your list and handle keypress events to scroll to next item or the previous one, this code may help you :
function scrollToNew () {
scrollTop = $(window).scrollTop();
$('.new').each(function(i, h2){ // loop through article headings
h2top = $(h2).offset().top; // get article heading top
if (scrollTop < h2top) { // compare if document is below heading
$.scrollTo(h2, 800); // scroll to in .8 of a second
return false; // exit function
}
});
}
jQuery(function () {
$("#next").click(scrollToNew);
$(document).keydown(function (evt) {
if (evt.keyCode == 40) { // down arrow
evt.preventDefault(); // prevents the usual scrolling behaviour
scrollToNew(); // scroll to the next new heading instead
} else if (evt.keyCode == 38) { // up arrow
evt.preventDefault();
scrollToLast();
}
}
});
More details : https://stackoverflow.com/a/2168876/2310699
In your sample page the class of the header is header not the id, therefore this is not working:
document.getElementById('header')
This goes for the content too. Change these 2 rows:
<div class="container">
<div class="header">
to
<div id="container">
<div id="header">
Why you are at it add a semi-colon to the end of this row:
var content, header
If this is not the case, then create your own jsfiddle to show your exact code.

Prevent scrolling of parent element when inner element scroll position reaches top/bottom?

I have a little "floating tool box" - a div with position:fixed; overflow:auto.
Works just fine.
But when scrolling inside that box (with the mouse wheel) and reaching the bottom OR top, the parent element "takes over" the "scroll request" : The document behind the tool box scrolls.
- Which is annoying and not what the user "asked for".
I'm using jQuery and thought I could stop this behaviour with event.stoppropagation():
$("#toolBox").scroll( function(event){ event.stoppropagation() });
It does enter the function, but still, propagation happens anyway (the document scrolls)
- It's surprisingly hard to search for this topic on SO (and Google), so I have to ask:
How to prevent propagation / bubbling of the scroll-event ?
Edit:
Working solution thanks to amustill (and Brandon Aaron for the mousewheel-plugin here:
https://github.com/brandonaaron/jquery-mousewheel/raw/master/jquery.mousewheel.js
$(".ToolPage").bind('mousewheel', function(e, d)
var t = $(this);
if (d > 0 && t.scrollTop() === 0) {
e.preventDefault();
}
else {
if (d < 0 && (t.scrollTop() == t.get(0).scrollHeight - t.innerHeight())) {
e.preventDefault();
}
}
});
I am adding this answer for completeness because the accepted answer by #amustill does not correctly solve the problem in Internet Explorer. Please see the comments in my original post for details. In addition, this solution does not require any plugins - only jQuery.
In essence, the code works by handling the mousewheel event. Each such event contains a wheelDelta equal to the number of px which it is going to move the scrollable area to. If this value is >0, then we are scrolling up. If the wheelDelta is <0 then we are scrolling down.
FireFox: FireFox uses DOMMouseScroll as the event, and populates originalEvent.detail, whose +/- is reversed from what is described above. It generally returns intervals of 3, while other browsers return scrolling in intervals of 120 (at least on my machine). To correct, we simply detect it and multiply by -40 to normalize.
#amustill's answer works by canceling the event if the <div>'s scrollable area is already either at the top or the bottom maximum position. However, Internet Explorer disregards the canceled event in situations where the delta is larger than the remaining scrollable space.
In other words, if you have a 200px tall <div> containing 500px of scrollable content, and the current scrollTop is 400, a mousewheel event which tells the browser to scroll 120px further will result in both the <div> and the <body> scrolling, because 400 + 120 > 500.
So - to solve the problem, we have to do something slightly different, as shown below:
The requisite jQuery code is:
$(document).on('DOMMouseScroll mousewheel', '.Scrollable', function(ev) {
var $this = $(this),
scrollTop = this.scrollTop,
scrollHeight = this.scrollHeight,
height = $this.innerHeight(),
delta = (ev.type == 'DOMMouseScroll' ?
ev.originalEvent.detail * -40 :
ev.originalEvent.wheelDelta),
up = delta > 0;
var prevent = function() {
ev.stopPropagation();
ev.preventDefault();
ev.returnValue = false;
return false;
}
if (!up && -delta > scrollHeight - height - scrollTop) {
// Scrolling down, but this will take us past the bottom.
$this.scrollTop(scrollHeight);
return prevent();
} else if (up && delta > scrollTop) {
// Scrolling up, but this will take us past the top.
$this.scrollTop(0);
return prevent();
}
});
In essence, this code cancels any scrolling event which would create the unwanted edge condition, then uses jQuery to set the scrollTop of the <div> to either the maximum or minimum value, depending on which direction the mousewheel event was requesting.
Because the event is canceled entirely in either case, it never propagates to the body at all, and therefore solves the issue in IE, as well as all of the other browsers.
I have also put up a working example on jsFiddle.
All the solutions given in this thread don't mention an existing - and native - way to solve this problem without reordering DOM and/or using event preventing tricks. But there's a good reason: this way is proprietary - and available on MS web platform only. Quoting MSDN:
-ms-scroll-chaining property - specifies the scrolling behavior that occurs when a user hits the scroll limit during a manipulation. Property values:
chained - Initial value. The nearest scrollable parent element begins scrolling when the user hits a scroll limit during a manipulation. No bounce effect is shown.
none - A bounce effect is shown when the user hits a scroll limit during a manipulation.
Granted, this property is supported on IE10+/Edge only. Still, here's a telling quote:
To give you a sense of how popular preventing scroll chaining may be,
according to my quick http-archive search "-ms-scroll-chaining: none"
is used in 0.4% of top 300K pages despite being limited in
functionality and only supported on IE/Edge.
And now good news, everyone! Starting from Chrome 63, we finally have a native cure for Blink-based platforms too - and that's both Chrome (obviously) and Android WebView (soon).
Quoting the introducing article:
The overscroll-behavior property is a new CSS feature that controls
the behavior of what happens when you over-scroll a container
(including the page itself). You can use it to cancel scroll chaining,
disable/customize the pull-to-refresh action, disable rubberbanding
effects on iOS (when Safari implements overscroll-behavior), and more.[...]
The property takes three possible values:
auto - Default. Scrolls that originate on the element may propagate to
ancestor elements.
contain - prevents scroll chaining. Scrolls do not
propagate to ancestors but local effects within the node are shown.
For example, the overscroll glow effect on Android or the
rubberbanding effect on iOS which notifies the user when they've hit a
scroll boundary. Note: using overscroll-behavior: contain on the html
element prevents overscroll navigation actions.
none - same as contain but it also prevents overscroll effects within the node itself (e.g. Android overscroll glow or iOS rubberbanding).
[...] The best part is that using overscroll-behavior does not adversely
affect page performance like the hacks mentioned in the intro!
Here's this feature in action. And here's corresponding CSS Module document.
UPDATE: Firefox, since version 59, has joined the club, and MS Edge is expected to implement this feature in version 18. Here's the corresponding caniusage.
UPDATE 2: And now (Oct, 2022) Safari officially joined the club: since 16.0 version, overscroll-behavior is no longer behind the feature flag.
It's possible with the use of Brandon Aaron's Mousewheel plugin.
Here's a demo: http://jsbin.com/jivutakama/edit?html,js,output
$(function() {
var toolbox = $('#toolbox'),
height = toolbox.height(),
scrollHeight = toolbox.get(0).scrollHeight;
toolbox.bind('mousewheel', function(e, d) {
if((this.scrollTop === (scrollHeight - height) && d < 0) || (this.scrollTop === 0 && d > 0)) {
e.preventDefault();
}
});
});
I know it's quite an old question, but since this is one of top results in google... I had to somehow cancel scroll bubbling without jQuery and this code works for me:
function preventDefault(e) {
e = e || window.event;
if (e.preventDefault)
e.preventDefault();
e.returnValue = false;
}
document.getElementById('a').onmousewheel = function(e) {
document.getElementById('a').scrollTop -= e. wheelDeltaY;
preventDefault(e);
}
EDIT: CodePen example
For AngularJS, I defined the following directive:
module.directive('isolateScrolling', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.bind('DOMMouseScroll', function (e) {
if (e.detail > 0 && this.clientHeight + this.scrollTop == this.scrollHeight) {
this.scrollTop = this.scrollHeight - this.clientHeight;
e.stopPropagation();
e.preventDefault();
return false;
}
else if (e.detail < 0 && this.scrollTop <= 0) {
this.scrollTop = 0;
e.stopPropagation();
e.preventDefault();
return false;
}
});
element.bind('mousewheel', function (e) {
if (e.deltaY > 0 && this.clientHeight + this.scrollTop >= this.scrollHeight) {
this.scrollTop = this.scrollHeight - this.clientHeight;
e.stopPropagation();
e.preventDefault();
return false;
}
else if (e.deltaY < 0 && this.scrollTop <= 0) {
this.scrollTop = 0;
e.stopPropagation();
e.preventDefault();
return false;
}
return true;
});
}
};
});
And then added it to the scrollable element (the dropdown-menu ul):
<div class="dropdown">
<button type="button" class="btn dropdown-toggle">Rename <span class="caret"></span></button>
<ul class="dropdown-menu" isolate-scrolling>
<li ng-repeat="s in savedSettings | objectToArray | orderBy:'name' track by s.name">
<a ng-click="renameSettings(s.name)">{{s.name}}</a>
</li>
</ul>
</div>
Tested on Chrome and Firefox. Chrome's smooth scrolling defeats this hack when a large mousewheel movement is made near (but not at) the top or bottom of the scroll region.
There are tons of questions like this out there, with many answers, but I could not find a satisfactory solution that did not involve events, scripts, plugins, etc. I wanted to keep it straight in HTML and CSS. I finally found a solution that worked, although it involved restructuring the markup to break the event chain.
1. Basic problem
Scrolling input (i.e.: mousewheel) applied to the modal element will spill over into an ancestor element and scroll it in the same direction, if some such element is scrollable:
(All examples are meant to be viewed on desktop resolutions)
https://jsfiddle.net/ybkbg26c/5/
HTML:
<div id="parent">
<div id="modal">
This text is pretty long here. Hope fully, we will get some scroll bars.
</div>
</div>
CSS:
#modal {
position: absolute;
height: 100px;
width: 100px;
top: 20%;
left: 20%;
overflow-y: scroll;
}
#parent {
height: 4000px;
}
2. No parent scroll on modal scroll
The reason why the ancestor ends up scrolling is because the scroll event bubbles and some element on the chain is able to handle it. A way to stop that is to make sure none of the elements on the chain know how to handle the scroll. In terms of our example, we can refactor the tree to move the modal out of the parent element. For obscure reasons, it is not enough to keep the parent and the modal DOM siblings; the parent must be wrapped by another element that establishes a new stacking context. An absolutely positioned wrapper around the parent can do the trick.
The result we get is that as long as the modal receives the scroll event, the event will not bubble to the "parent" element.
It should typically be possible to redesign the DOM tree to support this behavior without affecting what the end user sees.
https://jsfiddle.net/0bqq31Lv/3/
HTML:
<div id="context">
<div id="parent">
</div>
</div>
<div id="modal">
This text is pretty long here. Hope fully, we will get some scroll bars.
</div>
CSS (new only):
#context {
position: absolute;
overflow-y: scroll;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
3. No scroll anywhere except in modal while it is up
The solution above still allows the parent to receive scroll events, as long as they are not intercepted by the modal window (i.e. if triggered by mousewheel while the cursor is not over the modal). This is sometimes undesirable and we may want to forbid all background scrolling while the modal is up. To do that, we need to insert an extra stacking context that spans the whole viewport behind the modal. We can do that by displaying an absolutely positioned overlay, which can be fully transparent if necessary (but not visibility:hidden).
https://jsfiddle.net/0bqq31Lv/2/
HTML:
<div id="context">
<div id="parent">
</div>
</div>
<div id="overlay">
</div>
<div id="modal">
This text is pretty long here. Hope fully, we will get some scroll bars.
</div>
CSS (new on top of #2):
#overlay {
background-color: transparent;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
Here's a plain JavaScript version:
function scroll(e) {
var delta = (e.type === "mousewheel") ? e.wheelDelta : e.detail * -40;
if (delta < 0 && (this.scrollHeight - this.offsetHeight - this.scrollTop) <= 0) {
this.scrollTop = this.scrollHeight;
e.preventDefault();
} else if (delta > 0 && delta > this.scrollTop) {
this.scrollTop = 0;
e.preventDefault();
}
}
document.querySelectorAll(".scroller").addEventListener("mousewheel", scroll);
document.querySelectorAll(".scroller").addEventListener("DOMMouseScroll", scroll);
As variant, to avoid performance issues with scroll or mousewheel handling, you can use code like below:
css:
body.noscroll {
overflow: hidden;
}
.scrollable {
max-height: 200px;
overflow-y: scroll;
border: 1px solid #ccc;
}
html:
<div class="scrollable">
...A bunch of items to make the div scroll...
</div>
...A bunch of text to make the body scroll...
js:
var $document = $(document),
$body = $('body'),
$scrolable = $('.scrollable');
$scrolable.on({
'mouseenter': function () {
// add hack class to prevent workspace scroll when scroll outside
$body.addClass('noscroll');
},
'mouseleave': function () {
// remove hack class to allow scroll
$body.removeClass('noscroll');
}
});
Example of work: http://jsbin.com/damuwinarata/4
Angular JS Directive
I had to wrap an angular directive. The following is a Mashup of the other answers here. tested on Chrome and Internet Explorer 11.
var app = angular.module('myApp');
app.directive("preventParentScroll", function () {
return {
restrict: "A",
scope: false,
link: function (scope, elm, attr) {
elm.bind('mousewheel', onMouseWheel);
function onMouseWheel(e) {
elm[0].scrollTop -= (e.wheelDeltaY || (e.originalEvent && (e.originalEvent.wheelDeltaY || e.originalEvent.wheelDelta)) || e.wheelDelta || 0);
e.stopPropagation();
e.preventDefault();
e.returnValue = false;
}
}
}
});
Usage
<div prevent-parent-scroll>
...
</div>
Hopes this helps the next person that gets here from a Google search.
Using native element scroll properties with the delta value from the mousewheel plugin:
$elem.on('mousewheel', function (e, delta) {
// Restricts mouse scrolling to the scrolling range of this element.
if (
this.scrollTop < 1 && delta > 0 ||
(this.clientHeight + this.scrollTop) === this.scrollHeight && delta < 0
) {
e.preventDefault();
}
});
In case someone is still looking for a solution for this, the following plugin does the job http://mohammadyounes.github.io/jquery-scrollLock/
It fully addresses the issue of locking mouse wheel scroll inside a given container, preventing it from propagating to parent element.
It does not change wheel scrolling speed, user experience will not be affected. and you get the same behavior regardless of the OS mouse wheel vertical scrolling speed (On Windows it can be set to one screen or one line up to 100 lines per notch).
Demo: http://mohammadyounes.github.io/jquery-scrollLock/example/
Source: https://github.com/MohammadYounes/jquery-scrollLock
You can achieve this outcome with CSS, ie
.isolate-scrolling {
overscroll-behavior: contain;
}
This will only scroll the parent container if your mouse leaves the child element to the parent.
amustill's answer as a knockout handler:
ko.bindingHandlers.preventParentScroll = {
init: function (element, valueAccessor, allBindingsAccessor, context) {
$(element).mousewheel(function (e, d) {
var t = $(this);
if (d > 0 && t.scrollTop() === 0) {
e.preventDefault();
}
else {
if (d < 0 && (t.scrollTop() == t.get(0).scrollHeight - t.innerHeight())) {
e.preventDefault();
}
}
});
}
};
the method above is not that natural, after some googling I find a more nice solution , and no need of jQuery. see [1] and demo [2].
var element = document.getElementById('uf-notice-ul');
var isMacWebkit = (navigator.userAgent.indexOf("Macintosh") !== -1 &&
navigator.userAgent.indexOf("WebKit") !== -1);
var isFirefox = (navigator.userAgent.indexOf("firefox") !== -1);
element.onwheel = wheelHandler; // Future browsers
element.onmousewheel = wheelHandler; // Most current browsers
if (isFirefox) {
element.scrollTop = 0;
element.addEventListener("DOMMouseScroll", wheelHandler, false);
}
// prevent from scrolling parrent elements
function wheelHandler(event) {
var e = event || window.event; // Standard or IE event object
// Extract the amount of rotation from the event object, looking
// for properties of a wheel event object, a mousewheel event object
// (in both its 2D and 1D forms), and the Firefox DOMMouseScroll event.
// Scale the deltas so that one "click" toward the screen is 30 pixels.
// If future browsers fire both "wheel" and "mousewheel" for the same
// event, we'll end up double-counting it here. Hopefully, however,
// cancelling the wheel event will prevent generation of mousewheel.
var deltaX = e.deltaX * -30 || // wheel event
e.wheelDeltaX / 4 || // mousewheel
0; // property not defined
var deltaY = e.deltaY * -30 || // wheel event
e.wheelDeltaY / 4 || // mousewheel event in Webkit
(e.wheelDeltaY === undefined && // if there is no 2D property then
e.wheelDelta / 4) || // use the 1D wheel property
e.detail * -10 || // Firefox DOMMouseScroll event
0; // property not defined
// Most browsers generate one event with delta 120 per mousewheel click.
// On Macs, however, the mousewheels seem to be velocity-sensitive and
// the delta values are often larger multiples of 120, at
// least with the Apple Mouse. Use browser-testing to defeat this.
if (isMacWebkit) {
deltaX /= 30;
deltaY /= 30;
}
e.currentTarget.scrollTop -= deltaY;
// If we ever get a mousewheel or wheel event in (a future version of)
// Firefox, then we don't need DOMMouseScroll anymore.
if (isFirefox && e.type !== "DOMMouseScroll") {
element.removeEventListener("DOMMouseScroll", wheelHandler, false);
}
// Don't let this event bubble. Prevent any default action.
// This stops the browser from using the mousewheel event to scroll
// the document. Hopefully calling preventDefault() on a wheel event
// will also prevent the generation of a mousewheel event for the
// same rotation.
if (e.preventDefault) e.preventDefault();
if (e.stopPropagation) e.stopPropagation();
e.cancelBubble = true; // IE events
e.returnValue = false; // IE events
return false;
}
[1] https://dimakuzmich.wordpress.com/2013/07/16/prevent-scrolling-of-parent-element-with-javascript/
[2] http://jsfiddle.net/dima_k/5mPkB/1/
This actually works in AngularJS.
Tested on Chrome and Firefox.
.directive('stopScroll', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.bind('mousewheel', function (e) {
var $this = $(this),
scrollTop = this.scrollTop,
scrollHeight = this.scrollHeight,
height = $this.height(),
delta = (e.type == 'DOMMouseScroll' ?
e.originalEvent.detail * -40 :
e.originalEvent.wheelDelta),
up = delta > 0;
var prevent = function() {
e.stopPropagation();
e.preventDefault();
e.returnValue = false;
return false;
};
if (!up && -delta > scrollHeight - height - scrollTop) {
// Scrolling down, but this will take us past the bottom.
$this.scrollTop(scrollHeight);
return prevent();
} else if (up && delta > scrollTop) {
// Scrolling up, but this will take us past the top.
$this.scrollTop(0);
return prevent();
}
});
}
};
})
my jQuery plugin:
$('.child').dontScrollParent();
$.fn.dontScrollParent = function()
{
this.bind('mousewheel DOMMouseScroll',function(e)
{
var delta = e.originalEvent.wheelDelta || -e.originalEvent.detail;
if (delta > 0 && $(this).scrollTop() <= 0)
return false;
if (delta < 0 && $(this).scrollTop() >= this.scrollHeight - $(this).height())
return false;
return true;
});
}
I have a similar situation and here's how i solved it:
All my scrollable elements get the class scrollable.
$(document).on('wheel', '.scrollable', function(evt) {
var offsetTop = this.scrollTop + parseInt(evt.originalEvent.deltaY, 10);
var offsetBottom = this.scrollHeight - this.getBoundingClientRect().height - offsetTop;
if (offsetTop < 0 || offsetBottom < 0) {
evt.preventDefault();
} else {
evt.stopImmediatePropagation();
}
});
stopImmediatePropagation() makes sure not to scroll parent scrollable area from scrollable child area.
Here's a vanilla JS implementation of it:
http://jsbin.com/lugim/2/edit?js,output
New web dev here. This worked like a charm for me on both IE and Chrome.
static preventScrollPropagation(e: HTMLElement) {
e.onmousewheel = (ev) => {
var preventScroll = false;
var isScrollingDown = ev.wheelDelta < 0;
if (isScrollingDown) {
var isAtBottom = e.scrollTop + e.clientHeight == e.scrollHeight;
if (isAtBottom) {
preventScroll = true;
}
} else {
var isAtTop = e.scrollTop == 0;
if (isAtTop) {
preventScroll = true;
}
}
if (preventScroll) {
ev.preventDefault();
}
}
}
Don't let the number of lines fool you, it is quite simple - just a bit verbose for readability (self documenting code ftw right?)
Also I should mention that the language here is TypeScript, but as always, it is straightforward to convert it to JS.
We can simply use CSS.
Give a style to the child scroll container element.
style="overscroll-behavior: contain"
It doesn't trigger the parent's scroll event.
For those using MooTools, here is equivalent code:
'mousewheel': function(event){
var height = this.getSize().y;
height -= 2; // Not sure why I need this bodge
if ((this.scrollTop === (this.scrollHeight - height) && event.wheel < 0) ||
(this.scrollTop === 0 && event.wheel > 0)) {
event.preventDefault();
}
Bear in mind that I, like some others, had to tweak a value by a couple of px, that is what the height -= 2 is for.
Basically the main difference is that in MooTools, the delta info comes from event.wheel instead of an extra parameter passed to the event.
Also, I had problems if I bound this code to anything (event.target.scrollHeight for a bound function does not equal this.scrollHeight for a non-bound one)
Hope this helps someone as much as this post helped me ;)
Check out Leland Kwong's code.
Basic idea is to bind the wheeling event to the child element, and then use the native javascript property scrollHeight and the jquery property outerHeight of the child element to detect the end of the scroll, upon which return false to the wheeling event to prevent any scrolling.
var scrollableDist,curScrollPos,wheelEvent,dY;
$('#child-element').on('wheel', function(e){
scrollableDist = $(this)[0].scrollHeight - $(this).outerHeight();
curScrollPos = $(this).scrollTop();
wheelEvent = e.originalEvent;
dY = wheelEvent.deltaY;
if ((dY>0 && curScrollPos >= scrollableDist) ||
(dY<0 && curScrollPos <= 0)) {
return false;
}
});
I yoinked this from the chosen library: https://github.com/harvesthq/chosen/blob/master/coffee/chosen.jquery.coffee
function preventParentScroll(evt) {
var delta = evt.deltaY || -evt.wheelDelta || (evt && evt.detail)
if (delta) {
evt.preventDefault()
if (evt.type == 'DOMMouseScroll') {
delta = delta * 40
}
fakeTable.scrollTop = delta + fakeTable.scrollTop
}
}
var el = document.getElementById('some-id')
el.addEventListener('mousewheel', preventParentScroll)
el.addEventListener('DOMMouseScroll', preventParentScroll)
This works for me.
jQuery plugin with emulate natural scrolling for Internet Explorer
$.fn.mousewheelStopPropagation = function(options) {
options = $.extend({
// defaults
wheelstop: null // Function
}, options);
// Compatibilities
var isMsIE = ('Microsoft Internet Explorer' === navigator.appName);
var docElt = document.documentElement,
mousewheelEventName = 'mousewheel';
if('onmousewheel' in docElt) {
mousewheelEventName = 'mousewheel';
} else if('onwheel' in docElt) {
mousewheelEventName = 'wheel';
} else if('DOMMouseScroll' in docElt) {
mousewheelEventName = 'DOMMouseScroll';
}
if(!mousewheelEventName) { return this; }
function mousewheelPrevent(event) {
event.preventDefault();
event.stopPropagation();
if('function' === typeof options.wheelstop) {
options.wheelstop(event);
}
}
return this.each(function() {
var _this = this,
$this = $(_this);
$this.on(mousewheelEventName, function(event) {
var origiEvent = event.originalEvent;
var scrollTop = _this.scrollTop,
scrollMax = _this.scrollHeight - $this.outerHeight(),
delta = -origiEvent.wheelDelta;
if(isNaN(delta)) {
delta = origiEvent.deltaY;
}
var scrollUp = delta < 0;
if((scrollUp && scrollTop <= 0) || (!scrollUp && scrollTop >= scrollMax)) {
mousewheelPrevent(event);
} else if(isMsIE) {
// Fix Internet Explorer and emulate natural scrolling
var animOpt = { duration:200, easing:'linear' };
if(scrollUp && -delta > scrollTop) {
$this.stop(true).animate({ scrollTop:0 }, animOpt);
mousewheelPrevent(event);
} else if(!scrollUp && delta > scrollMax - scrollTop) {
$this.stop(true).animate({ scrollTop:scrollMax }, animOpt);
mousewheelPrevent(event);
}
}
});
});
};
https://github.com/basselin/jquery-mousewheel-stop-propagation/blob/master/mousewheelStopPropagation.js
The best solution I could find was listening to the scroll event on the window and set the scrollTop to the previous scrollTop if the child div was visible.
prevScrollPos = 0
$(window).scroll (ev) ->
if $('#mydiv').is(':visible')
document.body.scrollTop = prevScrollPos
else
prevScrollPos = document.body.scrollTop
There is a flicker in the background of the child div if you fire a lot of scroll events, so this could be tweaked, but it is hardly noticed and it was sufficient for my use case.
Don't use overflow: hidden; on body. It automatically scrolls everything to the top. There's no need for JavaScript either. Make use of overflow: auto;:
HTML Structure
<div class="overlay">
<div class="overlay-content"></div>
</div>
<div class="background-content">
lengthy content here
</div>
Styling
.overlay{
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(0, 0, 0, 0.8);
.overlay-content {
height: 100%;
overflow: scroll;
}
}
.background-content{
height: 100%;
overflow: auto;
}
Play with the demo here.
There's also a funny trick to lock the parent's scrollTop when mouse hovers over a scrollable element. This way you don't have to implement your own wheel scrolling.
Here's an example for preventing document scroll, but it can be adjusted for any element.
scrollable.mouseenter(function ()
{
var scroll = $(document).scrollTop();
$(document).on('scroll.trap', function ()
{
if ($(document).scrollTop() != scroll) $(document).scrollTop(scroll);
});
});
scrollable.mouseleave(function ()
{
$(document).off('scroll.trap');
});
M.K. offered a great plugin in his answer. Plugin can be found here. However, for the sake of completion, I thought it'd be a good idea to put it together in one answer for AngularJS.
Start by injecting the bower or npm (whichever is preferred)
bower install jquery-scrollLock --save
npm install jquery-scroll-lock --save
Add the following directive. I am choosing to add it as an attribute
(function() {
'use strict';
angular
.module('app')
.directive('isolateScrolling', isolateScrolling);
function isolateScrolling() {
return {
restrict: 'A',
link: function(sc, elem, attrs) {
$('.scroll-container').scrollLock();
}
}
}
})();
And the important piece the plugin fails to document in their website is the HTML structure that it must follow.
<div class="scroll-container locked">
<div class="scrollable" isolate-scrolling>
... whatever ...
</div>
</div>
The attribute isolate-scrolling must contain the scrollable class and it all needs to be inside the scroll-container class or whatever class you choose and the locked class must be cascaded.
It is worth to mention that with modern frameworks like reactJS, AngularJS, VueJS, etc, there are easy solutions for this problem, when dealing with fixed position elements. Examples are side panels or overlaid elements.
The technique is called a "Portal", which means that one of the components used in the app, without the need to actually extract it from where you are using it, will mount its children at the bottom of the body element, outside of the parent you are trying to avoid scrolling.
Note that it will not avoid scrolling the body element itself. You can combine this technique and mounting your app in a scrolling div to achieve the expected result.
Example Portal implementation in React's material-ui: https://material-ui-next.com/api/portal/
There is ES 6 crossbrowser + mobile vanila js decision:
function stopParentScroll(selector) {
let last_touch;
let MouseWheelHandler = (e, selector) => {
let delta;
if(e.deltaY)
delta = e.deltaY;
else if(e.wheelDelta)
delta = e.wheelDelta;
else if(e.changedTouches){
if(!last_touch){
last_touch = e.changedTouches[0].clientY;
}
else{
if(e.changedTouches[0].clientY > last_touch){
delta = -1;
}
else{
delta = 1;
}
}
}
let prevent = function() {
e.stopPropagation();
e.preventDefault();
e.returnValue = false;
return false;
};
if(selector.scrollTop === 0 && delta < 0){
return prevent();
}
else if(selector.scrollTop === (selector.scrollHeight - selector.clientHeight) && delta > 0){
return prevent();
}
};
selector.onwheel = e => {MouseWheelHandler(e, selector)};
selector.onmousewheel = e => {MouseWheelHandler(e, selector)};
selector.ontouchmove = e => {MouseWheelHandler(e, selector)};
}
I was searching for this for MooTools and this was the first that came up.
The original MooTools example would work with scrolling up, but not scrolling down so I decided to write this one.
MooTools 1.4.5: http://jsfiddle.net/3MzFJ/
MooTools 1.3.2: http://jsfiddle.net/VhnD4/
MooTools 1.2.6: http://jsfiddle.net/xWrw4/
var stopScroll = function (e) {
var scrollTo = null;
if (e.event.type === 'mousewheel') {
scrollTo = (e.event.wheelDelta * -1);
} else if (e.event.type === 'DOMMouseScroll') {
scrollTo = 40 * e.event.detail;
}
if (scrollTo) {
e.preventDefault();
this.scrollTo(0, scrollTo + this.scrollTop);
}
return false;
};
Usage:
(function)($){
window.addEvent('domready', function(){
$$('.scrollable').addEvents({
'mousewheel': stopScroll,
'DOMMouseScroll': stopScroll
});
});
})(document.id);

Categories

Resources