Cancel scrolling after user interaction - javascript

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);
}
}

Related

How to throttle function using the mousewheel plugin?

I'm using this plugin:
https://github.com/jquery/jquery-mousewheel
It's a horizontally scrolling one-page site, and the function is intended as a "scrollsnap"-- a scroll on the current section will force snap it to the next section. However, after the first "snap", it only happens after ~5 seconds, which I believe is due to the function firing too many times in a row.
function scrollSnap() {
$('.page:not(:last-child)').each(function(){
var nextTarget = $(this).next().position().left;
$(this).mousewheel(function(){
if(event.deltaY >= 50) {
$('main').animate({
scrollLeft: nextTarget
}, 700);
console.log("scrolled to: ", nextTarget);
}
});
});
$('.page:not(:first-child)').each(function(){
var prevTarget = $(this).prev().position().left;
$(this).mousewheel(function(){
if(event.deltaY <= -50) {
$('main').animate({
scrollLeft: prevTarget
}, 700);
console.log("scrolled to: ", prevTarget);
}
});
});
}
$(document).ready(function() {
scrollSnap();
});
You can throttle events like these with something called a debounce function. You can create your own or use a utility library like lodash which has it built in.

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.

Too many scroll events for smooth scrolling

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

jQuery - To perform a search using submit button on a live search plugin?

I started using mark.js live search plugin, and I was able to modify it to automatically scroll to the text part that's being searched on the page.
Like this:
SEARCH BOX |_jklmno____| <-- User searches here
123
456
789
abcde
fghi
jklmno <--- Then the page will automatically scroll and stop here.
pqrst
-> Done, it found the text <-
The code works, how can I build a button that when submitted, the page will jump to the next result?
I tried using this to jump to the next result when the form is submitted:
$('html,body').animate({scrollTop: mark.eq(index).offset().top}, 500);
}
This too:
else if ('mark[data-markjs]').live("submit", function(e) {
e.mark();
$('html,body').animate(
{scrollTop: mark.offset().top -100}
, 200);
});
But it didn't work.
Here's the working fiddle **(In order to see the search field, you have to scroll the result tab a little bit)
And here's the jQuery:
$.noConflict()
jQuery(function($) {
var mark = function() {
// Read the keyword
var keyword = $("input[name='keyword']").val();
// Determine selected options
var options = {
"filter": function(node, term, counter, totalCounter){
if(term === keyword && counter >= 1){
return false;
} else {
return true;
}
},
done: function() {
var mark = $('mark[data-markjs]');
if (mark.length) {
$('html,body').animate({scrollTop: mark.eq(index).offset().top}, 500);
}
/*
else if ('mark[data-markjs]').live("submit", function(e) {
e.mark();
$('html,body').animate(
{scrollTop: mark.offset().top -100}
, 200);
});
*/
}
};
$("input[name='opt[]']").each(function() {
options[$(this).val()] = $(this).is(":checked"); });
// Mark the keyword inside the context
$(".context").unmark();
$(".context").mark(keyword, options);
};
$("input[name='keyword']").on("keyup", mark);
$("input[type='checkbox']").on("change", mark);
$("input[name='keyword']").on("submit", mark);
});
I played a while with your fiddle.
It's a cool problem.
I decided to use the up/down arrows to scroll to the prev/next result...
Instead of the enter key or a button.
Here is the main part that I changed:
$("input[name='keyword']").on("keyup", function(e){
if(e.which==40){ // 40 = down arrow
e.preventDefault();
arrowOffset++;
}
if(e.which==38){ // 38 = up arrow
e.preventDefault();
arrowOffset--;
if(arrowOffset<1){
arrowOffset=1;
}
}
mark(arrowOffset);
});
I did not found how to "un-highlight" the previous result...
But since arrows make it scroll to the right result, I think it is quite cool like this.
done: function() {
var mark = $('mark[data-markjs]').last(); // Scroll to last <mark>
if (mark.length) {
$('html,body').animate({scrollTop: mark.offset().top-90}, 500);
}
}
Have a look at my fiddle for the complete updated script.

Slide down and slide up events mixed up in a fast mouse wheel up and down

I'm using jquery slideDown() and slideUp() to show a fixed gototop link when the scroll bar height is more than 200px.
Problem:
Link slide action mixed up in a fast mouse wheel up and down. Because of 0.4 sec running time of slide functions. I tried to define a visible flag and complete functions to prevent mixing. But not successful.
JsFiddle
Scroll down in result block to view the link and try a fast wheel up and down. If the result block has big height on your screen, please decrease the height to see the action.
impress: function () {
if ($(window).scrollTop() > this.MIN_SCROLL_HEIGHT
&& !this.buttonVisibleFlag)
{
this.button.slideDown(400, function() {
Blue.buttonVisibleFlag = true;
});
}
else if ($(window).scrollTop() <= this.MIN_SCROLL_HEIGHT
&& this.buttonVisibleFlag)
{
this.button.slideUp(400, function() {
Blue.buttonVisibleFlag = false;
});
}
}
Any ideas or help would be greatly appreciated.
I think your best bet would be to perform the sliding actions only after the user has stopped scrolling for a certain (small) period of time. I found this method to detect when the user stops scrolling and implemented in your code, here's the result:
Updated fiddle
var Blue = {
MIN_SCROLL_HEIGHT: 200,
button: null,
buttonVisibleFlag: null,
init: function () {
this.button = $(".gototop");
this.buttonVisibleFlag = false;
this.setWindowBindings();
},
setWindowBindings: function () {
$(window).scroll(function () {
//perform actions only after the user stops scrolling
clearTimeout($.data(this, 'scrollTimer'));
$.data(this, 'scrollTimer', setTimeout(function () {
Blue.impress();
}, 150));
});
},
impress: function () {
if ($(window).scrollTop() > this.MIN_SCROLL_HEIGHT) {
this.button.slideDown();
} else if ($(window).scrollTop() <= this.MIN_SCROLL_HEIGHT) {
this.button.slideUp();
}
}
}
$(document).ready(function () {
Blue.init();
});
Note: you may want to tweak the timeout interval to suit your needs

Categories

Resources