I have the following jQuery function which is supposed to detect when an element first scrolls into view on a page:
function isScrolledIntoView(elem) {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(elem).offset().top;
var elemBottom = elemTop + $(elem).height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}
My mind is mush today... how can I use this function?
Assume I want to detect when $('.guardrail360') comes into view.
Seems like you need to bind the scroll event and do the detection when that event fires.
$(window).scroll(function () {
isScrolledIntoView('.guardrail360');
});
There are several ways to do it. Just don't use a naive window.scroll implementation if you are concerned with user experience and/or performance.
It's a very, very, bad idea to attach handlers to the window scroll
event. Depending upon the browser the scroll event can fire a lot and
putting code in the scroll callback will slow down any attempts to
scroll the page (not a good idea). Any performance degradation in the
scroll handler(s) as a result will only compound the performance of
scrolling overall. Instead it's much better to use some form of a
timer to check every X milliseconds OR to attach a scroll event and
only run your code after a delay (or even after a given number of
executions - and then a delay).
-John Resig, creator of jQuery, on ejohn.org
Method 1: setInterval
First, there is the naive approach using timers:
var $el = $('.guardrail360');
var timer = setInterval(function() {
if (isScrolledIntoView($el)) {
// do stuff
// to run this block only once, simply uncomment the next line:
//clearInterval(timer);
}
}, 150);
Method 2: window.scroll event
Then there is the trivial, yet crazy inefficient way using the scroll event (depending on the browser, the scroll event will fire hundreds of times per second, so you do NOT want to run a lot of code in here, particularly not code that triggers browser reflows/redraws).
Ever visited a site where scrolling down the page felt sluggish and jittery? That is often caused by a piece of code like this one right here:
var $el = $('.guardrail360');
$(window).on('scroll', function() {
if (isScrolledIntoView($el)) {
// do stuff
}
});
Method 3: best of both worlds
The nifty hybrid approach for high traffic sites, as proposed by John Resig in the aforementioned blog post:
var $el = $('.guardrail360'),
didScroll = false;
$(window).scroll(function() {
didScroll = true;
});
var timer = setInterval(function() {
if ( didScroll ) {
didScroll = false;
if (isScrolledIntoView($el)) {
// do stuff
// to run this block only once, simply uncomment the next line:
//clearInterval(timer);
}
}
}, 250);
Method 4: Throttling / Debouncing
Throttling (minimum N milliseconds between invocations) or debouncing (only one invocation per 'burst') patterns can also be used to efficiently limit the rate at which your inner code executes. Assuming you'd be using Ben Alman's jQuery throttle/debounce plugin, the code looks like this:
var $el = $('.guardrail360');
// Throttling
$(window).on('scroll', $.throttle( 250, function(){
if (isScrolledIntoView($el)) {
// do stuff
}
}));
// Debouncing
$(window).on('scroll', $.debounce( 250, function(){
if (isScrolledIntoView($el)) {
// do stuff
}
}));
(Note that debouncing acts slightly differently from the other implementations, but that can sometimes be what you want, depending on your user experience scenario)
if ( isScrolledIntoView('.guardrail360') ) {
}
As suggested by the other answers, you should use act upon $(window).scroll(). In addition, in order to make the if statement run the first time the element is scrolled into view, I created this run_once function for you:
$('window').on('scroll', function() {
if ( isScrolledIntoView('.guardrail360') ) run_once(function() {
// ...
});
});
function run_once( callback ) {
var done = false;
return function() {
if ( !done ) {
done = true;
return callback.apply( this, arguments );
}
};
}
Related
I'm writing a Chrome extension that scrolls & listens for newly added child nodes to a parent node.
It then waits a random amount of time, then scrolls down again if more children are added, and stops if in the next 5 seconds nothing appears via ajax (when the list of results has been exhausted, for example).
My question is how I should handle waiting variable amounts of time between each event scroll reaction.
I'd like for it to work politely (yet not fail to scroll down if all 50 elements are loaded at once and the scrolls generated aren't quite enough to get to the next ajax load point).
Any ideas or ways I should think about this? (This is for a totally benign use case btw)
A good solution is a little tricky, but totally works:
var canPoll = true;
var timeout = ... whatever ...; // if we want an absolute timeout
var startTime = (new Date()).getTime();
function randomWait(t) {
// ... waits a random period of time
}
function isDone() {
// ... check if done. returns boolean
}
function complete() {
// ... run when everything is done
}
(function recursive() {
// check for our absolute walltime timeout
canPoll = ((new Date).getTime() - startTime) <= timeout;
// check other conditions too
if (!fn() && canPoll) {
// repeat!
setTimeout(recursive, randomWait(interval));
} else {
// we're done
complete();
}
})();
Adapted from this wonderful answer.
I can't figure out why smooth scrolling lags when other websites are being loaded simultaneously in other tabs.
For instance, on http://www.feedrover.com/, if you click multiple article links, the site's smooth scroll will lag heavily while the other articles load. This shouldn't be happening, since the javascript for feedrover should be done?
Any ideas on how to fix this?
A solution from MDN:
Since scroll events can fire at a high rate, the event handler shouldn't execute computationally expensive operations such as DOM modifications. Instead, it is recommended to throttle the event using requestAnimationFrame, setTimeout or customEvent, as follows:
(function() {
var throttle = function(type, name, obj) {
var obj = obj || window;
var running = false;
var func = function() {
if (running) { return; }
running = true;
requestAnimationFrame(function() {
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
};
/* init - you can init any event */
throttle ("scroll", "optimizedScroll");
})();
// handle event
window.addEventListener("optimizedScroll", function() {
console.log("Resource conscious scroll callback!");
});
I have a function that is repeatedly being called with setInterval creating animations. If there are still animations running I need it to stop calling the function until all the animations are complete. The code I am using is as follows:
EDIT: Added coded where I am removing the animated elements from the DOM, is that the problem?
var serviceLoop = setInterval(serviceQueue, LOOP_POLL_MS); //10 ms
function serviceQueue()
{
//do some animations..
function moveMan(from, to, plane)
{
(function() {
var tmp_from = from;
var tmp_to = to;
var tmp_plane = plane;
var pos = tmp_from.offset();
var temp = tmp_from.clone(true);
temp.css({ "visibility":"visible",
"position":"absolute",
"top":pos.top + "px",
"left":pos.left + "px"});
temp.appendTo("body");
tmp_from.css("visibility", "hidden");
//if (!tmp_plane) tmp_to.css("visibility", "hidden");
temp.animate(to.offset(), moveMan.ANIMATION_TIME, function() {
tmp_to.css("visibility", "visible");
temp.remove();
});
})();
}
if ($(":animated").length > 0)
{
clearInterval(serviceLoop);
$(":animated").promise().done(function() {
serviceLoop = setInterval(serviceQueue, LOOP_POLL_MS);
});
}
}
The problem I am having is after a couple of animations the function passed to done() is never called, and the script stops.
It seems likely that you end up waiting on a promise() that is waiting on some animations, but then you remove some of those objects from the DOM and then their animation never finishes so the promise never resolves.
See this quote from the jQuery doc for .promise():
Note: The returned Promise is linked to a Deferred object stored on
the .data() for an element. Since the.remove() method removes the
element's data as well as the element itself, it will prevent any of
the element's unresolved Promises from resolving. If it is necessary
to remove an element from the DOM before its Promise is resolved, use
.detach() instead and follow with .removeData() after resolution.
One quick hack might be top call .stop(true) on any item that you are removing from the DOM.
In general, this code needs to be rewritten to be safe from that possibility and hopefully to rethink how you approach whatever it is you're trying to do every 10ms because that's generally a bad design. You should use events to trigger changes, not a 10ms polling operation. You have not explained the purpose of this code so it's not clear to me what alternative to suggest.
I'm working on making an infinite scroll function for a page and am running into an issue where it appears that the $( window ).scroll() function is firing twice. I've seen similar questions on stackoverflow, but they seem to be related to fast scrolls and not having something in place to check weather the request has already been sent. Here's the code that I'm working with right now, I imagine that its something easy that I'm missing here.
var loading = false;
var scrollcount = 0;
$( window ).scroll( function(){
scrollcount++;
var scrolling = $( document ).scrollTop();
var position = $( "#ajax-load" ).offset();
if( scrolling + $( window ).height() > position.top - 200 && loading == false ){
console.log( loading ); //returns 2 falses
console.log( scrollcount ); //returns 2 of the same number
console.log( $( document ).scrollTop() ); //returns same number
loading = true;
$( "#ajax-load" ).css( "display", "block" );
$.get( "url/to/my/script", info )
.done( function( data ){
//here's where the items are appended to the document
setTimeout( function(){
loading = false;
}, 5000 );
});
}
});
The face that when I log out the scrollcount variable and the return from scrollTop() I get the same number twice seems to tell me that the event is actually firing twice at the same time for some reason. It seems that if it were firing twice, one after the other, the loading variable would be set to false and not fire that second time. Like I said, I imagine that it's something super simple that I'm missing. Thanks for your help!
My first guess is you have the event listener applied twice, wherever this code is.
Try adding $(window).unbind('scroll'); before $(window).scroll
The loading variable would be false for when it fires twice because false is set on a timeout, after an async call
var timeout;
$(window).scroll(function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
// do your stuff
}, 50);
});
Use this code.
I tinkered with this and tried some of the above solutions. None work 100% with major browsers for my use case (which was a simple scroll loader). To avoid using setTimeout and ONLY execute a specific function within a set duration, create this function object literal:
var hlpr = {
lastExec: new Date().getTime(),
isThrottled: function (timeout) {
if ((new Date().getTime() - this.lastExec) < timeout) {
console.log('returned');
return false;
}
this.lastExec = new Date().getTime();
return true;
}
};
And then add this to the function you have bound to your scroll event:
if (!hlpr.isThrottled(500))
return;
This way, the scrollbar event can fire off as weirdly as it wants but your attached function will never execute twice within a certain interval. The double-firing of the scroll event happens much faster than 500ms but since my usage was for a scroll loader, all I needed to ensure was that the function fired once within a 500ms window.
I would like to know if it is possible to fire an event after the scrolling of a page, when using the scrollbar or mouse-wheel (or with a swipe on a touch device).
Basically, I'd like to detect when the user has stopped scrolling so I can then AJAX-load, rather than loading while scrolling.
It seems that jQuery's .scroll() is firing every time a user scrolls, and it seems clunky to have an event fire all the time. Is there such thing as .onScrollAfter(), synonymous to the .onMouseUp()?
I'd like to know whether this is possible (or if a function already exists) without using a framework, though I would consider one; especially jQuery.
This event does not exist. You can emulate it by using timeouts:
Example (concept code):
(function() {
var timer;
/* Basic "listener" */
function scroll_finish(ev) {
clearTimeout(timer);
timer = setTimeout(scroll_finished, 200, ev);
//200ms. Too small = triggered too fast. Too high = reliable, but slow
}
window.onscroll = scroll_finish; // Or addEventListener, it's just a demo
// Fire "events"
var thingey = [];
function scroll_finished(ev) {
// Function logic
for (var i=0; i<thingey.length; i++) {
thingey[i](ev);
}
}
// Add listener
window.addScrollListener = function(fn) {
if (typeof fn === 'function') {
thingey.push(fn);
} else {
throw TypeError('addScrollListener: First argument must be a function.');
}
}
window.removeScrollListener = function(fn) {
var index = thingey.indexOf(fn);
if (index !== -1) thingey.splice(index, 1);
}
})();
Thought I would add this as an answer even though it's old. The event you are trying to recreate I believe is synonymous to debounce. This is available in underscore.js
debounce_.debounce(function, wait, [immediate])
Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked. Useful for implementing behavior that should only happen after the input has stopped arriving. For example: rendering a preview of a Markdown comment, recalculating a layout after the window has stopped being resized, and so on.
So it will wait after your last execution of the specific event. if you do not want a delay, you can just specify 0. David Walsh has a pretty nice implementation you can include in any project.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
Which you can go ahead adding by doing
var myEfficientFn = debounce(function() {
// All the taxing stuff you do
}, 250);
window.addEventListener('scroll', myEfficientFn);
Description
You can use the nice jQuery plugin Special scroll events for jQuery by James Padoley.
Works really great.
Check out the page and this jsFiddle Demonstration (Just scroll ;))
More Information
Special scroll events for jQuery
jsFiddle Demonstration