How can I reduce slowdowns from mousemove event? - javascript

I'm running a relatively simple function (update a span's innerHTML) on mousemove. The application is a Leaflet map. When the mouse is moving, there is palpable lag when zooming, panning and loading tiles. I only need to update the span about 10-20 times per second at most. (See here for the page in question; the update is for the X/Z indicator in the upper-right corner.)
What's the best way to delay and/or defer mousemove event calls? Is it good enough to skip updating innerHTML? Can I unregister and re-register the event after a timeout?

Modifying the text node of the span will be much more efficient than modifying innerHTML.
function mouseMoveAction(ev) {
span.firstChild.data = new Date.toString();
}
But if text nodes won't fulfill the requirement, and you need innerHTML on mousemove, you can use a threshold in the mousemove handler.
/* Keep CPUs to a minimum. */
var MOUSE_MOVE_THRESHOLD = 25,
lastMouseMoveTime = -1;
function mousemoveCallback(ev) {
var now = +new Date;
if(now - lastMouseMoveTime < MOUSE_MOVE_THRESHOLD)
return;
lastMouseMoveTime = now;
mouseMoveAction(ev);
}
Avoid jQuery, et al; they needlessly make things a lot slower and add a lot more complexity.

Have the mousemove set the innerHTML string to a variable and also use a direct plain DOM mousemove event on the element if feasible. See http://bugs.jquery.com/ticket/4398
!function () {
var buffer = null;
elem.onmousemove = function () {
buffer = value;
};
!function k() {
if (buffer) {
span.innerHTML = buffer;
buffer = null;
}
setTimeout(k, 100);
}();
}();
Now the mousemove event hardly does any work (setting innerHTML is A LOT of work btw) and the span is updated 10 times per second.

Related

How can I improve performance on my parallax scroll script?

