I'm working on a page with a simple side nav that has a fixed position, set top: 30%. What I need to happen is at the bottom of the page I want to fade the menu out so it doesn't overlap the footer, the code below works but I think it is checking on scroll so much it takes to long to calculate when you scroll down fast.
Is there a faster/more lightweight way to calculate when to hide the side-nav? I'm not familiar with debouncing but would it help?
Elements:
.body-container-wrapper - total height of page
.footer-container-wrapper - total height of the footer that we want the nav to be hidden at
.internal-side-nav - the menu position: fixed, top: 30%, right: 0
Example Page: http://hsb1.hubspot.com/profile-page-template
Script:
<script type="text/javascript">
$(document).scroll(function () {
var y = $(this).scrollTop();
if (y < $('.body-container-wrapper').outerHeight() - $('.footer-container- wrapper').outerHeight() - 400 ) {
$('.internal-side-nav').fadeIn();
} else {
$('.internal-side-nav').fadeOut();
}
});
</script>
I hadn't heard of debounce so I had to look it up. It could potentially help, but that would be an extra plugin you'd have to include and maintain and it might not work exactly how you want (I didn't see anything indicating that it does "bunches or time frames", just seemed to be bunches, which means it might fire late on you).
Instead, what you could do is throttle it yourself with a little bit of timing.
var scrollTimeInterval = 200; // how often we allow the action to trigger, in ms.
var lastScrollTime = 0;
var scrollTimeoutId = null;
$(document).scroll(function () {
var now = (new Date()).getTime();
var dScrollTime = now - lastScrollTime;
lastScrollTime = now;
if (dScrollTime < scrollTimeInterval) {
// Set a timeout so we catch the last one.
scrollTimeoutId = setTimeout(function() { $(document).scroll(); }, scrollTimeInterval - dScrollTime);
return; // too soon, so we'll skip
}
// Clear any potentially pending timeout.
clearTimeout(scrollTimeoutId);
var y = $(this).scrollTop();
if (y < $('.body-container-wrapper').outerHeight() - $('.footer-container- wrapper').outerHeight() - 400 ) {
$('.internal-side-nav').fadeIn();
} else {
$('.internal-side-nav').fadeOut();
}
});
With this, the scroll event simply won't do anything if it hasn't been a certain amount of time since it last triggered. To ensure we catch the last scroll event (the last one before they stopped scrolling), I added a timeout which will trigger the scroll event one last time. We also have to make sure to clear that if we handle another scroll event before it fires (so we don't double up on it).
You can control the time interval it allows with the first variable. If 200ms feels a bit sluggish, you can reduce it to 100ms or 50ms and see if that gives a better balance.
Hope that helps.
Dealing with scroll events, in certain circumstances, there's not much you can do.
Solution 1: setTimeout that cancels itself on each iteration
This method is most efficient I believe, but maybe not ideal for you, because there will still be 300ms in which the sidenav would visually overlap the footer before it fades out.
var scrollTimeout = false;
$(document).scroll(function(){
clearTimeout(scrollTimeout);
var _this = this; // for the setTimeout function to know what "this" is
scrollTimeout = setTimeout(function(){
// your original code block goes here
// BUT, replace "this" with "_this"
}, 300);
});
The above essentially only runs your code when the user has not scrolled for at least 300 milliseconds.
Solution 2: Just good old optimization (not too many tricks)
This solution should hide the sidenav immediately, but still runs on every scroll, just does less
var myScrollEvent = (function(){
var $win = $(window);
var $foot = $('.footer-container-wrapper');
var footer_top = $foot.offset().top; // where on the page the footer begins
var $nav = $('.internal-side-nav');
var nav_height = $nav.height(); // maybe use outerHeight(true)?...who knows, only you
var is_hidden = false;
// this is the actual function we want to run on-scroll
return function(){
// jquery, even on fixed elements, still seems to account for scroll position
// when calculating top offset value, below is the coordinate of the bottom of the nav
var nav_bottom = $nav.offset().top + nav_height;
// if the bottom coord of the nav is lower than the top coord of the footer
if(nav_bottom > footer_top && !is_hidden){
is_hidden = true;
// hide it code
}else if(is_hidden){
is_hidden = false;
// show it code
}
};
})();
$(window).scroll(myScrollEvent);
The idea here is to cache some variables and also do the calculation a slightly different way. Your way doesn't seem by any means wrong, but this is just an alternative. Note that with this method, we're assuming the nav height will never change.
You could always combine the two solutions if you'd like as well.
Also, note that I haven't done any browser-2-browser testing, so if there are any flaws, of course let me know.
Related
So I'm trying to get this element to scroll which it does but I'd like it to stop scrolling before the footer.
At the moment I have this but the pages don't have the same length so the >= 17900 is not a good solution for me.
$(window).scroll(function (event) {
var windowTop = $(this).scrollTop();
if (windowTop >= 17900) {
$(".product-form__item--quantity").addClass("non-fixed");
$(".product-form__item--submit").addClass("non-fixed");
$("#ProductPhotoImg").addClass("non-fixed");
$("#option_total").addClass("non-fixed");
$(".product-single__title").addClass("non-fixed");
$(".product-form__item--quantity").removeClass("change");
$(".product-form__item--submit").removeClass("change");
$("#ProductPhotoImg").removeClass("change");
$("#option_total").removeClass("change-option");
$(".product-single__title").removeClass("change");
} else {
//console.log('a');
$(".product-form__item--quantity").removeClass("non-fixed");
$(".product-form__item--submit").removeClass("non-fixed");
$("#ProductPhotoImg").removeClass("non-fixed");
$("#option_total").removeClass("non-fixed");
$(".product-single__title").removeClass("non-fixed");
}
});
Thanks for the help
You have more issues than only finding the footer's position here...
First is to find the position of the footer instead of hardcoding a value.
Okay...
Second is that you constantly add and remove classes on scroll.
This sure isn't the desired effect.
The scroll event fires like a dozen times or more on a single mouse wheel spin.
Third is that you force jQuery to lookup for elements, as #Taplar mentionned in comments, each times the script executes (Which is real bad if the script execute constantly!!). This is bad... And unuseful, since this those elements don't change.
So I modified your script... Almost completely :
;)
// Define an element collection ONCE.
var elementsList = $(".product-form__item--quantity, .product-form__item--submit, #ProductPhotoImg, #option_total, .product-single__title");
// Find the footer's position.
var footerPosition = $("#footer").offset().top;
// Set a flag to prevent the the script action when already done.
var footerVisible = false;
$(window).scroll(function (event) {
// How many pixels scrolled + viewport height = position of the last pixel at the bottom of the viewport relative to the document's top.
var viewportBottom = $(this).scrollTop() + $( window ).height();
if (viewportBottom >= footerPosition) {
if(!footerVisible){
// Will update classes on the element in the elementslist collection on user scroll enought to show the footer in viewport.
elementsList.addClass("non-fixed").removeClass("change change-option");
// Set a flag
footerVisible = true;
}
} else {
if(footerVisible){
// Will update classes on the element in the elementslist collection on user scroll from a "visible footer" to a footer below the viewport.
// In other words, You don't want to do it CONSTANTLY except when the footer is visible and dissapears due to user scroll up.
elementsList.removeClass("non-fixed");
// reset the flag.
footerVisible = false;
}
}
});
ive wrote a simple script that isnt working as intentioned:
var prevscroll = 0;
$(document).scroll(function(){
var currscroll = $(document).scrollTop();
if(currscroll > prevscroll){
$("header").toggle();
prevscroll = currscroll;
}
if(currscroll < prevscroll){
$("header").toggle();
prevscroll = currscroll;
}
});
When the page is scrolled down, its meant to hide the header and only show it if its scrolled up however whats happening is as i scroll down its flicking on and off. :|
I think I may of just spotted a silly error! ofcourse its going to keep flickering since prev scroll keeps increasing.
You can't use toggle() the way you are because the scroll event is called many, many times so if it's called more than once with the same value for your if statement, then it will flicker on, then off, then on, then off each time you call .toggle(). You need to explicitly hide or show when your condition is met.
I don't quite follow exactly what you're trying to do with prevscroll, but perhaps this is what you want:
var prevscroll = 0;
$(document).scroll(function(){
var currscroll = $(document).scrollTop();
if(currscroll > prevscroll){
$("header").hide();
prevscroll = currscroll;
}
if(currscroll < prevscroll){
$("header").show();
prevscroll = currscroll;
}
});
This will hide the header anytime you are scrolling down further and show the header as soon as you start to scroll up.
I have a navigation container near the top of the page that should add or remove the classname "stuck" (switching between position:static and position:fixed) when the page scrolls beyond a certain value. Seems to work fine in FF and Chrome, but of course IE (7,8 and 9) is having trouble.
The page lags heavily (essentially unusable) when scrolling using the mousewheel, although if I grab and drag the horiz scrollbar then the page slides smoothly with no lag.
My searching around revealed that it's probably because IE executes way more scroll events than the other browsers, but I can't figure out exactly how to throttle the number of events being fired. You can see in the code block below that I'm also using a 'scroll stop' solution but I really need to also be able to execute a callback WHILE the user is still scrolling when they go beyond a certain point on the page.
I thought the way I was implementing it was pretty and stripped down and basic, but is there a better way to handle this, at least just for IE?
var scrollValue = 0;
var scrollTimer = false;
$(window).bind('scroll', function(){
scrollValue = $(window).scrollTop();
// SET TIMER DELAY FOR SCROLL-STOP
if (scrollTimer) {
clearTimeout(scrollTimer);
}
scrollTimer = setTimeout(scrollStopped, 25);
// STICK/UNSTICK HEADER
if (scrollValue > 320){
if (!$(stickyWrap).hasClass('stuck')){
$(stickyWrap).addClass('stuck')
}
} else {
if ($(stickyWrap).hasClass('stuck')){
$(stickyWrap).removeClass('stuck');
}
}
});
Down with timeout, up with switch
If you made the jQuery a little more simple, and added a switch to only execute anything once before and after the threshold, it should speed things up nicely.
var header = $('.stickyWrap'),
trig = 320,
go = true;
$(window).bind('scroll', function(){
var scrollValue = $(this).scrollTop();
if ((go && scrollValue > trig) || (!go && scrollValue <= trig)) {//before or after
header.toggleClass('stuck');//toggle class
go ? go = false : go = true;//toggle boolean
}
});
Now it will only try to execute anything only once before and once after it crosses the threshold of 320.
Made A Fiddle >
I made a image scrolling with the mouse.
The image scroll to a position based on the mouse position percentage of the window height.
$(imageContainer).mouseenter(function(e){
var scrollingTo = ((e.pageY/$(this).height())-.083) * ( $(imageContainer).prop('scrollHeight') - $(imageContainer).height() );
hijacked = true;
$(imageContainer).animate({scrollTop:scrollingTo},300,function(){hijacked=false;});
}).mousemove(function(e){
if(hijacked) return;
var scrollingTo = ((e.pageY/$(this).height())-.083) * ( $(imageContainer).prop('scrollHeight') - $(imageContainer).height() );
$(imageContainer).scrollTop(scrollingTo);
});
So. in that line
$(imageContainer).animate({scrollTop:scrollingTo},300,function(){hijacked=false;});
I want that scrollingTo change. Because during the animation, the user can move the mouse, changing the scrollingTo variable.
Alright, I managed to cook together a hacky way of dynamically altering an animation. My understanding of the internal animation queue for jQuery is not great, but as far as I know there's no way to alter a queued animation, other than to make it stop. Anyway, here's the key code for an example that alters position, which should be adaptable to scrolling (in fiddle form):
$(document).ready(function () {
var last_update = 0;
$(document).on("mousemove", function (e) {
if (Date.now() - last_update > 50) {
$mover = $("#mover");
$mover.stop();
$mover.animate({ left: e.pageX, top: e.pageY}, 200, "linear");
last_update = Date.now();
}
});
});
There were a couple of tricks to make it work, I'll go through them and try to explain how I believe they could be adapted to scrolling:
The main idea is that on mousemove, the prior event is cancelled, and a new one is started.
I don't believe this will require any changes for scrolling.
Some forms of animation accelerate/decelerate over the course of the animation - it's too hard to preserve this in a constantly changing animation (at least without writing a custom animation function), so the animation easing is set to "linear".
rapidly changing animations takes time (especially for an event as common as mousemove), so there's a limit on how often the animation can change. Before a change to the animation is made, it's ensured that no changes have been made in the last .05 seconds (this is done with "last_update").
I believe if you just swap out the animation properties for your own (scrollTop), this should do what you're looking for.
I have a horizontally scrolling website, and I have a block that I want to stay in frame at all times as the user scrolls right. It looks perfectly smooth in webkit browsers, but is crazy jagged in Firefox and I don't really care about IEs.
function fixMyId(){
$('#myId').css({'margin-left': 150 + $(window).scrollLeft()});
}
function fixMyIdAlt(){
$('#myId').stop().animate({'margin-left': 150 + $(window).scrollLeft()}, 300);
}
And then I have it triggered on window scroll.
What would be a best way to average out the scrolling, so that maybe every so many seconds or pixels of scrolling it fires the function, or upon stopping the scrolling it animates the block into place? I tried playing with delay() but that doesn't do anything. And this one just looks stupid (plus I have no idea what the overhead of this kind of crunching is):
function fixMyIdStupid(){
window.scrollCounter++;
if(window.scrollCounter % 20 == 0) $('#myId').stop().animate({'margin-left': 150 + $(window).scrollLeft()}, 300);
}
So what do I do? setTimeout and setInterval may be required, but those always make my head hurt.
EDIT: Here's a jsfiddle of it in action: http://jsfiddle.net/xsxSq/
The #f0f square is the #myId.
I tried to do such things as well, problem is that the scroll event isn't fired as much as you want. A nice workaround was subscribing the calculation function to the mousemove event, so it triggers A LOT. But on the other hand, I came up with another solution.
Why not turn things around and ask yourself:
Lets make it a position:fixed object and calculate what happens on resize. Because you actually are trying to create a position-x:fixed; and a position-y:absolute;
I actually did the following for the opposite kind of thing. A block that has to be exactly in the middle of the x-document, but in the y it was fixed.
$(document).ready(function ()
{
replaceFixed();
$(window).resize(replaceFixed);
$('#content').ajaxSuccess(replaceFixed);
$(window).scroll(replaceFixed);
function replaceFixed()
{
var jEl = $('#centeredFixedContainer');
var winW = $(window).width();
var docW = $(document).width();
var scrL = $(window).scrollLeft();
var divW = jEl.width();
var result = 0;
// window bigger than the element
if(winW > divW)
{
result = -scrL + ((docW-winW)/2);
}
else
{
result = $('#mainContainer').offset().left - scrL;
}
jEl.css('left',result);
}
});
Copying this code will not give you the solution, but will indicate another way to look at your problem.