Limiting Scroll and Resize events - javascript

This is a general question about a problem I run into often, where I need something to happen at a certain screen width, or scrollTop position, so I end up triggering events for the entire scroll or resize event. This seems really unnecessary and hurts performance. I am wondering what steps I can take to limit calling code written inside scroll or resize events so that I am only triggering these events when I need them.
In the example below I just want the background color to change at a certain scrollTop offset, but since its wrapped in a scroll event, it gets trigged for every pixel.
I know there are things like lodash, but wouldn't I have the same problem of a throttle running just as often on scroll? Any general approach help would be greatly appreciated.
$(window).on('scroll', function() {
var scrollPosition = $(window).scrollTop();
if (scrollPosition > 500) {
$('.container').css('background-color', 'blue');
} else {
$('.container').css('background-color', 'red');
}
});
.container {
background-color: red;
height: 2000px;
width: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
</div>

You should really have a look at Intersection Observer (IO), this was created to solve problems like you described.
With IO you tell the browsers which elements to watch and the browser will then execute a callback function once they come into view (or leave the view) or intersect with each other.
First you have to set the options for your observer:
let options = {
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
Here for example I specified that everytime the observed element is fully visible in viewport I want to execute a callback function. Obviously you can set the parameters to your liking.
Second you have to specify which elements you want to observe:
let target = document.querySelectorAll('.container');
observer.observe(target);
Here I say I want to watch all elements on the page with the class container.
Last I have define the callback function which will be triggered everytime one container element is visible.
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element
});
};
With this approach you don't have to worry about performance issues of scroll events.
Since you can theoretically build all of this with listening to scroll events too you can also use this official polyfill from w3c to support older browsers.
You can also stop observing an element if you don't want to observe it anymore.
Lastly have a look at this demo, it shows you can easily change the background-color of an element depending on how much of the element is visible.

You definitely should use throttle or debounce for scroll or resize handlers. It can be lodash or your own implementation. Cancelled handler triggering costs almost nothing in term of performance, so don't even bother about that.

You can use a library like Waypoint.js to accomplish a similar feature. You'll just need to add an element at the event position and it should work quite efficient. Otherwise there aren't that many other ways except the ones that Huangism already mentioned. As you also asked about resizing events it may be better to use CSS media rules because these are quite performant and easy to use:
#media only screen and (max-width: 600px) {
.container {
background-color: blue;
}
}

As mentioned in comments, you just need to throttle the action. Here is a sample code of how scrolling throttling would work
The expensive part of the scroll/resize event is the part where you are doing something, like when you getting the scrolltop and comparing it then running something. By throttling, your executable code don't actually run until the threshold is reached which saves you big time on performance
Resizing would be the same except you do it for the resize function of course
scrollTimer = setTimeout(function() {
// your execution code goes here
}, 50);
var scrollInit = false;
var scrollTimer = null;
if (!scrollInit) {
var waiting = false;
$(window).on("scroll", function(event) {
if (waiting) {
return false;
}
clearTimeout(scrollTimer);
waiting = true;
scrollTimer = setTimeout(function() {
console.log("scroll event triggered");
}, 50);
});
}
scrollInit = true;
div {
background: grey;
height: 2000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div></div>

Related

How to animate a button when it is visible for the user by scrolling

When you scroll on a page, the page shows an element, I want to be able to specify this element and
do code with it using JS, is this possible? I tried something but it had another problem..
What I tried was,
let section = document.getElementById('out');
window.onscroll = function() {
if (window.scrollY >= 678) {
document.getElementById('out').style.color = "red";
} else {
document.getElementById('out').style.color = "black"
}
}
I didn't use animate here, I just made sure it works, and it did, well almost, because if you zoom in/out it ruins it, I think that's because I got the 678 by going to the button and printing scrollY manually, is there anyway to make that automatic, so it works on any element I need?
I searched a lot and can't seem to find what I need, the solutions need jQuery, I need a solution only with html, css, and javascript.
In the future the solution will be css scroll timelines, but as that feature is at the time of writing experimental and is not supported by major browsers you can use intersection observers.
Quoted from MDN:
The Intersection Observer API lets code register a callback function that is executed whenever an element they wish to monitor enters or exits another element (or the viewport), or when the amount by which the two intersect changes by a requested amount.
To animate a component when it is in or out of view, you can give animated elements a .hidden class in your html markup and create an intersection observer which appends the .shown class to .hidden elements when they are in view.
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => entry.target.classList.toggle(“shown”, entry.isIntersecting))
})
const hiddenElements = document.querySelectorAll(“.hidden”)
hiddenElements.forEach((el) => observer.observe(el))
Then you can just apply transitions under a <selector>.shown css rule.

