Detect whether scroll event was created by user - javascript

Is it possible to tell whether a scroll event was done by the browser or by the user? Specifically, when using the back button a browser may jump to the last known scroll position. If I bind to scroll event how can I tell whether this was caused by user or browser?
$(document).scroll( function(){
//who did this?!
});
I see three types of situations that cause scrolling in a browser.
The user performs some action. For example, uses mousewheel, arrow keys, page up/down keys, home/end keys, clicks the scrollbar or drags its thumb.
The browser scrolls automatically. For example, when using the back button in your browser it will jump to the last known scroll position automatically.
Javascript scrolls. For example, element.scrollTo(x,y).

Unfortunately, there is no direct way of telling that.
I would say if you can redesign your app so that it doesn't depend on this type of flow, go for that.
If not, a workaround I can think of is to keep track of user initiated scrolls and check that to see if the scroll was triggered by the browser or by the user.
Here's an example that I put together which does this pretty well (except for browsers where jQuery history has problems with).
You need to run this locally to be able to test it fully (jsFiddle/jsbin are not good fits as they iFrame the contents).
Here's the test cases that I validated:
Page loads - userScroll is false
Scroll using mouse/keyboard - userScroll becomes true
Click on the link to jump to page bottom - userScroll becomes false
Click Back/Forward - userScroll becomes false;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
<script type="text/javascript" src="https://raw.github.com/tkyk/jquery-history-plugin/master/jquery.history.js"></script>
</head>
<body>
<span> hello there </span><br/>
click here to go down
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<a name="bottom"> just sitting </a>
</body>
<script type="text/javascript">
var userScroll = false;
function mouseEvent(e) {
userScroll = true;
}
$(function() {
// reset flag on back/forward
$.history.init(function(hash){
userScroll = false;
});
$(document).keydown(function(e) {
if(e.which == 33 // page up
|| e.which == 34 // page dn
|| e.which == 32 // spacebar
|| e.which == 38 // up
|| e.which == 40 // down
|| (e.ctrlKey && e.which == 36) // ctrl + home
|| (e.ctrlKey && e.which == 35) // ctrl + end
) {
userScroll = true;
}
});
// detect user scroll through mouse
// Mozilla/Webkit
if(window.addEventListener) {
document.addEventListener('DOMMouseScroll', mouseEvent, false);
}
//for IE/OPERA etc
document.onmousewheel = mouseEvent;
// to reset flag when named anchors are clicked
$('a[href*=#]').click(function() {
userScroll = false;
});
// detect browser/user scroll
$(document).scroll( function(){
console.log('Scroll initiated by ' + (userScroll == true ? "user" : "browser"));
});
});
</script>
</html>
Notes:
This doesn't track scrolling when the user drags the scrollbar with mouse. This can be added with some more code, which I left as an exercise for you.
event.keyCodes may vary by OS, so you may have to change that appropriately.
Hope this helps!

Rather than trying to catch all the user events, it's much easier to do the opposite and handle only the programmatic events - and ignore those.
For example, this kind of code would work:
// Element that needs to be scrolled
var myElement = document.getElementById('my-container');
// Flag to tell if the change was programmatic or by the user
var ignoreNextScrollEvent = false;
function setScrollTop(scrollTop) {
ignoreNextScrollEvent = true;
myElement.scrollTop = scrollTop
}
myElement.addEventListener('scroll', function() {
if (ignoreNextScrollEvent) {
// Ignore this event because it was done programmatically
ignoreNextScrollEvent = false;
return;
}
// Process user-initiated event here
});
Then when you call setScrollTop(), the scroll event will be ignored, while if the user scroll with the mouse, keyboard or any other way, the event will be processed.

As far as I know it is impossible (without any work) to tell whenever scroll event has been issued by "user" or by other means.
You could try (as others mentioned) catch mousewheel events, then probably trying to catch keydown event on any keys that can trigger scroll (arrows, space etc.) while checking what element is currently focused, since you for example can't scroll using arrow keys while typing in an input field.
In general that would be complex and messy script.
Depending on situation you're dealing with you could I guess "revert the logic", and instead of detecting user issued scroll events just hook in into any scrolls made programatically and treat any scroll events not made by your code as made by an user.
Like I said it depends on a situation, and what you're trying to achive.

