jQuery: The 'body' element is activating the scroll event twice - javascript

I've implemented an animation for my photo blog. I still have big problem because the 'body' element is activating the animation twice.
I think the problem stems from the $('body').animate. Because I think that when the body is animating, the scroll event would be activated again and thus triggering the event twice.
The problem of my code is scrolling the page up. When I scroll the page upwards. The scrollAnimatePrev will trigger and then $('body') element will animate itself. After the animation the animating variable is set to false. But the $('body') element triggers the scroll event because I guess when I set the scrollTop the scroll event is triggered. So once again currentPos is set to the $(window).scrollTop() then currentPos > previousPos returns true and !animating returns true so it will trigger the scrollAnimate.
Now I want to fix this. How?
$(function() {
var record = 0;
var imgHeight = $(".images").height();
var offset = $(".images").eq(0).offset();
var offsetHeight = offset.top;
var previousPos = $(window).scrollTop();
var animating = false;
var state = 0;
$(window).scroll(function() {
var currentPos = $(window).scrollTop();
console.log(currentPos);
if(currentPos > previousPos && !animating) {
record++;
scrollAnimate(record, imgHeight, offsetHeight);
animating = true;
} else if (currentPos < previousPos && !animating) {
record--
scrollAnimatePrev(record, imgHeight, offsetHeight);
animating = true;
}
previousPos = currentPos;
console.log(previousPos)
})
function scrollAnimate(record, imgHeight, offsetHeight) {
$('body').animate(
{scrollTop: (parseInt(offsetHeight) * (record+1)) + (parseInt(imgHeight) * record)},
1000,
"easeInOutQuart"
)
.animate(
{scrollTop: (parseInt(offsetHeight) * (record)) + (parseInt(imgHeight) * (record))},
1000,
"easeOutBounce",
function() {
animating = false;
}
)
}
function scrollAnimatePrev(record, imgHeight, offsetHeight) {
$('body').animate(
{scrollTop: ((parseInt(imgHeight) * record) + (parseInt(offsetHeight) * record)) - offsetHeight},
1000,
"easeInOutQuart"
)
.animate(
{scrollTop: ((parseInt(imgHeight) * record) + (parseInt(offsetHeight) * record))},
1000,
"easeOutBounce",
function() {
animating = false;
}
)
}
})