Remove scroll function upon window resize/media query

I have a navigation menu set to display:none, which appears upon scroll and disappears once back at the top.
Is there a way to disable the scroll function once I reach a certain breakpoint (ex. max-width: 786px) and display the menu?
Javascript
$(window).on("scroll", function() {
if($(window).scrollTop()) {
$('nav').addClass('show');
}
else {
$('nav').removeClass('show');
}
})
CSS
.show {
display: block
}
You can solve this using either javascript or CSS, however I would personally go with the javascript one.
First up, for a javascript solution, the function you need is:
window.innerWidth
It will return the entire window width not including scroll bars. Read more about it here.
So, as Temani Afif suggested, you would write a test inside your scroll function to check for the desired window width like so:
$(window).on("scroll", function() {
if (window.innerWidth <= 786) return;
// Your other code here
})
For a purely CSS solution, you could override the effect of the 'show' class with a media query:
.show {
display: block
}
#media screen and (max-width: 786px) {
nav {
display: block !important
}
}
More on media queries here
You can activate/deactivate the scroll listener on browser resize. This way your scroll listener wont be called everytime user scrolls when browser width is more than 786px.
var scrollListenerActive = false;
var handleScrollListener = function() {
if( $(window).width() < 786 && !scrollListenerActive ) {
$(window).on("scroll", function() {
if($(window).scrollTop()) {
$('nav').addClass('show');
}
else {
$('nav').removeClass('show');
}
});
scrollListenerActive = true;
} else {
$(window).off("scroll");
scrollListenerActive = false;
}
}
$(document).ready(handleScrollListener); // attach the listener on page load
$(window).resize(handleScrollListener); // attach/remove listener on window resize
That's a good strategy above, however the event you want to listen for is simply 'resize', on the window object (some older browsers can do it on any dom element, but better to be consistent and current with the standard).
So something like:
window.addEventListener('resize',function(){
if(window.innerWidth >= 768){
document.body.style['overflow-x'] = 'hidden';
}
else{
document.body.style['overflow-x'] = 'auto';
}
});
You can trade 'auto' for 'scroll' if you want the scrollbar to always show when less than 768.
Similarly, you can switch out 'overflow' instead of 'overflow-x' if you want to affect both scrollbars.
Keep in mind that the event tends to fire for every width and height change as the window is resized, in case you have other logic that might have an issue with firing many times (thousands or more) as it is resized.
This also works on maximize/restore, as they trigger the resize event as well.
Here's MDN's doc on the resize event if needed:
https://developer.mozilla.org/en-US/docs/Web/Events/resize
This is vanilla javascript, so it should work whether you're using a lib like jquery or not.

Scrolling stops when middle clicked element is removed from the DOM