Yes, it is 100% possible. I'm current using this in an application where IE is not a requirement - client facing only. When my Backbone app initiates an animation where scroll is changed - scroll occurs but does not trigger these event captures. This is tested in FF, Safari & Chrome latest.
$('html, body').bind('scroll mousedown wheel DOMMouseScroll mousewheel keyup', function(evt) {
// detect only user initiated, not by an .animate though
if (evt.type === 'DOMMouseScroll' || evt.type === 'keyup' || evt.type === 'mousewheel') {
// up
if (evt.originalEvent.detail < 0 || (evt.originalEvent.wheelDelta && evt.originalEvent.wheelDelta > 0)) {
// down.
} else if (evt.originalEvent.detail > 0 || (evt.originalEvent.wheelDelta && evt.originalEvent.wheelDelta < 0)) {
}
}
});

Try using the Mousewheel and DOMMouseScroll events instead. See http://www.quirksmode.org/dom/events/scroll.html

You can check the scroll position on ready. When you fire the on scroll event check to make sure the scroll position is different than it was when the page loaded. Lastly be sure to clear out the stored value once the page is scrolled.
$(function () {
var loadScrollTop = ($(document).scrollTop() > 0 ? $(document).scrollTop() : null);
$(document).scroll(function (e) {
if ( $(document).scrollTop() !== loadScrollTop) {
// scroll code here!
}
loadScrollTop = null;
});
});

Regarding to:
Specifically, when using the back button a browser may jump to the last known scroll position.
That fires very soon, after the page is rendered. You can just delay listenting to the scroll event by 1 second or so.

There is one more way to separate the user-created scroll: you can use the alternative event handlers, for example 'mousewheel', 'touchmove', 'keydown' with codes 38 and 40 for arrow scrolling, for scrolling with scroll bar - if 'scroll' event is fired simultaneously with 'mousedown' until 'mouseup' event.

Found this very useful. Here's a coffeescript version for those so inclined.
$ ->
S.userScroll = false
# reset flag on back/forward
if $.history?
$.history.init (hash) ->
S.userScroll = false
mouseEvent = (e)->
S.userScroll = true
$(document).keydown (e) ->
importantKey = (e.which == 33 or # page up
e.which == 34 or # page dn
e.which == 32 or # spacebar
e.which == 38 or # up
e.which == 40 or # down
(e.ctrlKey and e.which == 36) or # ctrl + home
(e.ctrlKey and e.which == 35) # ctrl + end
)
if importantKey
S.userScroll = true;
# Detect user scroll through mouse
# Mozilla/Webkit
if window.addEventListener
document.addEventListener('DOMMouseScroll', mouseEvent, false);
# for IE/OPERA etc
document.onmousewheel = mouseEvent;

if you're using JQuery than there's a better answer, apparently - i'm just trying it out myself.
see: Detect jquery event trigger by user or call by code

It might not help with your application, but I needed to fire an event on user scroll but not programatic scroll and posting incase it helps anyone else.
Listen to the wheel event instead of scroll,
It is triggered whenever a user uses the mouse wheel or tracker pad( which I feel is how most people scroll anyway) and isn't fired when you programatically scroll. I used it to differentiate between a user scroll action and scrolling functions.
https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
element.addEventListener('wheel', (event) => {
//do user scroll stuff here
})
One caveat is that wheel doesn't fire on scroll on mobile, so I checked whether the device was mobile and used similar functions
if(this.mobile){
element.addEventListener('scroll', (event) => {
//do mobile scroll stuff here
})
}

Related

Microsoft Edge - keydown event