I'm using Javascript & jQuery to build a parallax scroll script that manipulates an image in a figure element using transform:translate3d, and based on the reading I've done (Paul Irish's blog, etc), I've been informed the best solution for this task is to use requestAnimationFrame for performance reasons.
Although I understand how to write Javascript, I'm always finding myself uncertain of how to write good Javascript. In particular, while the code below seems to function correctly and smoothly, I'd like to get a few issues resolved that I'm seeing in Chrome Dev Tools.
$(document).ready(function() {
function parallaxWrapper() {
// Get the viewport dimensions
var viewportDims = determineViewport();
var parallaxImages = [];
var lastKnownScrollTop;
// Foreach figure containing a parallax
$('figure.parallax').each(function() {
// Save information about each parallax image
var parallaxImage = {};
parallaxImage.container = $(this);
parallaxImage.containerHeight = $(this).height();
// The image contained within the figure element
parallaxImage.image = $(this).children('img.lazy');
parallaxImage.offsetY = parallaxImage.container.offset().top;
parallaxImages.push(parallaxImage);
});
$(window).on('scroll', function() {
lastKnownScrollTop = $(window).scrollTop();
});
function animateParallaxImages() {
$.each(parallaxImages, function(index, parallaxImage) {
var speed = 3;
var delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / speed;
parallaxImage.image.css({
'transform': 'translate3d(0,'+ delta +'px,0)'
});
});
window.requestAnimationFrame(animateParallaxImages);
}
animateParallaxImages();
}
parallaxWrapper();
});
Firstly, when I head to the 'Timeline' tab in Chrome Dev Tools, and start recording, even with no actions on the page being performed, the "actions recorded" overlay count continues to climb, at a rate of about ~40 per second.
Secondly, why is an "animation frame fired" executing every ~16ms, even when I am not scrolling or interacting with the page, as shown by the image below?
Thirdly, why is the Used JS Heap increasing in size without me interacting with the page? As shown in the image below. I have eliminated all other scripts that could be causing this.
Can anyone help me with some pointers to fix the above issues, and give me suggestions on how I should improve my code?
(1 & 2 -- same answer) The pattern you are using creates a repeating animating loop which attempts to fire at the same rate as the browser refreshes. That's usually 60 time per second so the activity you're seeing is the loop executing approximately every 1000/60=16ms. If there's no work to do, it still fires every 16ms.
(3) The browser consumes memory as needed for your animations but the browser does not reclaim that memory immediately. Instead it occasionally reclaims any orphaned memory in a process called garbage collection. So your memory consumption should go up for a while and then drop in a big chunk. If it doesn't behave that way, then you have a memory leak.
Edit: I had not seen the answers from #user1455003 and #mpd at the time I wrote this. They answered while I was writing the book below.
requestAnimationFrame is analogous to setTimeout, except the browser wont fire your callback function until it's in a "render" cycle, which typically happens about 60 times per second. setTimeout on the other hand can fire as fast as your CPU can handle if you want it to.
Both requestAnimationFrame and setTimeout have to wait until the next available "tick" (for lack of a better term) until it will run. So, for example, if you use requestAnimationFrame it should run about 60 times per second, but if the browsers frame rate drops to 30fps (because you're trying to rotate a giant PNG with a large box-shadow) your callback function will only fire 30 times per second. Similarly, if you use setTimeout(..., 1000) it should run after 1000 milliseconds. However, if some heavy task causes the CPU to get caught up doing work, your callback won't fire until the CPU has cycles to give. John Resig has a great article on JavaScript timers.
So why not use setTimeout(..., 16) instead of request animation frame? Because your CPU might have plenty of head room while the browser's frame rate has dropped to 30fps. In such a case you would be running calculations 60 times per second and trying to render those changes, but the browser can only handle half that much. Your browser would be in a constant state of catch-up if you do it this way... hence the performance benefits of requestAnimationFrame.
For brevity, I am including all suggested changes in a single example below.
The reason you are seeing the animation frame fired so often is because you have a "recursive" animation function which is constantly firing. If you don't want it firing constantly, you can make sure it only fires while the user is scrolling.
The reason you are seeing the memory usage climb has to do with garbage collection, which is the browsers way of cleaning up stale memory. Every time you define a variable or function, the browser has to allocate a block of memory for that information. Browsers are smart enough to know when you are done using a certain variable or function and free up that memory for reuse - however, it will only collect the garbage when there is enough stale memory worth collecting. I can't see the scale of the memory graph in your screenshot, but if the memory is increasing in kilobyte size amounts, the browser may not clean it up for several minutes. You can minimize the allocation of new memory by reusing variable names and functions. In your example, every animation frame (60x second) defines a new function (used in $.each) and 2 variables (speed and delta). These are easily reusable (see code).
If your memory usage continues to increase ad infinitum, then there is a memory leak problem elsewhere in your code. Grab a beer and start doing research as the code you've posted here is leak-free. The biggest culprit is referencing an object (JS object or DOM node) which then gets deleted and the reference still hangs around. For example, if you bind a click event to a DOM node, delete the node, and never unbind the event handler... there ya go, a memory leak.
$(document).ready(function() {
function parallaxWrapper() {
// Get the viewport dimensions
var $window = $(window),
speed = 3,
viewportDims = determineViewport(),
parallaxImages = [],
isScrolling = false,
scrollingTimer = 0,
lastKnownScrollTop;
// Foreach figure containing a parallax
$('figure.parallax').each(function() {
// The browser should clean up this function and $this variable - no need for reuse
var $this = $(this);
// Save information about each parallax image
parallaxImages.push({
container = $this,
containerHeight: $this.height(),
// The image contained within the figure element
image: $this.children('img.lazy'),
offsetY: $this.offset().top
});
});
// This is a bit overkill and could probably be defined inline below
// I just wanted to illustrate reuse...
function onScrollEnd() {
isScrolling = false;
}
$window.on('scroll', function() {
lastKnownScrollTop = $window.scrollTop();
if( !isScrolling ) {
isScrolling = true;
animateParallaxImages();
}
clearTimeout(scrollingTimer);
scrollingTimer = setTimeout(onScrollEnd, 100);
});
function transformImage (index, parallaxImage) {
parallaxImage.image.css({
'transform': 'translate3d(0,' + (
(
lastKnownScrollTop +
(viewportDims.height - parallaxImage.containerHeight) / 2 -
parallaxImage.offsetY
) / speed
) + 'px,0)'
});
}
function animateParallaxImages() {
$.each(parallaxImages, transformImage);
if (isScrolling) {
window.requestAnimationFrame(animateParallaxImages);
}
}
}
parallaxWrapper();
});
#markE's answer is right on for 1 & 2
(3) Is due to the fact that your animation loop is infinitely recursive:
function animateParallaxImages() {
$.each(parallaxImages, function(index, parallaxImage) {
var speed = 3;
var delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / speed;
parallaxImage.image.css({
'transform': 'translate3d(0,'+ delta +'px,0)'
});
});
window.requestAnimationFrame(animateParallaxImages); //recursing here, but there is no base base
}
animateParallaxImages(); //Kick it off
If you look at the example on MDN:
var start = null;
var element = document.getElementById("SomeElementYouWantToAnimate");
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress/10, 200) + "px";
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
I would suggest either stopping recursion at some point, or refactor your code so functions/variables aren't being declared in the loop:
var SPEED = 3; //constant so only declare once
var delta; // declare outside of the function to reduce the number of allocations needed
function imageIterator(index, parallaxImage){
delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / SPEED;
parallaxImage.image.css({
'transform': 'translate3d(0,'+ delta +'px,0)'
});
}
function animateParallaxImages() {
$.each(parallaxImages, imageIterator); // you could also change this to a traditional loop for a small performance gain for(...)
window.requestAnimationFrame(animateParallaxImages); //recursing here, but there is no base base
}
animateParallaxImages(); //Kick it off
Try getting rid of the animation loop and putting the scroll changes in the 'scroll' function. This will prevent your script from doing transforms when lastKnownScrollTop is unchanged.
$(window).on('scroll', function() {
lastKnownScrollTop = $(window).scrollTop();
$.each(parallaxImages, function(index, parallaxImage) {
var speed = 3;
var delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / speed;
parallaxImage.image.css({
'transform': 'translate3d(0,'+ delta +'px,0)'
});
});
});

stopping mousewheel event from happening twice in OSX

I noticed mousewheel event is happening multiple times in mac osx. Can be atributed to inertia feature.
Is there a way to fix this behaviour?
(self signed ssl no worries please!)
https://sandbox.idev.ge/roomshotel/html5_v2/
I'm using scrollSections.js https://github.com/guins/jQuery.scrollSections
And it uses mousewheel jquery plugin: https://github.com/brandonaaron/jquery-mousewheel
I'm seeing a lot of people having the same issue: https://github.com/brandonaaron/jquery-mousewheel/issues/36
There are some solutions but none works with scrollSections plugin.
Any ideas how to disable this inertia feature from JS?
My attempted fix:
// Fix for OSX inertia problem, jumping sections issue.
if (isMac) {
var fireEvent;
var newDelta = deltaY;
if (oldDelta != null) {
//check to see if they differ directions
if (oldDelta < 0 && newDelta > 0) {
fireEvent = true;
}
//check to see if they differ directions
if (oldDelta > 0 && newDelta < 0) {
fireEvent = true;
}
//check to see if they are the same direction
if (oldDelta > 0 && newDelta > 0) {
//check to see if the new is higher
if (oldDelta < newDelta) {
fireEvent = true;
} else {
fireEvent = false;
}
}
//check to see if they are the same direction
if (oldDelta < 0 && newDelta < 0) {
//check to see if the new is lower
if (oldDelta > newDelta) {
fireEvent = true;
} else {
fireEvent = false;
}
}
} else {
fireEvent = true;
}
oldDelta = newDelta;
} else {
fireEvent = true;
}
You can see fix implemented here: https://sandbox.idev.ge/roomshotel/html5_v2/ But it is a hit/miss.
The latest solution with timeouts had one major drawback: kinetic scrolling effect could last rather long (even 1s or so)... and disabling scrolling for 1-2 seconds wouldn't be the best decision.
Soooo, as promised, here's another approach.
Our goal is to provide one response for one user action, which in this case is scrolling.
What's 'one scrolling'? For the sake of solving this problem, let's say that 'one scrolling' is an event that lasts from the moment the page has started to move till the moment the movement has ended.
Kinetic scrolling effect is achieved by moving the page many times (say, every 20ms) for a small distance. It means that our kinetic scrolling consists of many-many little linear 'scrollings'.
Empirical testing has showed that this little 'scrollings' happen every 17-18ms in the middle of kinetic scroll, and about 80-90ms at the beginning and the end. Here's a simple test we can set up to see that:
var oldD;
var f = function(){
var d = new Date().getTime();
if(typeof oldD !== 'undefined')
console.log(d-oldD);
oldD = d;
}
window.onscroll=f;
Important! Every time this mini-scroll happens, scroll event is triggered. So:
window.onscroll = function(){console.log("i'm scrolling!")};
will be fired 15 to 20+ times during one kinetic scroll. BTW, onscroll has really good browser support (see compatibility table), so we can rely on it (except for touch devices, I'll cover this issue a bit later);
Some may say that redefining window.onscroll is not the best way to set event listeners. Yes, you're encouraged to use
$(window).on('scroll',function(){...});
or whatever you like, it's not the point of the problem (I personally use my self-written library).
So, with the help of onscroll event we can reliably say whether this particular mini-movement of the page belongs to one long-lasting kinetic scroll, or is it a new one:
var prevTime = new Date().getTime();
var f = function(){
var curTime = new Date().getTime();
if(typeof prevTime !== 'undefined'){
var timeDiff = curTime-prevTime;
if(timeDiff>200)
console.log('New kinetic scroll has started!');
}
prevTime = curTime;
}
window.onscroll=f;
Instead of "console.log" you can call your desired callback function (or event handler) and you're done!
The function will be fired only once on every kinetic or simple scroll, which was our goal.
You may have noticed that I've used 200ms as a criteria of whether it's a new scroll or a part of the previous scroll. It's up to you to set it to greater values to be 999% sure you prevent any extra calls. However, please keep in mind that it's NOT what we have used in my previous answer. It's just a period of time between any two page movements (whether it's a new scroll or a little part of a kinetic scroll). To my mind, there's a very little chance that there will be a lag more than 200ms between steps in kinetic scroll (otherwise it will be not smooth at all).
As I've mentioned above, the onscroll event works differently on touch devices. It won't fire during every little step of kinetic scroll. But it will fire when the movement of the page has finally ended. Moreover, there's ontouchmove event... So, it's not a big deal. If necessary, I can provide solution for touch devices too.
P.S. I understand that I've written a bit too much, so I'd be happy to answer all your questions and provide further code if you need one.
Provided solution is supported in all browsers, very lightweight and, what's more important, is suitable not only for macs, but for every device that might implement kinetic scrolling, so I think it's really a way to go.
You know, I think it's a better idea to use timeouts in this case. Why not write something like this:
// Let's say it's a global context or whatever...:
var fireEvent = true;
var newDelta, oldDelta, eventTimeout;
newDelta = oldDelta = eventTimeout = null;
// ... and the function below fires onmousewheel or anything similar:
function someFunc(){
if(!fireEvent) return; // if fireEvent is not allowed => stop execution here ('return' keyword stops execution of the function), else, execute code below:
newDelta = deltaY;
if(oldDelta!=null&&oldDelta*newDelta>0){ // (1.1) if it's not the first event and directions are the same => prevent possible dublicates for further 50ms:
fireEvent = false;
clearTimeout(eventTimeout); // clear previous timeouts. Important!
eventTimeout = setTimeout(function(){fireEvent = true},500);
}
oldDelta = newDelta;
someEventCallback(); // (1.2) fire further functions...
}
So, any mousewheel event fired within half a second after any previous mousewheel event call will be ignored, if it is made in the same direction as previous (see condition at 1.1). It will solve the problem and there's no way user would spot this. Delay amount may be changed to better meet your needs.
The solution is made on pure JS. You're welcome to ask any questions about integrating it in your environment, but then I'll need you to provide further code of your page.
P.S. I have not seen anything similar to eventCallback() call in your code (see 1.2 of my solution). there was only fireEvent flag. Were you doing something like:
if(fireEvent)
someEventCallback();
later on or something?
P.P.S.note that fireEvent should be in global scope in order to work here with setTimeout. If it's not, it's also quite easy to make it work fine, but the code needs to be altered a bit. If it's your case, tell me and I'll fix it for you.
UPDATE
After a brief search I found out, that similar mechanism is used in Underscore's _debounce() function. See Underscore documentation here
Have you though about using fullpage.js instead?
It has a delay between arriving to a section and the moment you are able to scroll to the next section which solves part of the problem Mac users experience with track-pads or Apple magic mouses.
It would also provide you some other benefits, such as much more options, methods and compatibility with touch devices and old browsers with no CSS3 support.
To have something to start with, let's make your solution shorter (therefore easier to understand & debug):
var fireEvent;
var newDelta = deltaY;
var oldDelta = null;
fireEvent = EventCheck();
oldDelta = newDelta;
function EventCheck(){
if(oldDelta==null) return true; //(1.1)
if(oldDelta*newDelta < 0) return true; // (1.2) if directions differ => fire event
if(Math.abs(newDelta)<Math.abs(oldDelta)) return true; // (1.3) if oldDelta exceeds newDelta in absolute values => fire event
return false; // (1.4) else => don't fire;
}
As you see, it does absolutely what your code does.
However, I can't understand this part of your code (which corresponds to (1.3) in my snippet):
//check to see if the new is lower
if (oldDelta > newDelta) {
fireEvent = true;
} else {
fireEvent = false;
}
from code provided it's unclear how deltaY is calculated. As one could assume, delta equals to endPosition - initialPosition. So, oldDelta>newDelta does not mean that the new position is lower, but that the new gap between these two values is bigger. If it's what it mean and you still use it, I suppose you try to track inertia with that. Then you should alter comparative operator (use less than, instead of greater then and vice-versa). In other words, I'd write:
if(Math.abs(newDelta)>Math.abs(oldDelta)) return true; // (1.3)
you see, now I've used 'greater than' operator, which means: newDelta exceeds oldDelta in absolute values => it's not inertia and you can still fire the event.
Is it what you're trying to achieve or have I misinterpreted your code? If so, please explain how deltaY is calculated and what was your goal by comparing old&new Deltas.
P.S. I'd suggest not to use if(isMac) in this step, while a problem can also potentially hide there.

How to get all the HTML elements affected by an event

I have assigned a "mousemove" event on some div elements. Those elements might overlap each other due to an animation process, so several "mousemove" events could be called at once by moving the mouse over the overlapping parts.
The problem is that two triggered "mousemove" events can lead to conflicting decisions. Hence, I would like to make a decision based on ALL elements that are concerned by the "mousemove" event, when such an event occur for at least one of them.
My question is : do you know an efficient way to do it ?
Thanks !
If I understand the q correctly, you want to synchronize the execution of mousemove events for each div. There are hacky ways to do it, though the best would be to change your HTML markup, so you don't have overlaps.
Anyway, for your case, you could do the following:
var g_focusDivId = "";
function onMouseMove(e)
{
if (g_focusDivId != "" && g_focusDivId != e.target.id)
return; // Deciding to not exec any other mouse moves
g_focusDivId = e.target.id;
// Do your stuff
g_focusDivId = "";
}
This, of course, assumes that JS event handling is single-threaded, which is not always true: Is JavaScript guaranteed to be single-threaded?
The alternative is to do this (I have not tried this). I am using a queue to run the events in sequence on a single method. Much more controlled, but it may lead to some events getting processed late.
var g_syncEventQueue = new Array();
function onMouseEvent(e)
{
g_syncEventQueue.push(e);
}
function queueListenerProc()
{
if (g_syncEventQueue.size() > 0)
{
var evt = g_syncEventQueue[0];
g_syncEventQueue = g_syncEventQueue.splice(0, 1);
return queueListenerProc(); // Immediately process the next event
}
setTimeout("queueListenerProc()", 1000);
}
queueListenerProc(); // Not ideal because it keeps running without an exit condition.

How to limit handling of event to once per X seconds with jQuery / javascript?

For a rapidly-firing keypress event, I want to limit the handling of the event to a maximum of once per X seconds.
I'm already using jQuery for the event handling, so a jQuery-based solution would be preferred, though vanilla Javascript is fine too.
This jsfiddle shows keypress firing rapidly without any limiting on the handling
This jsfiddle implements limiting the handling to once per 0.5 seconds in vanilla JS using setTimeout()
My question is
Does jQuery have an inbuilt way of doing this? I don't see anything in the .on() docs
If not, is there a better pattern for doing this in vanilla JS than I've used in my second jsfiddle example?
Does jQuery have an inbuilt way of doing this?
No.
If not, is there a better pattern for doing this in vanilla JS than I've used in my second jsfiddle example?
Instead of using setTimeout and flags, you can keep track of when the handler was called the last time and only call it after a defined interval has passed. This is referred to as throttling and there are various ways to implement this.
In its simplest form you just have to ignore every call that is made within the time of lastCall + interval, so that only one call in every interval occurs.
E.g. here is a function that returns a new function which can only be called once every X milliseconds:
function throttle(func, interval) {
var lastCall = 0;
return function() {
var now = Date.now();
if (lastCall + interval < now) {
lastCall = now;
return func.apply(this, arguments);
}
};
}
which you can use as
$("#inputField").on("keypress", throttle(function(event) {
$("div#output").append("key pressed <br/>");
}, 500));
DEMO
As Esailija mentions in his comment, maybe it is not throttling that you need but debouncing. This is a similar but slightly different concept. While throttling means that something should occur only once every x milliseconds, debouncing means that something should occur only if it didn't occur in the last x milliseconds.
A typical example is the scroll event. A scroll event handler will be called very often because the event is basically triggered continuously. But maybe you only want to execute the handler when the user stopped scrolling and not while he is scrolling.
A simple way to achieve this is to use a timeout and cancel it if the function is called again (and the timeout didn't run yet):
function debounce(func, interval) {
var lastCall = -1;
return function() {
clearTimeout(lastCall);
var args = arguments;
var self = this;
lastCall = setTimeout(function() {
func.apply(self, args);
}, interval);
};
}
A drawback with this implementation is that you cannot return a value from the event handler (such as return false;). Maybe there are implementations which can preserve this feature (if required).
DEMO
I would do that event throttling as simple as this:
$("#inputField").on("keypress", function(event) {
var now = Date.now();
var nt = $(this).data("lastime") || now;
if( nt > now ) return;
$(this).data("lastime", now + 500);
$("div#output").append("key pressed <br/>");
});
Record when the last time you processed the event was, and each time you get an event, do a check to see if the interval you want has elapsed.

How to select the last element on viewport

I'm looking for an effecient way to constantly select the last element within the visible window/viewport.
So far, this is my code:
$(window).scroll(function () {
$('.post-content p').removeClass("temp last")
$('.post-content p').filter(":onScreen").addClass("temp")
$(".temp").eq(-1).addClass("last")
});
As you could probably imagine, this hauls up a lot of resources and doesn't perform very well. Can somebody please suggest from more elegant code?
My knowledge of Javascript is very basic, so please be patient with me. Thank you.
PS: I am using the onScreen plugin for the :onScreen selector: http://benpickles.github.com/onScreen/
Binding the scroll handler
Binding functions to the scroll Event can lead to serious performance problems. The scroll event fires really vigorously on page scroll, so binding functions with resource-heavy code to it is a bad idea.
What John suggests is setting up the interval and thereby having the code only execute some time after a scroll event.
Have a look at this jsfiddle to see difference between the implementations
The indirect handler solution comes at the cost of a noticeable lag between scrolling and executing the code, and it is your decision if you can trade in performance for snappier execution. Be sure to test performance on every browser you support.
Speeding up code execution
There are a lot of different concepts you can use to speed up your code. Regarding your code, it comes down to:
Caching selectors. You reselect elements every time the scroll handler fires, which is unnecessary
Not using jQuery plugins without knowing what they do. In your case, the plugin code is nice and quite straightforward, but for your goal you can have even snappier code.
preventing any unnecessary calculation. With your and the plugin's code, the offset of every element is calculated every time the scroll handler fires.
So what I've come up with is a Jsfiddle with an example how you could do you scroll handler. It's not exactly matched to your DOM because I don't know your html, but it should be easy to match it to your implementation.
I managed to reduce the time used by 95% compared to your code. You can see for yourself by profiling the two samples in chrome.
I assumed you just want to select the last element and you do not need the temp class
So, here's the code with explanations
// Store the offsets in an array
var offsets = [];
// Cache the elements to select
var elements = $('.elem');
// Cache the window jQuery Object
var jWindow = $(window);
// Cache the calculation of the window height
var jWindowHeight = jWindow.height();
// set up the variable for the current selected offset
var currentOffset;
// set up the variable for the current scrollOffset
var scrollOffset;
// set up the variable for scrolled, set it to true to be able to assign at
// the beginning
var scrolled = true;
// function to assign the different elements offsets,
// they don't change on scroll
var assignOffsets = function() {
elements.each(function() {
offsets.push({
offsetTop: $(this).offset().top,
height: $(this).height(),
element: $(this)
});
});
};
// execute the function once. Exectue it again if you added
// or removed elements
assignOffsets();
// function to assign a class to the last element
var assignLast = function() {
// only execute it if the user scrolled
if (scrolled) {
// assigning false to scrolled to prevent execution until the user
// scrolled again
scrolled = false;
// assign the scrolloffset
scrollOffset = jWindowHeight + jWindow.scrollTop();
// only execute the function if no current offset is set,
// or the user scrolled down or up enough for another element to be
// the last
if (!currentOffset || currentOffset.offsetTop < scrollOffset || currentOffset.offsetTop + currentOffset.height > scrollOffset) {
// Iterate starting from the bottom
// change this to positive iteration if the elements count below
// the fold is higher than above the fold
for (var i = offsets.length - 1; i >= 0; i--) {
// if the element is above the fold, reassign the current
// element
if (offsets[i].offsetTop + offsets[i].height < (scrollOffset)) {
currentOffset && (currentOffset.element.removeClass('last'));
currentOffset = offsets[i];
currentOffset.element.addClass('last');
// no further iteration needed and we can break;
break;
}
}
return true;
} else {
return false;
}
}
}
assignLast();
// reassign the window height on resize;
jWindow.on('resize', function() {
jWindowHeight = jWindow.height();
});
// scroll handler only doing assignment of scrolled variable to true
jWindow.scroll(function() {
scrolled = true;
});
// set the interval for the handler
setInterval(assignLast, 250);
// assigning the classes for the first time
assignLast();

Categories