I want to animate page scrolling incrementally to different sections of the page when the user scrolls. So I wrote this code:
var $window = $(window);
var sectionHeight = $window.height();
var animating = false;
var dir;
// initialize page position (0)
var pagePos = $(window).scrollTop();
$(window).scroll(function() {
// current page position
var st = $(this).scrollTop();
// whether to animate up or down
dir = ((st > pagePos) ? '+=' : '-=');
// animate
if (animating == false) {
animating = true;
$('html, body').stop().animate({scrollTop: dir+sectionHeight},500, function() {
pagePos = $(window).scrollTop();
animating = false;
});
}
});
The problem is, I get a chained animation after my initial animation down, that animates the page back to the top. I'm not sure why, because it shouldn't animate unless 'animating' is set to false. It only gets set back to false when the animation is complete... right?
One potential fix, although I'm sure somebody can come up with a better solution, is to disable it for a set period of time after it scrolls.
Relevant code:
var ct = new Date().getTime();
if (animating == false && new Date().getTime() > ct + 10)
This will only allow the animation to happen if at least 11ms have passed. Seems to work for values as low as 2. You don't really notice any lag in scrolling but again, I would not consider this an ideal solution.
DEMO
I was able to get it to work, I still don't know why it wouldn't work as it was coded, but here is the solution that worked for me- I was inspired by #sachleen's answer. Thanks sachleen.
I moved the scroll animation into a separate function, then on the callback to the animation function I used a setTimeout with the same duration as the animation to call another function that simply changed the 'animating' boolean back to false.
Related
Disclaimer: I am not a JavaScript developer, I'm a web designer. HTML and CSS, I handle all day, JS, not so much. That's why I'm reaching out for help.
The following script allows for a smooth scroll to the top of the page:
function scrollToTop() {
var position =
document.body.scrollTop || document.documentElement.scrollTop;
if (position) {
window.scrollBy(0, -Math.max(1, Math.floor(position / 10)));
scrollAnimation = setTimeout("scrollToTop()", 30);
} else clearTimeout(scrollAnimation);
}
Is there a way to "stop" the script from executing if the user decides to scroll back down the moment the script is running and taking the user back to the top of the page?
Here's a demo for reference: https://codepen.io/ricardozea/pen/ewBzyO
Thank you.
To specifically detect scrolling back down the page, you could check the old postion against the current position and ensure the scroll is moving in the intended direction:
function scrollToTop(prevPosition) {
// first time round, prevPosition is undefined
var position =
document.body.scrollTop || document.documentElement.scrollTop;
// did page move in non-expected direction? If so, bail-out
if (prevPosition <= position) {
return;
}
var scrollAnimation; //declare this so it doesn't leak onto global scope
if (position) {
var scrollAmt = -Math.max(1, Math.floor(position / 10));
window.scrollBy(0, scrollAmt);
// After timeout, re-call the function with current position.
// Becomes prevPosition for the next time round
scrollAnimation = setTimeout(() => scrollToTop(position), 30);
} else clearTimeout(scrollAnimation);
}
See https://codepen.io/spender/pen/eYvRyox
Why not listen to wheel events? This won't detect dragging the scrollbar with the mouse.
Thanks a lot for your answers!
After consulting with a friend of mine, he provided me with a much succinct way to accomplish the overall behavior of smooth scrolling to the top while solving the potential case of a user wanting to scroll back down during the animation.
Just add this script to a <button> element in the onclick: attribute:
window.scrollTo({top: 0, behavior: "smooth"});
It looks like this:
<button onclick='window.scrollTo({top: 0, behavior: "smooth"});'>Back to Top ↑</button>
Here's a new demo: https://codepen.io/ricardozea/pen/NWpgyjL
I'm trying to achieve this concept, but I'm having some frustration after trying long, convoluted methods that are still causing glitches on mobile, Firefox, etc.
The concept is this:
There are two headlines on the left and right of the page. The one on the left is focused first, and its text is visible while Headline 2 is grayed out and its text is hidden.
Then, if you scroll down, switch the focus to Headline 2, show its text, and hide the text of Headline 1.
Is this just not cleanly achievable? It started out as a fun "what if" concept that I just can't let go.
Currently, I'm trying something like this to capture the scroll:
var lastScrollTimeout;
var scrollDetectionLocked = false;
var lastScrollTop = 0;
$(window).bind('scroll', function(e) {
// do nothing if still locked
if ( scrollDetectionLocked === true )
return false;
// lock it
scrollDetectionLocked = true;
// perform operations if not locked
var direction;
var st = $(this).scrollTop();
if (st > lastScrollTop)// scrolled down
direction = -1;
else // scrolled up
direction = 1;
lastScrollTop = st;
console.log('doScroll('+direction+')');
clearTimeout(lastScrollTimeout)
lastScrollTimeout = setTimeout(function(){
// unlock it after certain time
scrollDetectionLocked = false;
}, timeoutBetweenScrolls);
});
The doScroll() function just sees which Headline/text is currently loaded, and loads the next/previous one accordingly.
However, the console output is sometimes wrong, saying it should doScroll(1) (up) when it should be doScroll(-1) (down) among other things. It just isn't a clean solution in my opinion.
I believe the issue is caused because all elements are fixed on the screen, and the only scrollable item is an element called .fake-scroll. This started out as a fix for mobile phones which don't allow e.originalEvent.wheelDelta, and again, just seems messy.
I've messed with parallax scrolling, but didn't think I could use it to achieve this effect. Any help would be greatly appreciated.
Aside from the "Main | Alt" nav links not staying fixed and the "More" submenu not working, this example shows the last semi-working example I had. https://jsfiddle.net/0vju51ba
I need your help again. A try to code something like this:
I got an imaginary point on screen in pixel (lets say half of body) and I got a element in body. I want to alert if the elements TOP hits the imaginary line and alert if the elements bottom leave imaginary line.
I got something to work (for hit the line) but it only works a few times, but if I scroll fast it doesn't work. If I scroll very very slow and exactly the alert appears:
$(window).scroll(function () {
// A POINT I SET On SCREEN --> IN THIS CASE HALF SIZE OF BODY
var halfBody = $("body").height() / 2;
if ($(window).scrollTop() == halfBody) alert('HIER');
});
I think the problem is the exact pixels i try to match. I think with scrolling I haven't always the exact pixels.
How can this be done so that elements top cross the line alert('element top hits the line') and elements bottom leave the line alert('no element leaving the line'); ?
I hope you understand :)
Scroll events are trigger happy and handing each even can overwhelm your handler.
Try using lodash/underscore debnounce/threshould to handle them every X milliseconds.
It's a common practice to scroll handling in browsers.
Ok i found a nice solution with caching variables and a set timeout:
var timer;
var windowHeight = $(window).height();
var triggerHeight = 0.5 * windowHeight;
$(window).scroll(function() {
if(timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(function() {
// this variable changes between callbacks, so we can't cache it
var y = $(window).scrollTop() ;
if(y > triggerHeight) {
}
if( $(window).scrollTop() == 0 ) { // IF HITS TOP OF PAGE
}
}, 10);
});
maybe it helps someone!
I thought I had resolved this issue but turns out i hadn't. Basically building a tumblr theme and something in my code is conflicting with the jquery animated scroll to top. I've tried removing things as I'm not entirely sure what it could be and thought it might be really obvious to somebody else?
Here is a link to my theme http://minori-theme.tumblr.com/ and a jsfiddle that I was following in order to animate the smooth scroll http://jsfiddle.net/YtJcL/1008/
The code I'm using is below and I'm just using a standard a link and id which are linking fine as it goes to the correct point its just not smooth scrolling.
$(document).ready(function() {
var hashTagActive = "";
$(".scroll").click(function (event) {
if(hashTagActive != this.hash) { //this will prevent if the user click several times the same link to freeze the scroll.
event.preventDefault();
//calculate destination place
var dest = 0;
if ($(this.hash).offset().top > $(document).height() - $(window).height()) {
dest = $(document).height() - $(window).height();
} else {
dest = $(this.hash).offset().top;
}
//go to destination
$('html,body').animate({
scrollTop: dest
}, 2000, 'swing');
hashTagActive = this.hash;
}
});
});
Edit: If anyone has a simpler alternative I'm open to suggestions?
Easing effect on your animate function won't work if you don't add easing libary. jQuery core does not have easing.
<script type="text/javascript" src="http://gsgd.co.uk/sandbox/jquery/easing/jquery.easing.compatibility.js"></script>
Also you can define dynamic value to animate's timing based on your element's offset top value. This will make your animation smoother based on the current value.
Here is jsFiddle with smooth scrolling effect.
$(".scroll").click(function(event){
event.preventDefault();
var dest = 0;
var $target = $(this.hash);
var targetTop = $target.offset().top;
var remainTop = $(document).height() - $(window).height();
if( targetTop > remainTop){
dest = remainTop;
}else{
dest = targetTop;
}
// Dynamic value for timing
var dur = dest*1.2;
$('html,body').animate({scrollTop:dest}, dur,'easeInOutCubic');
});
And one last note, you should orgnize your code with using variables, these coding of your is hard to read and maintain.
The scroll works the first time, but not any time after that. Check through all the var's that get used to see if something is being kept.
EDIT:
The line
if(hashTagActive != this.hash)
will not bother with the jquery animation if you have already clicked "Back to top" because the hashTagActive is already #top. Thats why it works once but not twice.
EDIT 2:
Check out This fiddle for a way around it
I try to make a mousewheel event script, but getting some issues since I'm using an Apple Magic Mouse and its continue-on-scroll function.
I want to do this http://jsfiddle.net/Sg8JQ/ (from jQuery Tools Scrollable with Mousewheel - scroll ONE position and stop, using http://brandonaaron.net/code/mousewheel/demos), but I want a short animation (like 250ms) when scrolling to boxes, AND ability to go throught multiple boxes when scrolling multiple times during one animation. (If I scroll, animation start scrolling to second box, but if I scroll again, I want to go to the third one, and if I scroll two times, to the forth, etc.)
I first thought stopPropagation / preventDefault / return false; could "stop" the mousewheel velocity (and the var delta) – so I can count the number of new scroll events (maybe with a timer) –, but none of them does.
Ideas?
EDIT : If you try to scroll in Google Calendars with these mouses, several calendars are switched, not only one. It seems they can't fix that neither.
EDIT 2 : I thought unbind mousewheel and bind it again after could stop the mousewheel listener (and don't listen to the end of inertia). It did not.
EDIT 3 : tried to work out with Dates (thanks to this post), not optimal but better than nothing http://jsfiddle.net/eZ6KE/
Best way is to use a timeout and check inside the listener if the timeout is still active:
var timeout = null;
var speed = 100; //ms
var canScroll = true;
$(element).on('DOMMouseScroll mousewheel wheel', function(event) {
// Timeout active? do nothing
if (timeout !== null) {
event.preventDefault();
return false;
}
// Get scroll delta, check for the different kind of event indexes regarding delta/scrolls
var delta = event.originalEvent.detail ? event.originalEvent.detail * (-120) : (
event.originalEvent.wheelDelta ? event.originalEvent.wheelDelta : (
event.originalEvent.deltaY ? (event.originalEvent.deltaY * 1) * (-120) : 0
));
// Get direction
var scrollDown = delta < 0;
// This is where you do something with scrolling and reset the timeout
// If the container can be scrolling, be sure to prevent the default mouse action
// otherwise the parent container can scroll too
if (canScroll) {
timeout = setTimeout(function(){timeout = null;}, speed);
event.preventDefault();
return false;
}
// Container couldn't scroll, so let the parent scroll
return true;
});
You can apply this to any scrollable element and in my case, I used the jQuery tools scrollable library but ended up heavily customizing it to improve browser support as well as adding in custom functionality specific to my use case.
One thing you want to be careful of is ensuring that the timeout is sufficiently long enough to prevent multiple events from triggering seamlessly. My solution is effective only if you want to control the scrolling speed of elements and how many should be scrolled at once. If you add console.log(event) to the top of the listener function and scroll using a continuous scrolling peripheral, you will see many mousewheel events being triggered.
Annoyingly the Firefox scroll DOMMouseScroll does not trigger on magic mouse or continuous scroll devices, but for normal scroll devices that have a scroll and stop through the clicking cycle of the mouse wheel.
I had a similar problem on my website and after many failed attempts, I wrote a function, which calculated total offset of selected box and started the animation over. It looked like this:
function getOffset() {
var offset = 0;
$("#bio-content").children(".active").prevAll().each(function (i) {
offset += $(this)[0].scrollHeight;
});
offset += $("#bio-content").children(".active")[0].scrollHeight;
return offset;
}
var offset = getOffset();
$('#bio-content').stop().animate( {
scrollTop: offset
}, animationTime);
I hope it gives you an idea of how to achieve what you want.
you can try detecting when wheel stops moving, but it would add a delay to your response time
$(document).mousewheel(function() {
clearTimeout($.data(this, 'timer'));
$.data(this, 'timer', setTimeout(function() {
alert("Haven't scrolled in 250ms!");
//do something
}, 250));
});
source:
jquery mousewheel: detecting when the wheel stops?
or implement flags avoiding the start of a new animation
var isAnimating=false;
$(document).bind("mousewheel DOMMouseScroll MozMousePixelScroll", function(event, delta) {
event.preventDefault();
if (isAnimating) return;
navigateTo(destination);
});
function navigateTo(destination){
isAnimating = true;
$('html,body').stop().animate({scrollTop: destination},{complete:function(){isAnimating=false;}});
}