I think it might be firing that callback twice. I had a similar problem recently.
I had something similiar to
$('#id, #id2').animate({width: '200px'}, 100, function() { doSomethingOnceOnly(); })
It was calling my doSomethingOnceOnly() twice, and I thought it must have been the dual selectors in the $ argument. I simply made it 2 different selectors and it worked fine. So like this
$('#id').animate({width: '200px'}, 100);
$('#id2').animate({width: '200px'}, 100, function() { doSomethingOnceOnly(); );

Using a flag to control the trigger did the trick for me.
var targetOffset = 0;
var allow_trigger = true;
$('html,body').animate({scrollTop: targetOffset}, 'slow', function() {
if (allow_trigger) {
allow_trigger = false;
doSomethingOnlyOnce();
}
});

Related

jQuery scrollTop() returns wrong offset on scroll-direction change

I'm trying to get the correct scroll direction via jQuery's "scroll" event.
For this, I'm using the solution here: https://stackoverflow.com/a/4326907/8407840
However, if I change the direction of my scroll, the offset returned by scrollTop is incorrect on the first time. This results in the following behavior:
Wheel down -> down
Wheel down -> down
Wheel up -> down
Wheel up -> up
Wheel down -> up
Wheel down -> down
... and so on, I think you get it.
var ACTIVE_SECTION = null;
var ANIMATION_DURATION = 700;
$(document).ready(function() {
ACTIVE_SECTION = $("section:first-of-type").get(0);
var prevPosition = $(window).scrollTop();
$(window).on("scroll", function() {
doScrollingStuff(prevPosition);
});
});
function doScrollingStuff(prevPosition) {
var ctPosition = $(window).scrollTop();
var nextSection = ACTIVE_SECTION;
// Remove and re-append event, to prevent it from firing too often.
$(window).off("scroll");
setTimeout(function() {
$(window).on("scroll", function() {
doScrollingStuff(prevPosition);
});
}, ANIMATION_DURATION + 100);
// Determine scroll direction and target the next section
if(ctPosition < prevPosition) {
console.log("up");
nextSection = $(ACTIVE_SECTION).prev("section").get(0);
} else if(ctPosition > prevPosition) {
console.log("down");
nextSection = $(ACTIVE_SECTION).next("section").get(0);
}
// If a next section exists: Scroll to it!
if(typeof nextSection != 'undefined') {
var offset = $(nextSection).offset();
$("body, html").animate({
scrollTop: offset.top
}, ANIMATION_DURATION);
ACTIVE_SECTION = nextSection;
} else {
nextSection = ACTIVE_SECTION;
}
console.log(ACTIVE_SECTION);
prevPosition = ctPosition;
}
section {
width:100%;
height:100vh;
padding:60px;
box-sizing:border-box;
}
section:nth-child(1) { background:#13F399; }
section:nth-child(2) { background:#14FD43; }
section:nth-child(3) { background:#4EE61E; }
section:nth-child(4) { background:#BEFD14; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<section id="sect1">Section 1</section>
<section id="sect2">Section 2</section>
<section id="sect3">Section 3</section>
<section id="sect4">Section 4</section>
Here's a pen, where you can see my implementation: https://codepen.io/EigenDerArtige/pen/aVEyxd
I am trying to accomplish an autoscroll to the next or previous section, whenever the user scrolls or swipes up/down... Therefore I only fire the "scroll"-event once every second, to prevent multiple scrolljacks all happening at once... However the above behavior seems to result in the user being scrolled to the wrong section.
I've been trying for a couple of hours now to get it working, but to no avail. Help is greatly appreciated!
The problem lies in the assignment prevPosition = ctPosition.
Each time the scroll handler runs, var ctPosition = $(window).scrollTop(); is good for determining scroll direction, however it's not the value that should be rememberad as prevPosition.
prevPosition needs to be $(window).scrollTop() as measured after the animation has completed.
Try this :
$(document).ready(function() {
var ANIMATION_DURATION = 700;
var ACTIVE_SECTION = $("section:first-of-type").eq(0);
var prevPosition = $(window).scrollTop();
$(window).on("scroll", doScrollingStuff);
function doScrollingStuff(e) {
$(window).off("scroll");
var ctPosition = $(window).scrollTop();
var nextSection = (ctPosition < prevPosition) ? ACTIVE_SECTION.prev("section") : (ctPosition > prevPosition) ? ACTIVE_SECTION.next("section") : ACTIVE_SECTION; // Determine scroll direction and target the next section
// If next section exists and is not current section: Scroll to it!
if(nextSection.length > 0 && nextSection !== ACTIVE_SECTION) {
$("body, html").animate({
'scrollTop': nextSection.offset().top
}, ANIMATION_DURATION).promise().then(function() {
// when animation is complete
prevPosition = $(window).scrollTop(); // remember remeasured .scrollTop()
ACTIVE_SECTION = nextSection; // remember active section
$(window).on("scroll", doScrollingStuff); // no need for additional delay after animation
});
} else {
setTimeout(function() {
$(window).on("scroll", doScrollingStuff);
}, 100); // Debounce
}
}
});

Detecting Animation Elements in View

I am trying to add a class to an element when it is in the viewport. I have achieved this however it causes serious issues to the performance of my site when I scroll.
I currently have this JavaScript:
//Cache reference to window and animation items
var $animation_elements = $('.animation-element');
var $window = $(window);
$window.on('scroll resize', check_if_in_view);
$window.trigger('scroll');
function check_if_in_view() {
var window_height = $window.height();
var window_top_position = $window.scrollTop();
var window_bottom_position = (window_top_position + window_height);
$.each($animation_elements, function() {
var $element = $(this);
var element_height = $element.outerHeight();
var element_top_position = $element.offset().top;
var element_bottom_position = (element_top_position + element_height);
//check to see if this current container is within viewport
if ((element_bottom_position >= window_top_position) &&
(element_top_position <= window_bottom_position)) {
$element.addClass('in-view');
} else {
$element.removeClass('in-view');
}
});
}
So as you can see the check_if_in_view() function seems to be constantly firing as the page is being scrolled and I believe this might be the reason why the performance might be so bad.
Is there a more efficient way of adding a class when scrolling the page that wont cause performance issues on my site?
Use setTimeout to delay calling the function every time a scroll event is fired. In the following code (which I borrowed from Codrops), a flag is set to call the function every 60 milliseconds in the case of continous scrolling.
function Scroller(el) {
this.elements = Array.prototype.slice.call( el );
this._init();
}
Scroller.prototype = {
_init : function() {
//this flag prevents that the function _scrollPage is called
//every time the 'scroll' event is fired
this.didScroll = false;
window.addEventListener( 'scroll', this._scrollHandler.bind(this), false );
},
_scrollHandler : function() {
if( !this.didScroll ) {
this.didScroll = true;
setTimeout( function() { this._scrollPage(); }, 60 );
}
},
_scrollPage : function() {
this.elements.forEach( function( el, i ) {
if( inViewport(el) ) {
classie.add( el, 'i-am-in-the-viewport' );
}
else {
classie.remove( el, 'i-am-in-the-viewport' );
}
});
this.didScroll = false;
}
};
To use it call new Scroller( document.getElementsByClassName('elements-to-watch') );.
Check out the complete code on Codrops to see the implementation of the inViewPort() function. Classie.js is used to handle the assignation of class names.
Don't be afraid to ask for clarification if there's something you don't get!

trigger a jquery effect when you scroll

Hey guys I'm back with another question. I'm using the code below to add a bouncing effect to a div on my site. It works fine right now but the div has to be clicked in order for the effect to start. I would like to modify it so that when the user is scrolling down the page and reaches that section it triggers the effect automatically. How can I modify the code below to trigger the effect when the user scroll's down and reaches that section of the site?
Here is the code I' using
$(".servi-block").click(function () {
doBounce($(this), 2, '10px', 150);
});
function doBounce(element, times, distance, speed) {
for (i = 0; i < times; i++) {
element.animate({
marginTop: '-=' + distance
}, speed)
.animate({
marginTop: '+=' + distance
}, speed);
}
}
$(window).scroll(function(event){
isElementVisible = inViewport($(".servi-block"));
if(isElementVisible)
{
doBounce($(this), 2, '10px', 150);
}
});
function inViewport (el)
{
var r, html;
if ( !el || 1 !== el.nodeType ) { return false; }
html = document.documentElement;
r = el.getBoundingClientRect();
return ( !!r
&& r.bottom >= 0
&& r.right >= 0
&& r.top <= html.clientHeight
&& r.left <= html.clientWidth
);
}
This should help you out: http://api.jquery.com/scroll/
$( "#target" ).scroll(function() {
$( "#log" ).append( "<div>Handler for .scroll() called.</div>" );
});
Also utilize this
$('#target').on("mousewheel", function() {
alert($(document).scrollTop());
});
Those two together should get you the ability to figure out you are scrolling, and when you reach position X, do something.
EDITED
Let's go at it this way -
var targetPos = "500px";
$( document ).scroll(function() {
if ($(document).scrollTop() == targetPos) {
doBounce($(this), 2, '10px', 150);
}
});
You can simply check to see when the element comes into view by taking the element's offset and subtracting that element's parent height and scrollTop value.
Here's an example:
$(document).ready(function() {
$(document).on('scroll', function() {
var offset = $('.element').offset().top,
scroll = $(document).scrollTop(),
height = $(document).height(),
inViewDown = ((offset - scroll - height) <= 0) ? true : false;
if (inViewDown) {
// Do some stuff
}
});
});
Here's a working example: http://jsfiddle.net/ughEe/

jQuery function losing the called elements and only keeping the last caller

I am developing a parallax site, and I want to ease out the elements when the scrolling has stopped. So I developed a plugin to detect when the scrolling stops, and once it stops, then smooth out the movement of the element (The object moves 5 pixels on to the direction in which the user was scrolling). It works but only to the last element that the plugin was applied to. When i was trying to debug, I see that both elements are still in effect inside the $(window).scroll(function(event) { but once we reach $(window).scrollStopped(function(){ only the last element is in effect. Any solutions?
// Scroll Direction set
var lastScrollTop = 0, scrollDirection = "";
$(window).scroll(function(event){
var st = $(this).scrollTop();
if (st > lastScrollTop){
scrollDirection = "down";
} else {
scrollDirection = "up";
}
lastScrollTop = st;
});
// Scroll Stopped detection
$.fn.scrollStopped = function(callback) {
$(this).scroll(function(){
var self = this, $this = $(self);
if ($this.data('scrollTimeout')) {
clearTimeout($this.data('scrollTimeout'));
}
$this.data('scrollTimeout', setTimeout(callback,250,self));
});
};
// Smooth ending
$.fn.smoothStop = function () {
var $this = $(this);
$(window).scroll(function(event) {
$(window).scrollStopped(function(){
var top = parseFloat($this.css("top"));
if(scrollDirection == "down")
{
console.log(top, $this);
var new_top = top + 5;
$this.animate({
top: new_top + 'px'},
1000);
}
else{
var new_top = top - 5;
$this.animate({
top: new_top + 'px'},
1000);
}
});
});
};
$(".g6").smoothStop();
$(".g2").smoothStop();
JSFIDDLE
// Scroll Stopped detection
$.fn.scrollStopped = function(callback) {
$(this).scroll(function(){ <-- this is the window
var self = this, $this = $(self);
if ($this.data('scrollTimeout')) {
clearTimeout($this.data('scrollTimeout')); <----timeout is removed from window
}
$this.data('scrollTimeout', setTimeout(callback,250,self)); <----timeout is set to window
});
};
basically you are trying to run multiple events, but you end up storing those multiple events in the same memory location. So when you add a new one, it cancells out the previous entry.

javascript - Need onclick to go full distance before click works again

I have this javascript function I use that when clicked goes a certain distance. This is used within a scroller going left to right that uses about 7 divs. My question is how do I get the click to go the full distance first before the click can be used again? The issue is if the user rapidly clicks on the arrow button it resets the distance and sometimes can end up in the middle of an image instead of right at the seam. What code am I missing to accomplish this?
$(function () {
$("#right, #left").click(function () {
var dir = this.id == "right" ? '+=' : '-=';
$(".outerwrapper").stop().animate({ scrollLeft: dir + '251' }, 1000);
});
});
I would've thought that the easiest way would be to have a boolean flag indicating whether or not the animation is taking place:
$(function () {
var animating = false,
outerwrap = $(".outerwrapper");
$("#right, #left").click(function () {
if (animating) {return;}
var dir = (this.id === "right") ? '+=' : '-=';
animating = true;
outerwrap.animate({
scrollLeft: dir + '251'
}, 1000, function () {
animating = false;
});
});
});
works for me: http://jsfiddle.net/BYossarian/vDtwy/4/
Use .off() to unbind the click as soon as it occurs, then re-bind it once the animation completes.
function go(elem){
$(elem).off('click'); console.log(elem);
var dir = elem.id == "right" ? '+=' : '-=';
$(".outerwrapper").stop().animate({ left: dir + '251' }, 3000, function(){
$("#right, #left").click(go);
});
}
$("#right, #left").click(function () {
go(this);
});
jsFiddle example
You can see in this simplified example that the click event is unbound immediately after clicking, and then rebound once the animation completes.
Use an automatic then call like this
var isMoving = false;
$(function () {
$("#right, #left").click(function () {
if (isMoving) return;
isMoving = true;
var dir = this.id == "right" ? '+=' : '-=';
$(".outerwrapper").stop().animate({ scrollLeft: dir + '251' }, 1000).then(function(){isMoving = false}());
});
});
I think that you miss the fact that when you make stop() you actually position the slider at some specific point. I.e. if your scroller is 1000px and you click left twice very quickly you will probably get
scrollLeft: 0 - 251
scrollLeft: -2 - 251
So, I think that you should use an index and not exactly these += and -= calculations. For example:
$(function () {
var numberOfDivs = 7;
var divWidth = 251;
var currentIndex = 0;
$("#right, #left").click(function () {
currentIndex = this.id == "right" ? currentIndex+1 : currentIndex-1;
currentIndex = currentIndex < 0 ? 0 : currentIndex;
currentIndex = currentIndex > numberOfDivs ? numberOfDivs : currentIndex;
$(".outerwrapper").stop().animate({ scrollLeft: (currentIndex * divWidth) + "px" }, 1000);
});
});
A big benefit of this approach is that you are not disabling the clicking. You may click as many times as you want and you can do that quickly. The script will still works.
This will work perfectly fine:
var userDisplaysPageCounter = 1;
$('#inventory_userdisplays_forward_button').bind('click.rightarrowiventory', function(event) {
_goForwardInInventory();
});
$('#inventory_userdisplays_back_button').bind('click.leftarrowiventory', function(event) {
_goBackInInventory();
});
function _goForwardInInventory()
{
//$('#inventory_userdisplays_forward_button').unbind('click.rightarrowiventory');
var totalPages = $('#userfooterdisplays_list_pagination_container div').length;
totalPages = Math.ceil(totalPages/4);
// alert(totalPages);
if(userDisplaysPageCounter < totalPages)
{
userDisplaysPageCounter++;
$( "#userfooterdisplays_list_pagination_container" ).animate({
left: "-=600",
}, 500, function() {
});
}
}
function _goBackInInventory()
{
//$('#inventory_userdisplays_back_button').unbind('click.leftarrowiventory');
if(userDisplaysPageCounter > 1)
{
userDisplaysPageCounter--;
$( "#userfooterdisplays_list_pagination_container" ).animate({
left: "+=600",
}, 500, function() {
});
}
}
I second BYossarian's answer.
Here is a variation on his demo, which "skips" the animation when the user clicks several times quickly on the buttons :
$(function () {
var targetScroll = 0,
outerwrap = $(".outerwrapper");
$("#right, #left").click(function () {
// stop the animation,
outerwrap.stop();
// hard set scrollLeft to its target position
outerwrap.scrollLeft(targetScroll*251);
if (this.id === "right"){
if (targetScroll < 6) targetScroll += 1;
dir = '+=251';
} else {
if (targetScroll > 0) targetScroll -=1;
dir = '-=251';
}
outerwrap.animate({ scrollLeft: dir }, 1000);
});
});
fiddle

Categories

Resources