Too many scroll events for smooth scrolling - javascript

Hello there I've been trying to find a fix for the many scroll events firing on one scroll. This is the only thing close to working for me so far. I want to smoothscroll between two divs (#boxes and #header) I want to use the scroll bar to trigger this smooth scroll and not a button. Any suggestions on how to only take one scroll event? I also used solutions based from prev stackoverflow questions. I used my own locator instead of offsets because thats also unreliable
$(window).scroll(function () {
if (timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(function () {
if (locator == 0) {
id = $("#boxes");
locator = 1;
} else if (locator = 1) {
id = $("#header");
locator = 0;
}
// target element
var $id = $(id);
if ($id.length === 0) {
return;
}
// top position relative to the document
var pos = $id.offset().top;
// animated top scrolling
$('html, body').animate({scrollTop: pos}, 1500, function () {
$('html, body').clearQueue();
$('html, body').stop();
});
}, 2);
});

So, to be clear, you want any minor scroll event to scroll between one item and the other? Note that when a user scrolls, there is a "momentum" that the browser implements, and you'll be battling with that.
Regardless: You don't need to wrap this in a setTimeout. Right now, your javascript is creating a new setTimeout function that is being fired every 2ms. Scroll events occur with every pixel of movement in the scroll, so if you scroll 100px, you're going to be firing 100 times every 2ms. (That's 50,000 times).
Instead, have a a variable (isScrolling) track the state, so, if you're in the middle of scrolling, the function won't fire.
var isScrolling = false;
var locator = 0;
$(window).scroll(function () {
if (isScrolling) return false;
if (locator == 0) {
id = $("#boxes");
locator = 1;
} else if (locator = 1) {
id = $("#header");
locator = 0;
}
// target element
var $id = $(id);
if ($id.length === 0) {
return;
}
// top position relative to the document
var pos = $id.offset().top;
// animated top scrolling
isScrolling = true;
$('html, body').animate({scrollTop: pos}, 1500, function () {
$('html, body').clearQueue();
$('html, body').stop();
isScrolling = false;
});
});
Here's a JSbin: http://jsbin.com/jugefup/edit?html,css,js,output

Related

jQuery "Snap To" Effect

I have a specific effect I want for a website I'm building. As you can see in this website, I want the screen to "snap to" the next section after the user scrolls, but only after (not the instant) the scroll event has fired. The reason I don't want to use a plugin like panelSnap is because I
1: Want smaller code and
2. Want the website, when viewed on mobile, to have more of the "instant snap" effect (try reducing the browser size in the website mentioned above). I know I theoretically could try combining two plugins, like panelsnap and scrollify, and activate them appropriately when the browser is a certain width, but I don't know if I want to do that... :(
So all of that said, here's the code:
var scrollTimeout = null;
var currentElem = 0;
var options = {
scrollSpeed: 1100,
selector: 'div.panels',
scrollDelay: 500,
};
$(document).ready(function() {
var $snapElems = $(options.selector);
console.log($($snapElems[currentElem]).offset().top);
function snap() {
if ($('html, body').scrollTop() >= $($snapElems[currentElem]).offset().top) {
if (currentElem < $snapElems.length-1) {
currentElem++;
}
}else{
if (currentElem > 0) {
currentElem = currentElem - 1;
}
}
$('html, body').animate({
scrollTop: $($snapElems[currentElem]).offset().top
}, options.scrollSpeed);
}
$(window).scroll(function() {
if ($(window).innerWidth() > 766) {
if (scrollTimeout) {clearTimeout(scrollTimeout);}
scrollTimeout = setTimeout(function(){snap()}, options.scrollDelay);
}else{
//I'll deal with this later
}
});
});
My problem is that every time the snap function is called, it triggers the scroll event, which throws it into a loop where the window won't stop scrolling between the first and second elements. Here's the poor, dysfunctional site: https://tcfchurch.herokuapp.com/index.html Thank for the help.
You can use a boolean to record when the scroll animation in snap is in progress and prevent your $(window).scroll() event handler from taking any action.
Here's a working example:
var scrollTimeout = null;
var currentElem = 0;
var options = {
scrollSpeed: 1100,
selector: 'div.panels',
scrollDelay: 500,
};
$(document).ready(function() {
var scrollInProgress = false;
var $snapElems = $(options.selector);
console.log($($snapElems[currentElem]).offset().top);
function snap() {
if ($('html, body').scrollTop() >= $($snapElems[currentElem]).offset().top) {
if (currentElem < $snapElems.length-1) {
currentElem++;
}
}else{
if (currentElem > 0) {
currentElem = currentElem - 1;
}
}
scrollInProgress = true;
$('html, body').animate({
scrollTop: $($snapElems[currentElem]).offset().top
}, options.scrollSpeed, 'swing', function() {
// this function is invoked when the scroll animate is complete
scrollInProgress = false;
});
}
$(window).scroll(function() {
if (scrollInProgress == false) {
if ($(window).innerWidth() > 766) {
if (scrollTimeout) {clearTimeout(scrollTimeout);}
scrollTimeout = setTimeout(function(){snap()}, options.scrollDelay);
}else{
//I'll deal with this later
}
}
});
});
The variable scrollInProgress is set to false by default. It is then set to true when the scroll animate starts. When the animate finishes, scrollInProgress is set back to false. A simple if statement at the top of your $(window).scroll() event handler prevents the handler from taking any action while the animate scroll is in progress.
Have you considered using the well known fullPage.js library for that? Check out this normal scroll example. The snap timeout is configurable through the option fitToSectionDelay.
And nothing to worry about the size... it is 7Kb Gzipped!
I know I theoretically could try combining two plugins, like panelsnap and scrollify, and activate them appropriately when the browser is a certain width, but I don't know if I want to do that
fullPage.js also provides responsiveWidth and responsiveHeight options to turn it off under certain dimensions.

Setting Scroll on Slider Div

I am building a news feed with slides that show a summary and a read more button that when clicked reveals the rest of the content.
Here is a jsFiddle: http://jsfiddle.net/pbunz5ue/1/
When Read More is clicked the story opens and the scrolling stops, then the user clicks Less, its hides the story and continues scrolling.
When Read More is clicked I need the scroll of the div to be aligned so that the opened story starts at the top of the div. My slider does this perfectly the first round the stories make, once they are displayed a second time from the button up this no longer works.
Can someone explain why ??
Here is my code:
JS:
$(document).ready(function() {
//User clicks Read More, add 'open' class to news item
$('.news-read-more').on('click', function() {
blockedSlider = true;
clearInterval(myTimer);
$('.news-list').children('li').each(function() {
$(this).removeClass('open');
});
$(this).parent().toggleClass('open');
var n = $(this).parent();
var pos = n.position();
$('.news-slider-wrapper').scrollTop(pos.top);
});
//User clicks Less, remove 'open' class from news item
$('.news-read-less').on('click', function() {
if (blockedSlider == true) {
blockedSlider = false;
$(this).parent().removeClass('open');
myTimer = setInterval(slideLoop, 2000)
}
});
var myTimer = setInterval(slideLoop, 2000)
var blockedSlider = false;
function slideLoop() {
// Work out width of current slider size
var widthPx = $('.news-list-item').css('height');
var width = widthPx.substring(0, widthPx.length - 2);
// Work out current left
var left = $('.news-list').css('top');
left = left.substring(0, left.length - 2);
if (left <= -(width * 2)) {
var neg = '-' + widthPx;
$('.news-list').css('top', neg);
var slide = $('.news-list li:first');
$('.news-list').children('li:first').remove();
$('.news-list ').append(slide);
//User clicks Read More, add 'open' class to news item
$('.news-read-more').on('click', function() {
blockedSlider = true;
clearInterval(myTimer);
$('.news-list').children('li').each(function() {
$(this).removeClass('open');
});
$(this).parent().toggleClass('open');
var n = $(this).parent();
var pos = n.position();
$('.news-slider-wrapper').scrollTop(pos.top - 360);
});
//User clicks Less, remove 'open' class from news item
$('.news-read-less').on('click', function() {
if (blockedSlider == true) {
blockedSlider = false;
$(this).parent().removeClass('open');
myTimer = setInterval(slideLoop, 2000)
}
});
var move = "-=" + widthPx;
$('.news-list').animate({ top: move }, "slow", "swing");
}
else {
var move = "-=" + widthPx;
$('.news-list').animate({ top: move }, "slow", "swing");
}
}
});
The problem is caused by negative "top" position of .news-list that busts the position of the opened element.
-> I update your fiddle: http://jsfiddle.net/pbunz5ue/3/ <-
What I've changed
the main error:
when you calculate the position you've not considered the negative top offset of the news container .news-list when it slide up:
I've changed this two lines:
var pos = n.position();
$('.news-slider-wrapper').scrollTop(pos.top);
in:
var pos = n.position().top + parseInt($(".news-list").css("top")) || n.position().top;
$('.news-slider-wrapper').scrollTop(pos);
If $(".news-list") has top position I add It to the calculation of position (if the result is auto there is a fallback that prevent NaN result || n.position().top)
You had duplicated the handler $('.news-read-more').on('click') and $('.news-read-less').on('click'); I deleted those within the function because they seems useless.
EDIT:: .on click issue
Try this two way to solve your problem:
1.
If you have some problem when new item was added into your list try to bind the event to the "items" container .news-list and filter the selector every time it will fire:
$('.news-read-more').on('click', function() { //...
$('.news-read-less').on('click', function() { //...
becomes:
$('.news-list').on('click','.news-read-more', function() { //...
$('.news-list').on('click','.news-read-less', function() { //...
the container .news-list never change during your animation, and DOM manipulation.
2.
An other way is to change this rows, in your code:
var slide = $('.news-list li:first');
$('.news-list').children('li:first').remove();
$('.news-list ').append(slide);
becomes:
var slide = $('.news-list li:first');
$('.news-list ').append(slide);
or:
$('.news-list ').append('.news-list li:first');
if you .remove the element, (I think) you unbind all event because you remove the element from DOM, but you not need to remove you need to move it from top to bottom. This way prevent the unbind of click event.
I hope to had understand the problem :).
sorry for may bad english

Track the scroll position beyond elements

I'm putting together a jQuery plugin. The plugin takes panels, and auto-sizes their height. So I start off with something like this:
<div class="panel">Test 1</div>
<div class="panel">Test 2</div>
<div class="panel">Test 3</div>
The code for that looks something like:
sizePanels: function(){
panels.each(function(){
$(this).height(docHeight);
});
},
There is a down button, that when clicked, will take the user to the next $(".panel):
nextPanel: function(){
$.scrollTo($(".panel:eq(" + panelIndex + ")"), 250, { easing: "swing" });
}
With that, I'm keeping track of the panel index that their on:
if (panelIndex < (panelCount - 1) ) {
panelIndex += 1;
}
I'm trying to figure out a way to track if they happen to scroll manually, and pass one of the elements, to then increase the "panelIndex", so that the button doesn't move them up instead of down because it was never incremented properly due to the user using the scroll bar instead of the button. This is what I have so far:
$(window).scroll(function(){
panels.each(function(index){
if ($(window).scrollTop() > $(this).scrollTop()) {
panelIndex = index;
// console.log(index);
}
});
if (panelIndex < panelCount - 1){
s.showDownButton();
}
});
The code excessively checks and feels somewhat overboard. is there a better way to do it?
An easy optimization is to only calculate the scrollTop once and to exit the each loop when a match is found. You can exit a $.each loop by returning false.
$(window).scroll(function(){
var scrollTop = $(window).scrollTop();
panels.each(function(index){
if (scrollTop > $(this).scrollTop()) {
panelIndex = index;
} else {
return false;
}
});
if (panelIndex < panelCount - 1){
s.showDownButton();
}
});
The next way that I would suggest optimizing this is to pre-calculate the scrollTop of each panel on page load (and when the viewport is resized). If you store these values in an array, then you can loop through them very quickly.
Here is some rough code to illustrate the idea:
var panelTops = [];
findPanelTops(); // calculate on load
$(window).on("resize", findPanelTops); // calculate on resize
function findPanelTops() {
panelTops = [];
panels.each(function(index) {
panelTops.push($(this).scrollTop());
});
}
$(window).scroll(function(){
var scrollTop = $(window).scrollTop();
for (var i = 0; i < panelTops.length; i++) {
if (scrollTop > panelTops[i]) {
panelIndex = i;
} else {
break;
}
};
if (panelIndex < panelCount - 1){
s.showDownButton();
}
});
The scroll event can fire a lot and very quickly, so you want to keep the amount of computation as minimal as possible. One way to get around all of this is to implement a scrollend handler. This will only fire when the scroll event has appeared to have stopped.
Here is some basic code for doing that. It will fire when the scroll event has stopped for more than 500ms:
var scrollTimeout = null;
function onScroll() {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(onScrollEnd, 500);
}
function onScrollEnd() {
// Scrolling has stopped
// Do stuff ...
scrollTimeout = null;
}
$(window).on("scroll", onScroll);

Cancel scrolling after user interaction

My webpage animates scrolling when users click on links to the same page. I want to cancel this animation as soon as the user tries to scroll (otherwise the user and the browser are fighting for control) – no matter whether with the mouse wheel, the keyboard or the scrollbar (or any other way – are there other ways of scrolling?). I managed to cancel the animation after the mouse wheel or keyboard are used, how do I get this working with the scrollbar?
Here is how my code looks for the keyboard:
$(document.documentElement).keydown( function (event) {
if(event.keyCode == 38 || 40) stopScroll();
});
function stopScroll() {
$("html, body").stop(true, false);
}
I also tried a more elegant way of doing this by using scroll(), the problem is that scroll() catches everything including the animated and automated scrolling. I could not think of any way to let it catch all scrolling except the animated scrolling.
you need animation marker, something like this
$("html, body").stop(true, false).prop('animatedMark',0.0).animate({scrollTop : top, animatedMark: '+=1.0'})
Here is the code, the code was mix of GWT and javascript so moved it to js, not fully tested, please try it
var lastAnimatedMark=0.0;
function scrollToThis(top){
// Select/ stop any previous animation / reset the mark to 0
// and finally animate the scroll and the mark
$("html, body").stop(true, false).prop('animatedMark',0.0).
animate({scrollTop : top, animatedMark: '+=1.0'}
,10000,function(){
//We finished , nothing just clear the data
lastAnimatedMark=0.0;
$("html, body").prop('animatedMark',0.0);
});
}
//Gets the animatedMark value
function animatedMark() {
var x=$("html, body").prop('animatedMark');
if (x==undefined){
$("html, body").prop('animatedMark', 0.0);
}
x=$("html, body").prop('animatedMark');
return x;
};
//Kills the animation
function stopBodyAnimation() {
lastAnimatedMark=0;
$("html, body").stop(true, false);
}
//This should be hooked to window scroll event
function scrolled(){
//get current mark
var currentAnimatedMark=animatedMark();
//mark must be more than zero (jQuery animation is on) & but
//because last=current , this is user interaction.
if (currentAnimatedMark>0 && (lastAnimatedMark==currentAnimatedMark)) {
//During Animation but the marks are the same !
stopBodyAnimation();
return;
}
lastAnimatedMark=currentAnimatedMark;
}
Here is the blog about it
http://alaamurad.com/blog/#!canceling-jquery-animation-after-user-interaction
Enjoy!
Here's a jquery function that should do the trick:
function polite_scroll_to(val, duration, callback) {
/* scrolls body to a value, without fighting the user if they
try to scroll in the middle of the animation. */
var auto_scroll = false;
function stop_scroll() {
if (!auto_scroll) {
$("html, body").stop(true, false);
}
};
$(window).on('scroll', stop_scroll);
$("html, body").animate({
scrollTop: val
}, {
duration: duration,
step: function() {
auto_scroll = true;
$(window).one('scroll', function() {
auto_scroll = false;
});
},
complete: function() {
callback && callback();
},
always: function() {
$(window).off('scroll', stop_scroll);
}
});
};
It's not very elegant, but you could use a flag of some kind to detect what type of scrolling you're dealing with (animated or 'manual') and always kill it when it's animated. Here's an untested example:
var animatedScroll = false;
// you probably have a method looking something like this:
function animatedScrollTo(top) {
// set flag to true
animatedScroll = true;
$('html').animate({
scrollTop : top
}, 'slow', function() {
// reset flag after animation is completed
animatedScroll = false;
});
}
function stopScroll() {
if (animatedScroll) {
$("html, body").stop(true, false);
}
}

Adding listener for position on screen

I'd like to set something up on my site where when you scroll within 15% of the bottom of the page an element flyouts from the side... I'm not sure how to get started here... should I add a listener for a scroll function or something?
I'm trying to recreate the effect at the bottom of this page: http://www.nytimes.com/2011/01/25/world/europe/25moscow.html?_r=1
update
I have this code....
console.log(document.body.scrollTop); //shows 0
console.log(document.body.scrollHeight * 0.85); //shows 1038.7
if (document.body.scrollTop > document.body.scrollHeight * 0.85) {
console.log();
$('#flyout').animate({
right: '0'
},
5000,
function() {
});
}
the console.log() values aren't changing when I scroll to the bottom of the page. The page is twice as long as my viewport.
[Working Demo]
$(document).ready(function () {
var ROOT = (function () {
var html = document.documentElement;
var htmlScrollTop = html.scrollTop++;
var root = html.scrollTop == htmlScrollTop + 1 ? html : document.body;
html.scrollTop = htmlScrollTop;
return root;
})();
// may be recalculated on resize
var limit = (document.body.scrollHeight - $(window).height()) * 0.85;
var visible = false;
var last = +new Date;
$(window).scroll(function () {
if (+new Date - last > 30) { // more than 30 ms elapsed
if (visible && ROOT.scrollTop < limit) {
setTimeout(function () { hide(); visible = false; }, 1);
} else if (!visible && ROOT.scrollTop > limit) {
setTimeout(function () { show(); visible = true; }, 1);
}
last = +new Date;
}
});
});
I know this is an old topic, but the above code that received the check mark was also triggering the $(window).scroll() event listener too many times.
I guess twitter had this same issue at one point. John Resig blogged about it here: http://ejohn.org/blog/learning-from-twitter/
$(document).ready(function(){
var ROOT = (function () {
var html = document.documentElement;
var htmlScrollTop = html.scrollTop++;
var root = html.scrollTop == htmlScrollTop + 1 ? html : document.body;
html.scrollTop = htmlScrollTop;
return root;
})();
// may be recalculated on resize
var limit = (document.body.scrollHeight - $(window).height()) * 0.85;
var visible = false;
var last = +new Date;
var didScroll = false;
$(window).scroll(function(){
didScroll = true;
})
setInterval(function(){
if(didScroll){
didScroll = false;
if (visible && ROOT.scrollTop < limit) {
hideCredit();
visible = false;
} else if (!visible && ROOT.scrollTop > limit) {
showCredit();
visible = true;
}
}
}, 30);
function hideCredit(){
console.log('The hideCredit function has been called.');
}
function showCredit(){
console.log('The showCredit function has been called.');
}
});
So the difference between the two blocks of code is when and how the timer is called. In this code the timer is called off the bat. So every 30 millaseconds, it checks to see if the page has been scrolled. if it's been scrolled, then it checks to see if we've passed the point on the page where we want to show the hidden content. Then, if that checks true, the actual function then gets called to show the content. (In my case I've just got a console.log print out in there right now.
This seems to be better to me than the other solution because the final function only gets called once per iteration. With the other solution, the final function was being called between 4 and 5 times. That's got to be saving resources. But maybe I'm missing something.
bad idea to capture the scroll event, best to use a timer and every few milliseconds check the scroll position and if in the range you need then execute the necessary code for what you need
Update: in the past few years the best practice is to subscribe to the event and use a throttle avoiding excessive processing https://lodash.com/docs#throttle
Something like this should work:
$(window).scroll(function() {
if (document.body.scrollTop > document.body.scrollHeight * 0.85) {
// flyout
}
});
document.body.scrollTop may not work equally well on all browsers (it actually depends on browser and doctype); so we need to abstract that in a function.
Also, we need to flyout only one time. So we can unbind the event handler after having flyed out.
And we don't want the flyout effect to slow down scrolling, so we will run our flytout function out of the event loop (by using setTimeout()).
Here is the final code:
// we bind the scroll event, with the 'flyout' namespace
// so we can unbind easily
$(window).bind('scroll.flyout', (function() {
// this function is defined only once
// it is private to our event handler
function getScrollTop() {
// if one of these values evaluates to false, this picks the other
return (document.documentElement.scrollTop||document.body.scrollTop);
}
// this is the actual event handler
// it has the getScrollTop() in its scope
return function() {
if (getScrollTop() > (document.body.scrollHeight-$(window).height()) * 0.85) {
// flyout
// out of the event loop
setTimeout(function() {
alert('flyout!');
}, 1);
// unbind the event handler
// so that it's not call anymore
$(this).unbind('scroll.flyout');
}
};
})());
So in the end, only getScrollTop() > document.body.scrollHeight * 0.85 is executed at each scroll event, which is acceptable.
The flyout effect is ran only one time, and after the event has returned, so it won't affect scrolling.

Categories

Resources