I would like to switch between pages using arrows (37 - left arrow, 39 - right arrow). The code below works correctly with Firefox, Chrome and Internet Explorer.
The solution does not work with Microsoft Edge after Back (back in browsing history) button has been clicked in the browser. Does anybody know how to fix it?
<script type="text/javascript">
window.addEventListener("keydown", checkKeyPressed, false);
function checkKeyPressed(event) {
var x = event.which || event.keyCode;
if (x == 37) { window.location.href = "page1.html";}
if (x == 39) { window.location.href = "page2.html";}
};
</script>
This looks like a bug. In that when you use the navigation controls (or the refresh button) the window seems to lose focus and so keydown events do not fire. Also window.focus doesn't seem to work as expected either.
But I have found a workaround (or two). The first is to modify your script to look like this:
<script>
window.onload = function(){
document.body.focus();
document.addEventListener("keydown", checkKeyPressed, false);
function checkKeyPressed(event) {
var x = event.which || event.keyCode;
if (x == 37) { window.location.href = "page1.html"; }
if (x == 39) { window.location.href = "page2.html"; }
};
}
</script>
You then need to add a tab index to the body tag e.g:
<body tabindex="1">
This then allows you to programmatically set the focus of the page (And it isn't ignored by Microsoft Edge like window.focus() is) The reason you need to add tabindex to the body is because the focus method is implicitly applicable to a limited set of elements, chiefly form and <a href> tags. In recent browser versions, the event can be extended to include all element types by explicitly setting the element's tabindex property.
This workaround does add a potential accessibility issue since your element can gain focus via keyboard commands, such as the Tab key. Although I'm not sure how much of a problem that really is.
The second option is to add a form element to your page and either manually set focus to it or add the autofocus attribute:
<input autofocus>
Edge seems to respect this and gives the element auto focus and your key down events will now fire. Sadly You can't just hide this element, since if it's hidden it no longer get auto focus. (maybe you could set it's opacity to 0) but I didn't try.
Of the two options I prefer workaround 1. I will file this as a bug with the Edge team on connect when I get a chance this afternoon.

How to activate my "off-canvas" navigation when a key is pressed on the keyboard

I've been looking for a way to activate my "off-canvas" navigation when I press the "M" key.
I'll be more specific, I want my "off-canvas" navigation to slide in or out when I press the "M" key on my keyboard.
I already have the basic functionality of my "off-canvas" navigation worked out. I'm just stuck on the keypress thing.
Thanks!
I've had good experiences with the Mousetrap plugin. With it you can easily bind keypresses/combinations and execute code with it. It, among other things, automatically handles whether an input is focused (in which case you wouldn't want to execute the keybind)
Without having seen your code, I'm guessing by offcanvas navigation you mean the navigation menu you see in mobile pages, opened by pressing a button?
In which case you could use the following:
Mousetrap.bind('m', function() {
$("#your_menu_toggle_button").trigger("click");
});
If you donĀ“t care about which element has the focus you can bind an event-handler to the window
// Jquery:
$(window).on('keypress', callBack);
// JS:
window.onkeypress = callBack;
and check for the key (http://www.mediaevent.de/javascript/onkeydown.html)
var callBack = function(event){
// checking for 'm' and 'M' cross-Browser compatible
var pressedM = (event.keyCode == 77 || event.keyCode == 109
|| event.which == 77 || event.keyCode == 109)
if (pressedM){
/* logic here */
}
}
Then you need to define, how to activate your navigation and how to deactivate.
Your talking about sliding it in, so I assume you want to use something like JQuerys slideToggle() functionality for now:
...
/* logic here */
$('#offCanvasNav').animate({height: 'toggle'});
// same as .slideToggle() change height for width for sideways slide
jquery .slideToggle() horizontal alternative? - http://jsfiddle.net/7ZBQa/

Disable Firefox's silly right click context menu

I am making an HTML 5 game which requires the use of right click to control the player.
I have been able to disable the right click context menu by doing:
<body oncontextmenu="return(false);">
Then it came to my attention that if you hold shift and right click, a context menu still opens in Firefox!
So I disabled that by adding this JS as well:
document.onclick = function(e) { if(e.button == 2 || e.button == 3) { e.preventDefault(); e.stopPropagation(); return(false); } };
However, if you hold shift, and then double right click in Firefox it still opens!
Please tell me how to disable this bloody thing once and for all (I'm even willing to revert to some obscure, hacky, and unpractical solution, as long as it works).
You will never be able to entirely disable the context menu in all cases, as firefox has a setting that allows the user to tell the browser to ignore such hijinx as you are trying to pull.
Note: I'm on a mac, but this setting is in pretty uch the same place over all platforms.
That being said, try event.preventDefault() (see Vikash Madhow's comment on this other SO question:
How to disable right-click context-menu in javascript)
There is actually example in official documentation that blocks directly context menu event:
document.oncontextmenu = function () { // Use document as opposed to window for IE8 compatibility
return false;
};
window.addEventListener('contextmenu', function (e) { // Not compatible with IE < 9
e.preventDefault();
}, false);
document.ondblclick = function(e) {
if(e.button == 2 || e.button == 3) {
e.preventDefault();
e.stopPropagation();
return(false);
}
};

any way to detect ctrl + click in javascript for osx browsers? no jQuery

using vanilla js. Any way to grab the "right-click" (option-click) from OSX?
function clickey(e)
{
if(event.button==2 || /*how you'd do it in Java=)*/ e.getButton() == MouseButton.BUTTON3 )
...
}
but in js, how do eeet?
You need to listen to the contextmenu event. This is triggered when the context menu should be shown. So either if the right mouse butten or or ctrl + mouse.
If it is not supported then you can try to check the mousedown event where button is 2 and ctrlKey is true if it is triggered by using ctrl + mouse
document.addEventListener("contextmenu",function(event){
});
OR (depending on what the browser supports)
document.addEventListener("mousedown",function(event){
if( event.ctrlKey || event.button == 2 ) {
}
});
edit: removed the which info
I'm not experienced with OSX, but the Mouse Events have the option to check the modifier keys. So something along these lines should work:
DOMElement.addEventListener("click",function(event){
// either check directly the button
if (event.button == 2){}
// or
if (event.ctrlKey || event.altKey || event.metaKey){
// do stuff
}
});

JavaScript on iOS: preventDefault on touchstart without disabling scrolling

I am working with JavaScript and jQuery in an UIWevView on iOS.
I'v added some javascript event handler that allow me to capture a touch-and-hold event to show a message when someone taps an img for some time:
$(document).ready(function() {
var timeoutId = 0;
var messageAppeared = false;
$('img').on('touchstart', function(event) {
event.preventDefault();
timeoutId = setTimeout(function() {
/* Show message ... */
messageAppeared = true;
}, 1000);
}).on('touchend touchcancel', function(event) {
if (messageAppeared) {
event.preventDefault();
} else {
clearTimeout(timeoutId);
}
messageAppeared = false;
});
});
This works well to show the message. I added the two "event.preventDefault();" lines to stop imgs inside links to trigger the link.
The problem is: This also seems to prevent drag events to scroll the page from happen normally, so that the user wouldn't be able to scroll when his swipe happens to begin on an img.
How could I disable the default link action without interfering with scrolling?
You put me on the right track Stefan, having me think the other way around. For anyone still scratching their head over this, here's my solution.
I was trying to allow visitors to scroll through images horizontally, without breaking vertical scrolling. But I was executing custom functionality and waiting for a vertical scroll to happen. Instead, we should allow regular behavior first and wait for a specific gesture to happen like Stefan did.
For example:
$("img").on("touchstart", function(e) {
var touchStart = touchEnd = e.originalEvent.touches[0].pageX;
var touchExceeded = false;
$(this).on("touchmove", function(e) {
touchEnd = e.originalEvent.touches[0].pageX;
if(touchExceeded || touchStart - touchEnd > 50 || touchEnd - touchStart > 50) {
e.preventDefault();
touchExceeded = true;
// Execute your custom function.
}
});
$(this).on("touchend", function(e) {
$(this).off("touchmove touchend");
});
});
So basically we allow default behavior until the horizontal movement exceeds 50 pixels.
The touchExceeded variable makes sure our function still runs if we re-enter the initial < 50 pixel area.
(Note this is example code, e.originalEvent.touches[0].pageX is NOT cross browser compatible.)
Sometimes you have to ask a question on stack overflow to find the answer yourself. There is indeed a solution to my problem, and it's as follows:
$(document).ready(function() {
var timeoutId = 0;
$('img').on('touchstart', function(event) {
var imgElement = this;
timeoutId = setTimeout(function() {
$(imgElement).one('click', function(event) {
event.preventDefault();
});
/* Show message ... */
}, 1000);
}).on('touchend touchcancel', function(event) {
clearTimeout(timeoutId);
});
});
Explanation
No preventDefault() in the touch event handlers. This brings back scrolling behavior (of course).
Handle a normal click event once if the message appeared, and prevent it's default action.
You could look at a gesture library like hammer.js which covers all of the main gesture events across devices.

Categories

Resources