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!
Related
I'm trying to check if element crossed bottom edge of viewport. If it did, I want to add class start to this element. The problem is that when condition is satisfied class adds to all h2 elements.
Here is my code:
$.fn.checkAnimation = function() {
var context = this;
function isElementInViewport(elem) {
var $elem = context;
// Get the scroll position of the page.
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
// Get the position of the element on the page.
var elemTop = Math.round( $elem.offset().top );
var elemBottom = elemTop + $elem.height();
return (elemTop < viewportBottom);
}
// Check if it's time to start the animation.
function checkAnimation() {
console.log(isElementInViewport($elem));
var $elem = context;
// If the animation has already been started
if ($elem.hasClass('start')) return;
if (isElementInViewport($elem)) {
// Start the animation
context.addClass('start');
}
}
checkAnimation();
return this;
};
$(window).on('scroll scrollstart touchmove orientationchange resize', function(){
$('h2').checkAnimation();
});
You'll need to change your checkAnimation jQuery plugin to loop through all elements in the jQuery object and process them individually or call your function like this
$('h2').each(function(){
$(this).checkAnimation();
}
Here is what I mean by processing the elements individually inside the plugin:
$.fn.checkAnimation = function() {
function isElementInViewport($elem) {
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
var elemTop = Math.round( $elem.offset().top );
var elemBottom = elemTop + $elem.height();
return (elemTop < viewportBottom);
}
function checkAnimation() {
var $elem = $(this);
if ($elem.hasClass('start')) return;
if (isElementInViewport($elem)) {
$elem.addClass('start');
}
}
return this.each(checkAnimation);
};
If you use this version of the plugin you can call it like this:
$('h2').checkAnimation();
It will add the class only to the element that matches the condition not to all the element in the jQuery object you've called the function on.
Should be $elem.addClass('start'); instead and remove the var $elem = context; statement like :
function checkAnimation() {
console.log(isElementInViewport($elem));
// If the animation has already been started
if ($elem.hasClass('start')) return;
if (isElementInViewport($elem)) {
// Start the animation
$elem.addClass('start');
}
}
Hope this helps.
this inside a jQuery plugin is the jQuery object that contains the whole collection of elements represented by the previous selector/filter.
In order to treat each element in the collection as an individual instance you need to loop through the initial this.
Very basic pattern:
$.fn.pluginName = function(options){
// return original collection as jQuery to allow chaining
// loop over collection to access individual elements
return this.each(function(i, elem){
// do something with each element instance
$(elem).doSomething(); // elem === this also
});
}
based on this code in the link https://www.smashingmagazine.com/2014/08/how-i-built-the-one-page-scroll-plugin/
function init_scroll(event, delta) {
var deltaOfInterest = delta,
timeNow = new Date().getTime(),
quietPeriod = 500;
// Cancel scroll if currently animating or within quiet period
if(timeNow - lastAnimation < quietPeriod + settings.animationTime) {
event.preventDefault();
return;
}
if (deltaOfInterest < 0) {
el.moveDown()
} else {
el.moveUp()
}
lastAnimation = timeNow;
}
$(document).bind('mousewheel DOMMouseScroll', function(event) {
event.preventDefault();
var delta = event.originalEvent.wheelDelta || -event.originalEvent.detail;
init_scroll(event, delta);
});
What is el. part before it calls moveDown()? I'm new to jQuery and I'm not sure what it's called.
It can also be seen calling swipeEvents().
el.swipeEvents().unbind("swipeDown swipeUp");
Cheers
Looking at their example code shows:
var el = $(this)
Where this is the element that plugin is initialized on:
$(".main").onepage_scroll();
Putting a breakpoint in the init_scroll function and inspecting el shows:
[<div class="main onepage-wrapper" ... > ... </div>]
Which is indeed the jQuery selector over the element that the plugin was initialized on.
I built a slider that moves left or right if some items are hidden. Obviously this needs to work responsively, so I am using a resize (smartresize) function to check when the browser is resized. It works, but after resizing when you click more (right arrow) it takes 2-5 seconds to actually calculate what is hidden and then execute.
Can anyone explain to me why this is happening, and how to possibly fix it?
Thanks!
$(window).smartresize(function () {
var cont = $('#nav-sub-menu-2 .container');
var ul = $('#nav-sub-menu-2 ul');
var li = $('#nav-sub-menu-2 ul li');
var amount = li.length;
var width = li.width();
var contWidth = cont.width();
var ulWidth = width * amount;
var remainder = ulWidth - contWidth;
ul.width(ulWidth);
if(remainder <= 0) {
$('.more, .less').fadeOut();
} else {
$('.more').fadeIn();
}
$('.more').click(function() {
ul.animate({ 'right' : remainder });
$(this).fadeOut();
$(".less").fadeIn();
});
$('.less').click(function() {
ul.animate({ 'right' : 0 });
$(this).fadeOut();
$(".more").fadeIn();
});
}).smartresize();
It could be because it is recalculating the screen size at every interval as you are resizing...
Try using a debouncer to delay the function calls until everything's settled.
/* Debounce Resize */
function debouncer( func , timeout ) {
var timeoutID , timeout = timeout || 200;
return function () {
var scope = this , args = arguments;
clearTimeout( timeoutID );
timeoutID = setTimeout( function () {
func.apply( scope , Array.prototype.slice.call( args ) );
} , timeout );
}
}
$( window ).resize( debouncer( function ( e ) {
/* Function */
}));
This question already has answers here:
Animate counter when in viewport
(4 answers)
Closed 6 years ago.
I'm trying to make a number count up when it's within the viewport, but currently, the script i'm using will interrupt the count on scroll.
How would I make it so that it will ignore the scroll and just count up when it's within the viewport? This needs to work on mobile, so even when a user is scrolling on touch. It cannot interrupt the count.
Please see here:
http://jsfiddle.net/Q37Q6/27/
(function ($) {
$.fn.visible = function (partial, hidden) {
var $t = $(this).eq(0),
t = $t.get(0),
$w = $(window),
viewTop = $w.scrollTop(),
viewBottom = viewTop + $w.height(),
_top = $t.offset().top,
_bottom = _top + $t.height(),
compareTop = partial === true ? _bottom : _top,
compareBottom = partial === true ? _top : _bottom,
clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true;
return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop));
};
})(jQuery);
// Scrolling Functions
$(window).scroll(function (event) {
function padNum(num) {
if (num < 10) {
return "" + num;
}
return num;
}
var first = 25; // Count up to 25x for first
var second = 4; // Count up to 4x for second
function countStuffUp(points, selector, duration) { //Animate count
$({
countNumber: $(selector).text()
}).animate({
countNumber: points
}, {
duration: duration,
easing: 'linear',
step: function () {
$(selector).text(padNum(parseInt(this.countNumber)));
},
complete: function () {
$(selector).text(points);
}
});
}
// Output to div
$(".first-count").each(function (i, el) {
var el = $(el);
if (el.visible(true)) {
countStuffUp(first, '.first-count', 1600);
}
});
// Output to div
$(".second-count").each(function (i, el) {
var el = $(el);
if (el.visible(true)) {
countStuffUp(second, '.second-count', 1000);
}
});
});
Your example is more complicated than you're aware, I think. You're doing things in a pretty unusual way, here, using a jQuery animate method on a custom property as your counter. It's kind of cool, but it also makes things a little more complicated. I've had to add a number of things to straighten up the situation.
I went ahead and rewrote your visible plugin, largely because I had no idea what yours was doing. This one's simple!
When your counters become visible, they get a "counting" class so that the counter isn't re-fired on them when they're already counting.
I save a reference to the object you have your custom counter animation on to the data attribute of the counter. This is vital: without that reference, you can't stop the animation when it goes offscreen.
I do some fanciness inside the step function to keep track of how much time is left so that you can keep your counter running at the same speed even if it stops and starts. If your counter runs for half a second and it's set to use one second for the whole animation, if it gets interrupted and restarted you only want to set it to half a second when you restart the counter.
http://jsfiddle.net/nate/p9wgx/1/
(function ($) {
$.fn.visible = function () {
var $element = $(this).eq(0),
$win = $(window),
elemTop = $element.position().top,
elemBottom = elemTop + $element.height(),
winTop = $win.scrollTop(),
winBottom = winTop + $win.height();
if (elemBottom < winTop) {
return false;
} else if (elemTop > winBottom) {
return false;
} else {
return true;
}
};
})(jQuery);
function padNum(num) {
if (num < 10) {
return " " + num;
}
return num;
}
var $count1 = $('.first-count');
var $count2 = $('.second-count');
// Scrolling Functions
$(window).scroll(function (event) {
var first = 25; // Count up to 25x for first
var second = 4; // Count up to 4x for second
function countStuffUp(points, selector, duration) {
//Animate count
var $selector = $(selector);
$selector.addClass('counting');
var $counter = $({
countNumber: $selector.text()
}).animate({
countNumber: points
}, {
duration: duration,
easing: 'linear',
step: function (now) {
$selector.data('remaining', (points - now) * (duration / points));
$selector.text(padNum(parseInt(this.countNumber)));
},
complete: function () {
$selector.removeClass('counting');
$selector.text(points);
}
});
$selector.data('counter', $counter);
}
// Output to div
$(".first-count").each(function (i, el) {
var el = $(el);
if (el.visible() && !el.hasClass('counting')) {
var duration = el.data('remaining') || 1600;
countStuffUp(first, '.first-count', duration);
} else if (!el.visible() && el.hasClass('counting')) {
el.data('counter').stop();
el.removeClass('counting');
}
});
// Output to div
$(".second-count").each(function (i, el) {
var el = $(el);
if (el.visible() && !el.hasClass('counting')) {
var duration = el.data('remaining') || 1000;
countStuffUp(second, '.second-count', duration);
} else if (!el.visible() && el.hasClass('counting')) {
el.data('counter').stop();
el.removeClass('counting');
}
});
});
There's a lot here. Feel free to ask me questions if anything's not clear.
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();
}
});