JavaScript – detect if element stays in viewport for n seconds - javascript
I’m need to push a data layer event whenever a content block of a certain css class is visible for 5 seconds (a sign that the user is reading the content.
Ive used something like this:
$(window).on(‘scroll resize’, function() {
$(‘.myClass’).each(function(element) {
If (isInViewport(element)) {
setTimeout(function() {
if (isInViewport(element)) {
... // Push the data layer event.
}
}, 5000);
}
});
});
function isInViewport(element) {
... // Returns true if element is visible.
};
Just wrote this from memory, so it may not be 100% correct, but the gist is I try to:
Test visibility on every myClass element on scroll/resize
If one is visible, wait 5 seconds and check the same element one more time.
Trouble is, element is undefined when setTimeout runs isInViewport. Maybe jQuery’s .each and setTimeout are a bad match?
I managed to do this using the intersection observer. My requirements were to check if the element was 50% in view for at least a second and if so then trigger an event.
let timer;
const config = {
root: null,
threshold: 0.5 // This was the element being 50% in view (my requirements)
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
timer = setTimeout(() => {
//... push to data layer
}, 1000);
} else {
clearTimeout(timer);
}
});
}, config);
observer.observe(YourElement);
I used the jquery-visible plugin to achieve a script that will output the time (in seconds) since a particular element is in view. The output uses an interval of X seconds... out of the scroll handler.
On stop scrolling, we check all the monitored elements to know if they're in the viewport.
If an element is, we check if it already was logged in the visible_begins array on a previous scroll stop. If it isn't, we push an object containing its id and the actual time in milliseconds.
Still on scroll stop, if an element isn't in the viewport, we check if it was logged in the visible_begins and if it's the case, we remove it.
Now on an interval of X seconds (your choice), we check all the monitored elements and each that is still in viewport is outputed with the time differential from now.
console.clear();
var scrolling = false;
var scrolling_timeout;
var reading_check_interval;
var reading_check_delay = 5; // seconds
var completePartial = false; // "true" to include partially in viewport
var monitored_elements = $(".target");
var visible_begins = [];
// Scroll handler
$(window).on("scroll",function(){
if(!scrolling){
console.log("User started scrolling.");
}
scrolling = true;
clearTimeout(scrolling_timeout);
scrolling_timeout = setTimeout(function(){
scrolling = false;
console.log("User stopped scrolling.");
// User stopped scrolling, check all element for visibility
monitored_elements.each(function(){
if($(this).visible(completePartial)){
console.log(this.id+" is in view.");
// Check if it's already logged in the visible_begins array
var found = false;
for(i=0;i<visible_begins.length;i++){
if(visible_begins[i].id == this.id){
found = true;
}
}
if(!found){
// Push an object with the visible element id and the actual time
visible_begins.push({id:this.id,time:new Date().getTime()});
}
}
});
},200); // scrolling delay, 200ms is good.
}); // End on scroll handler
// visibility check interval
reading_check_interval = setInterval(function(){
monitored_elements.each(function(){
if($(this).visible(completePartial)){
// The element is visible
// Check all object in the array to fing this.id
for(i=0;i<visible_begins.length;i++){
if(visible_begins[i].id == this.id){
var now = new Date().getTime();
var readTime = ((now-visible_begins[i].time)/1000).toFixed(1);
console.log(visible_begins[i].id+" is in view since "+readTime+" seconds.")
}
}
}else{
// The element is not visible
// Remove it from thevisible_begins array if it's there
for(i=0;i<visible_begins.length;i++){
if(visible_begins[i].id == this.id){
visible_begins.splice(i,1);
console.log(this.id+" was removed from the array.");
}
}
}
});
},reading_check_delay*1000); // End interval
.target{
height:400px;
border-bottom:2px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-visible/1.2.0/jquery.visible.min.js"></script>
<div id="one" class="target">1</div>
<div id="two" class="target">2</div>
<div id="three" class="target">3</div>
<div id="four" class="target">4</div>
<div id="five" class="target">5</div>
<div id="six" class="target">6</div>
<div id="seven" class="target">7</div>
<div id="eight" class="target">8</div>
<div id="nine" class="target">9</div>
<div id="ten" class="target">10</div>
Please run the snippet in full page mode, since there is a couple console logs.
CodePen
You can use this function to check if an element is in the viewport (from this answer):
function isElementInViewport (el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}
<input id="inViewport"/>
<span style="margin-left: 9999px;" id="notInViewport">s</span>
<script>
function isElementInViewport (el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}
console.log("#inViewport in viewport: "+isElementInViewport(document.getElementById("inViewport")));
console.log("#notInViewport in viewport: "+isElementInViewport(document.getElementById("notInViewport")));
</script>
You can try using Waypoints, its a library that allows you to determine when a element enters or leaves that viewport. You pass it an event handler that accepts a direction parameter. The direction tells you whether the tracked element entered or exited the screen. Once you detect the element has entered the screen then start a timer. If you don't see and event for when the element exited the viewport then you know it has been on screen for that period of time.
Related
how to check multi div scroll to top?
I have multiple divs on my page with different classes how can I check which div is scrolled to the top and therefore do something. <div class="menu-session-0"> content </div> <div class="menu-session-1"> content </div> <div class="menu-session-2"> content </div> I already tried this : $(window).scroll(function() { setTimeout(function(){ var hT = $('.session-index_1').offset().top, hH = $('.session-index_1').outerHeight(), wH = $(window).height(), wS = $(this).scrollTop() + 175; if(hT <= (wS + 250)){ $('.menu-item').removeClass('menu-item_active'); $('.item-index-1').addClass('menu-item_active'); mySwiper.slideTo(1); mySwiper.update(); } },1050); }); But it did not work as I expected...
Ok.. I get it now, I think.. :P This should (in theory) work just fine if you copy and paste instead of your code. If you really want to use JQuery just replace the element reference getters and class writers const menuItems = document.getElementsByClassName('menu-item'); const menuSessions = document.getElementsByClassName('menu-session'); var nextSession, activeSession; // do this to get menu-session offsets.. rerun function on viewport resize function getSessionOffset() { for( let session of menuSessions ) { // store position and dataset.index directy on the element reference session.y = session.offsetTop; session.indx = session.dataset.index; } // define active elements which position we listen for // these are correct if the window scroll offset is 0 (or at the top) activeSession = menuSessions[0]; nextSession = menuSessions[1]; onScroll(window.pageYOffset); // so we check and set the correct active elements } getSessionOffset(); // page scroll listener window.addEventListener( 'scroll' , ScrollHandler ); var lastScrollPos = 0; // last recorded window scroll offset var scrollTick = false; // the tick is used to throttle code execution function ScrollHandler (ev) { lastScrollPos = ev.target.scrollTop; if (!scrollTick) { window.requestAnimationFrame(() => { // <- read up on this if you are not familiar. Very usefull onScroll(lastScrollPos); scrollTick = false; }); scrollTick = true; } } function onScroll(scrollY) { if (scrollY > nextSession.y) { // if lower offset is less than scrollPos removeActiveClass(activeSession.indx); // we remove the active class from menu item activeSession = menuSessions[nextSession.indx]; // define new elements to listen for nextSession = menuSessions[nextSession.indx + 1]; addActiveClass(activeSession.indx); // and add an active class to the new menu item } else if (scrollY < activeSession.y) { // do the same here only in reverse removeActiveClass(activeSession.indx); nextSession = menuSessions[activeSession.indx]; activeSession = menuSessions[nextSession.indx - 1]; addActiveClass(activeSession.indx); } } function removeActiveClass(indx) { menuItems[indx].classList.remove('menu-item_active'); } function addActiveClass(indx) { menuItems[indx].classList.add('menu-item_active'); } We listen only for the current and next values. This may look like a lot but can be shortened to a third of the size. Hope this helps :)
Prevent triggering a scroll event by an animated scrollTop when using jQuery
I am working on a fullscreen scrolling script. It is supposed to scroll in fixed steps, to the previous or next element, each typically occupying the full height of the page. This is a good example. I have a scroll event callback which contains an animated scrollTop, triggering the scroll event again and getting caught in a loop. I have tried a few things such as flags, but none seem to work for me. Here's the code: function pageDown() { // Some stuff, not important if (currentIndex+1 === pageCount) nextIndex = 0; else nextIndex = currentIndex+1; nextPage = $pages.eq(nextIndex); // Important stuff $('html body').animate({ scrollTop: nextPage.offset().top }, 400, function() {preventScroll=false}); } function pageUp() { // Some stuff, not important if (currentIndex === 0) nextIndex = pageCount-1; else nextIndex = currentIndex-1; nextPage = $pages.eq(nextIndex); // Important stuff $('html body').animate({ scrollTop: nextPage.offset().top }, 400, function() {preventScroll=false}); } var lastScroll = 0, preventScroll = false; $(window).on('scroll', function() { var currentScroll = $(window).scrollTop(); if(!preventScroll) { preventScroll = true; if (currentScroll > lastScroll) pageDown(); else pageUp(); } lastScroll = currentScroll; });
The main issue I have witnessed when testing this out is that the complete callback of jQuery's animate fires before the final scroll event it generates. Note that seems to only happen when scrolling down for some reason. After experimenting with a 2 steps lock, where I used a flag with 3 states to cancel that final scroll event, which worked fairly well, I explored further as it was less cooperative with the rollover logic that is present in your original code (jumping to the opposite end when reaching an end). I came up with the following code, which records the target position to be reached and ignores all scroll events as long as the current position does not match the target. This also implements the rollover logic and must be combined with the associated HTML and CSS to work properly, as we need some blank space (a single pixel on each side here) to allow for a scroll event to be fired at the top and bottom. We also initiate a first scroll as to correctly position the first element and allow the rollover to work immediately. I hope the comments in the code will provide the additional information necessary to understand the logic being used. A working demo is available in this jsfiddle HTML: <div class="pageScroller"> <div class="bumper"></div> <div class="page" style="background-color:red;"></div> <div class="page" style="background-color:green;"></div> <div class="page" style="background-color:blue;"></div> <div class="page" style="background-color:violet;"></div> <div class="page" style="background-color:cyan;"></div> <div class="bumper"></div> </div> CSS: html, body { height:100%; margin:0; padding:0; } .pages { padding:1px 0; background-color:yellow; } .pageScroller, .page { height:100%; } .bumper { height:1px; } JavaScript: var $pages = $('.page'); var currentIndex = 0; var lastScroll = 0; var currentScroll = 0; var targetScroll = 1; // must be set to the same value as the first scroll function doScroll(newScroll) { $('html, body').animate({ scrollTop: newScroll }, 400); } $(window).on('scroll', function() { // get current position currentScroll = $(window).scrollTop(); // passthrough if(targetScroll == -1) { // no target set, allow execution by doing nothing here } // still moving else if(currentScroll != targetScroll) { // target not reached, ignore this scroll event return; } // reached target else if(currentScroll == targetScroll) { // update comparator for scroll direction lastScroll = currentScroll; // enable passthrough targetScroll = -1; // ignore this scroll event return; } // get scroll direction var dirUp = currentScroll > lastScroll ? false : true; // update index currentIndex += (dirUp ? -1 : 1); // reached before start, jump to end if(currentIndex < 0) { currentIndex = $pages.length-1; } // reached after end, jump to start else if(currentIndex >= $pages.length) { currentIndex = 0; } // get scroll position of target targetScroll = $pages.eq(currentIndex).offset().top; // scroll to target doScroll(targetScroll); }); // scroll to first element $(window).scrollTop(1)
How to trigger automatic page scroll on a particular page position with sound
I am trying to create a interactive web comic (in html, css and javascript) where I want to trigger an automatic page scroll down at a defined speed from one point to another point, to make a sequential animation using multiple jpg images. In simple words, when a reader scrolls and reaches a certain (my already defined) position of a page, the page automatically force scrolls the page further down at a pre-defined point. Exactly like they did in this web toon: http://comic.naver.com/webtoon/detail.nhn?titleId=350217&no=31 I also want to trigger the sound effects, just like they did in the above mentioned link... I tried to accomplish this using the following script, but was unable to control the start and stop position for the scroll. Plus I also want it to scroll only once on a single page load. <SCRIPT language=JavaScript1.2> //change 1 to another integer to alter the scroll speed. Greater is faster var speed=1 var currentpos=0,alt=1,curpos1=0,curpos2=-1 function initialize(){ startit() } function scrollwindow(){ if (document.all && !document.getElementById) temp=document.body.scrollTop else temp=window.pageYOffset if (alt==0) alt=2 else alt=1 if (alt==0) curpos1=temp else curpos2=temp if (curpos1!=curpos2){ if (document.all) currentpos=document.body.scrollTop+speed else currentpos=window.pageYOffset+speed window.scroll(0,currentpos) } else{ currentpos=0 window.scroll(0,currentpos) } } function startit(){ setInterval("scrollwindow()",50) } window.onload=initialize </SCRIPT> Thanks in advance
Here is a basic auto scroll function which will trigger when the user scrolls past 300px. var scroll = true; $(window).scroll(function () { var position = $(document).scrollTop(); console.log(position); if(position > 300 && position < 400 && scroll==true ) { scroll = false; $('html,body').animate({ scrollTop: $("#scrollTo").offset().top },2000); } }) EDIT At the moment it is a once time thing, however if you add this if statement after the first if statement it will reset the scroll state to true and will allow it to run again. if (position > 0 && position < 300 && scroll==false) { scroll = true; } EDIT To get sound to play you can use the new the .play command and just add it to what happens in the IF statement, here is an example of the .play code; $('#videoId').get(0).play(); EDIT Here is the code working not by setting a position but it finding the divs position - here is it working in JSFiddle - https://jsfiddle.net/3fxcbs2k/3/ var scroll = true; $(window).scroll(function (e) { var position = $(document).scrollTop(); var startP = $("#scrollTOO").position(); var finishP = $("#scrollTo"); if(position > startP.top && startP.top+100 && scroll==true ) { scroll = false; $('html,body').animate({scrollTop: finishP.offset().top},2000); } if (position > 0 && position < 300 && scroll==false) { scroll = true; } }) To set the position you want it to start scrolling change the value of var startP = $(" ").position(); Then to set the finish position change the value of var finsihP = $(" "); Audio <audio id="sound"> <source src="sound.mp3" type="audio/mpeg"> </audio> var scroll = true; $(window).scroll(function (e) { var position = $(document).scrollTop(); var startP = $("#scrollTOO").position(); var finishP = $("#scrollTo"); Jquery play looks like - $('#sound').get(0).play(); So just add it to the what happens when triggered, like below. if(position > startP.top && startP.top+100 && scroll==true ) { scroll = false; $('html,body').animate({scrollTop: finishP.offset().top},2000); $('#sound').get(0).play(); } if (position > 0 && position < 300 && scroll==false) { scroll = true; } }) To make more do the following Think of the var as id tags which can be called upon later in the code and after the = is there values for example; var startP = $("#scrollTOO").position(); startP is the vars unique name, and its value is the div with the id of scrollTOO position. So to make more start and end points you will have to make more vars 1 start and one end point. Example; var startP = $("#scrollTOO").position(); var finishP = $("#scrollTo"); var startP2 = $("#point2").position(); var finishP2 = $("#pint2"); and then its just to make another if statement, the same as before but replace the start points and end points, if(position > startP2.top && startP2.top+100 && scroll==true ) { scroll = false; $('html,body').animate({scrollTop: finishP2.offset().top},2000); However because we have it so it will only trigger once will have to make more var scrolls, 1 for each one. var scroll = true; var scroll2 = true; var scroll3 = true; var scroll4 = true; and then each if statement will have to look for the relevant scroll for example the second scroll if statement will look like. if(position > startP2.top && startP2.top+100 && scroll2==true ) { scroll2 = false;
Javascript clear timeout on mousemove
I have created an animated menu that opens when the users cursor is placed within 20px of the right hand side of their screen. I want to prevent the menu opening if the users cursor moves out of this region within 2 seconds but I'm struggling with the Javascript timeouts. My code looks like this so far: HTML Javascript // Timer variable var timer; function openToolbar() { // Only execute for desktop $('.no-touch').on('mousemove',function(event) { // Toolbar and Window width var tableToolbar = $('.ac-table-toolbar'), winWidth = $(window).width(); // If cursor enters right hand side of the screen start the timer // and execute after 2 seconds if(event.pageX > (winWidth - 20)) { // Timeout timer = setTimeout(function() { // Add active class to toobar and css transition will animate it // to open position tableToolbar.addClass('active').removeClass('notActive'); }, 2000); } // If mouse pointer leaves right hand side of the screen and // still has notActive class cancel the timeout to prevent // the toolbar from opening if(event.pageX < (winWidth - 20) && tableToolbar.hasClass('notActive')) { clearTimeout(timer); } // Toolbar has active class so we know its visible if(tableToolbar.hasClass('active') && event.pageX < (winWidth - 220)) { // Clear timeout (if needed?) clearTimeout(timer); // Remove active class and css transition will return it to docked position tableToolbar.removeClass('active').addClass('notActive'); } }); } The animation is handled with CSS transitions that are triggered by the active notActive classes. Please can anyone point me in the right direction. Many thanks in advance.
Too complex for this task. Big amount of mousemove events will slow down your page. Try to use another approach: HTML: <div id='rightActivateZone'></div> CSS: #rightActivateZone { background-color: red; // change to transparent for your purpose height: 100%; width: 20px; position: fixed; top: 0; right: 0; } JS: var timer; $('#rightActivateZone').on('mouseenter', function() { timer = setTimeout(function() { alert('fire!'); // your code to show menu is here }, 2000); }); $('#rightActivateZone').on('mouseleave', function() { clearTimeout(timer); }); JSFiddle demo
I agree with finelords answer. That is the best approach but to answer your question Demo: http://jsfiddle.net/robschmuecker/EZJv6/ We had to do a check on the timer being in existence aswell, see comments below. JS: var timer = null; function openToolbar() { // Moved these out of event to prevent re-processing. // Toolbar and Window width var tableToolbar = $('.ac-table-toolbar'), winWidth = $(window).width(); // Only execute for desktop $('.no-touch').on('mousemove', function (event) { // If cursor enters right hand side of the screen start the timer // and execute after 2 seconds // here you are setting a timer on every mousemove, even the ones when the cursor is over the active bar so we need to fix by checking if if (event.pageX > (winWidth - 20) && tableToolbar.hasClass('notActive') && timer == null) { // Timeout console.log('setting timeout'); timer = setTimeout(function () { // Add active class to toobar and css transition will animate it to open position tableToolbar.addClass('active').removeClass('notActive'); }, 500); } // If mouse pointer leaves right hand side of the screen and // still has notActive class cancel the timeout to prevent // the toolbar from opening if (event.pageX < (winWidth - 20) && tableToolbar.hasClass('notActive') && timer != null) { clearTimeout(timer); timer = null; console.log('cancelling timeout 1'); } // Toolbar has active class so we know its visible if (tableToolbar.hasClass('active') && event.pageX < (winWidth - 20)) { // Clear timeout (if needed?) clearTimeout(timer); timer = null; console.log('cancelling timeout 2'); // Remove active class and css transition will return it to docked position tableToolbar.removeClass('active').addClass('notActive'); } }); } openToolbar();
Rather than clearing the time out, perhaps let it run, but keep track of the latest mouse position var currentX; function openToolbar() { // Only execute for desktop $('.no-touch').on('mousemove',function(event) { currentX = event.pageX; // Toolbar and Window width var tableToolbar = $('.ac-table-toolbar'), winWidth = $(window).width(); // If cursor enters right hand side of the screen start the timer // and execute after 2 seconds if(currentX > (winWidth - 20)) { // Timeout timer = setTimeout(function() { // check the mouse position after the timeout if(currentX > (winWidth - 20)) { // Add active class to toobar and css transition will animate it // to open position tableToolbar.addClass('active').removeClass('notActive'); } }, 2000); } // Toolbar has active class so we know its visible if(tableToolbar.hasClass('active') && currentX < (winWidth - 220)) { // Remove active class and css transition will return it to docked position tableToolbar.removeClass('active').addClass('notActive'); } }); }
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);