I'm waist deep in my own React virtualization implementation and one of the minor issues that has been annoying me is that if I middle click on an item in my list and start scrolling, once that element is removed from the DOM the scrolling halts. My first theory was that the element was gaining focus and that preventing that would solve the issue, but what I've tried hasn't been working and I'm not even sure that's the issue.
How can I prevent this from happening?
See this fiddle for a basic demonstration:
https://jsfiddle.net/v169xkym/2/
And the relevant bit of code that handles virtualization:
$('#container').scroll(function(e) {
$('#container').children().each(function(i) {
if ($('.item:eq(' + i + ')').length > 0) {
if ($('.item:eq(' + i + ')').offset().top < 0) {
$('.item:eq(' + i + ')').remove();
$('#topPadding').height($('#topPadding').height() + 45);
}
}
});
});
Basically, I'm using the standard method of removing the element and upping the padding. In my React implementation this is handled different but here you get a basic functional representation.
you can get around this by not having the disappearing element register mouse events.
this can be done with CSS3 :
div.item {
pointer-events : none;
}
(Not entirely sure why, but my guess is that once the element disappears, the origin of the event is missing, so browsers simply stop doing what they were doing.)
Jsfiddle here
Maybe a bit late to the party. A workaround I am using on a virtual scroller is to detect when there is a scroll event, and when there has been no new events for a time, I consider the scroll is complete.
let scrollTimer = null;
let isScrolling = false;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimer);
isScrolling = true;
scrollTimer = setTimeout(()=>{
isScrolling = false;
},500);
}, false);
I then grab a reference to the element that is hovered at the time isScrolling becomes true (using mouseOver) and prevent this element being unloaded until isScrolling is false. It is a bit of a juggle, but works. I am hoping I can find something simpler as it only seems to be a Chrome problem.
Update: It seems to be a known bug, about to be fixed related to pointer-events: none on something that overlays a virtual scroller (reproduction by someone https://codepen.io/flachware/pen/WNMzKav). I have no idea why my work around above works, but nice to know it wont be needed come Chrome 103. https://bugs.chromium.org/p/chromium/issues/detail?id=1330045&q=chrome%20scroll&can=2&sort=-opened

jQuery’s css() lags when applied on scroll event

I’m trying to implement a simple “fixed headers” table. I know this can in theory be done with CSS only, but it doesn’t work very well when it comes to OSX Lion and its disappearing scrollbars. So I’m doing it with jQuery.
An approach is simple, it’s just 1.5 lines of code:
$('.inbox').scroll(function() {
$(this).find('.inbox-headers').css('top', $(this).scrollTop());
});
Demo.
This works fine and smooth in Firefox, but lags horribly in webkit browsers. Why is that happening and how do I optimise this code? Or maybe approach the problem differently.
The "scroll" event is fired very frequently. It's always going to be really hard to keep up if you're modifying the DOM while scrolling in some browsers.
What you can do is one of these things:
In your case, position: fixed; might be a good alternative.
If not, then have the event handler start a timer for like 100 milliseconds in the future, canceling any previous timer in the process. That way, the DOM will be updated only after scrolling stops or pauses.
If you want continuous updates, keep track of the timestamp when you do an update, and do nothing in the handler if it's been less than some amount of time (100ms or whatever).
function debounce(func, wait) {
var timeout;
return function() {
var context = this,
args = arguments,
later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
$('.inbox').scroll(debounce(function() {
$(this).find('.inbox-headers').css('top', $(this).scrollTop());
}, 100));
This is a little debounce function I use a lot in situations like this.
The best way to do static header is to strictly separate the header and the body of table:
Then you should apply a overflow:scroll style to .body DIV only
No absolute positioning
No scroll events
If you table is very wide then in any case you need use scroll events

Javascript - loading/busy indicator or transparent div over page on event click

i got a client side javascript function which is triggered on a button click (basically, its a calculator!!). Sometimes, due to enormous data on the page, the javascript calculator function take to long & makes the page appear inactive to the user. I was planning to display a transparent div over entire page, maybe with a busy indicator (in the center) till the calculator function ends, so that user waits till process ends.
function CalculateAmountOnClick() {
// Display transparent div
// MY time consuming loop!
{
}
// Remove transparent div
}
Any ideas on how to go about this? Should i assign a css class to a div (which surrounds my entire page's content) using javascript when my calculator function starts? I tried that but didnt get desired results. Was facing issues with transparency in IE 6. Also how will i show a loading message + image in such a transparent div?
TIA
Javacript to show a curtain:
function CalculateAmountOnClick () {
var curtain = document.body.appendChild( document.createElement('div') );
curtain.id = "curtain";
curtain.onkeypress = curtain.onclick = function(){ return false; }
try {
// your operations
}
finally {
curtain.parentNode.removeChild( curtain );
}
}
Your CSS:
#curtain {
position: fixed;
_position: absolute;
z-index: 99;
left: 0;
top: 0;
width: 100%;
height: 100%;
_height: expression(document.body.offsetHeight + "px");
background: url(curtain.png);
_background: url(curtain.gif);
}
(Move MSIE 6 underscore hacks to conditionally included files as desired.)
You could set this up as add/remove functions for the curtain, or as a wrapper:
function modalProcess( callback ) {
var ret;
var curtain = document.body.appendChild( document.createElement('div') );
curtain.id = "curtain";
curtain.onkeypress = curtain.onclick = function(){ return false; }
try {
ret = callback();
}
finally {
curtain.parentNode.removeChild( curtain );
}
return ret;
}
Which you could then call like this:
var result = modalProcess(function(){
// your operations here
});
I'm going to make some heavy assumptions here, but it sounds to me what is happening is that because you are directly locking the browser up with intense processing immediately after having set up the curtain element, the browser never has a chance to draw the curtain.
The browser doesn't redraw every time you update the DOM. It may woit to see if you're doing something more, and then draw what is needed (browsers vary their method for this). So in this case it may be refreshing the display only after it has removed the curtain, or you have forced a redraw by scrolling.
A fair waring: This kind of intense processing isn't very nice of you because it not only locks up your page. Because browsers generally implement only a single Javascript thread for ALL tabs, your processing will lock up all open tabs (= the browser). Also, you run the risk of the execution timeout and browser simply stopping your script (this can be as low as 5 seconds).
Here is a way around that.
If you can break your processing up into smaller chunks you could run it with a timeout (to allow the browser breathing space). Something like this should work:
function processLoop( actionFunc, numTimes, doneFunc ) {
var i = 0;
var f = function () {
if (i < numTimes) {
actionFunc( i++ ); // closure on i
setTimeout( f, 10 )
}
else if (doneFunc) {
doneFunc();
}
};
f();
}
// add a curtain here
processLoop(function (i){
// loop code goes in here
console.log('number: ', i);
},
10, // how many times to run loop
function (){
// things that happen after the processing is done go here
console.log('done!');
// remove curtain here
});
This is essentially a while loop but each iteration of the loop is done in an timed interval so the browser has a bit of time to breathe in between. It will slow down the processing though, and any work done afterwards needs to go into a callback as the loop runs independently of whatwever may follow the call to processLoop.
Another variation on this is to set up the curtain, call your processing function with a setTimeout to allow the browser time to draw the curtain, and then remove it once you're done.
// add a curtain
var curtain = document.body.appendChild( document.createElement('div') );
curtain.id = "curtain";
curtain.onkeypress = curtain.onclick = function(){ return false; }
// delay running processing
setTimeout(function(){
try {
// here we go...
myHeavyProcessingFunction();
}
finally {
// remove the curtain
curtain.parentNode.removeChild( curtain );
}
}, 40);
If you are using a js-library, you may want to look at a ready made solution for creating curtains. These should exist for most libraries, here is one for jQuery, and they can help with the CSS.
I would do something like:
unhide a div (display:inline)
make the position:absolute
give it a z-index:99
make the height and width 100%
when the processing is done set display:none
To make it transparent you'll have to set the opacity which is different in Firefox, IE, etc.
To show a loading icon you can always create a second div and position it where you want to on the page. When it's done loading, remove it along with the transparent one.
In addition to all of the above, don't forget to put an invisible iframe behind the shim, so that it shows up above select boxes in IE.
Edit:
This site, although it provides a solution to a more complex problem, does cover creating a modal background.
http://www.codeproject.com/KB/aspnet/ModalDialogV2.aspx
For the loading message, I would use a <div> with position:absolute, position it using left and top, and set the display to none.
When you want to show the loading indicator, you're going to have to use a timeout otherwise the div won't display until your processing is done. So, you should modify your code to this:
function showLoadingIndicator()
{
// Display div by setting display to 'inline'
setTimeout(CalculateAmountOnClick,0);
}
function CalculateAmountOnClick()
{
// MY time consuming loop!
{
}
// Remove transparent div
}
Because you set the timeout, the page will redraw before the time-consuming loop happens.

Categories

Resources