Prevent browser scroll on HTML5 History popstate - javascript

Is it possible to prevent the default behaviour of scrolling the document when a popstate event occurs?
Our site uses jQuery animated scrolling and History.js, and state changes should scroll the user around to different areas of the page whether via pushstate or popstate. The trouble is the browser restores the scroll position of the previous state automatically when a popstate event occurs.
I've tried using a container element set to 100% width and height of the document and scrolling the content inside that container. The problem with that I've found is it doesn't seem to be nearly as smooth as scrolling the document; especially if using lots of css3 like box-shadows and gradients.
I've also tried storing the document's scroll position during a user initiated scroll and restoring it after the browser scrolls the page (on popstate). This works fine in Firefox 12 but in Chrome 19 there is a flicker due to the page being scrolled and restored. I assume this is to do with a delay between the scroll and the scroll event being fired (where the scroll position is restored).
Firefox scrolls the page (and fires the scroll event) before popstate fires and Chrome fires popstate first then scrolls the document.
All the sites I've seen that use the history API either use a solution similar to those above or just ignore the scroll position change when a user goes back/forward (e.g. GitHub).
Is it possible to prevent the document being scrolled at all on popstate events?

if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
(Announced by Google on September 2, 2015)
Browser support:
Chrome: supported (since 46)
Firefox: supported (since 46)
IE: not supported
Edge: supported (since 79)
Opera: supported (since 33)
Safari: supported
For more info, see Browser compatibility on MDN.

This has been a reported issue with the mozilla developer core for more than a year now. Unfortunately, the ticket did not really progress. I think Chrome is the same: There is no reliable way to tackle the scroll position onpopstate via js, since it's native browser behaviour.
There is hope for the future though, if you look at the HTML5 history spec, which explicitly wishes for the scroll position to be represented on the state object:
History objects represent their browsing context's session history as a flat list of session history entries. Each session history entry consists of a URL and optionally a state object, and may in addition have a title, a Document object, form data, a scroll position, and other information associated with it.
This, and if you read the comments on the mozilla ticket mentioned above, gives some indication that it is possible that in the near future scroll position will not be restored anymore onpopstate, at least for people using pushState.
Unfortunately, until then, the scroll position gets stored when pushState is used, and replaceState does not replace the scroll position. Otherwise, it would be fairly easy, and you could use replaceState to set the current Scroll position everytime the user has scrolled the page (with some cautious onscroll handler).
Also unfortunately, the HTML5 spec does not specify when exactly the popstate event has to be fired, it just says: «is fired in certain cases when navigating to a session history entry», which does not clearly say if it's before or after; if it was always before, a solution with handling the scroll event occuring after the popstate would be possible.
Cancel the scroll event?
Furthermore, it would also be easy, if the scroll event where cancelable, which it isn't. If it was, you could just cancel the first scroll event of a series (user scroll events are like lemmings, they come in dozens, whereas the scroll event fired by the history repositioning is a single one), and you would be fine.
There's no solution for now
As far as I see, the only thing I'd recommend for now is to wait for the HTML5 Spec to be fully implemented and to roll with the browser behaviour in this case, that means: animate the scrolling when the browser lets you do it, and let the browser reposition the page when there's a history event. The only thing you can influence position-wise is that you use pushState when the page is positioned in a good way to go back to. Any other solution is either bound to have bugs, or to be too browser-specific, or both.

You're going to have to use some kind of horrible browser sniffing here. For Firefox, I would go with your solution of storing the scroll position and restoring it.
I thought I had a good Webkit solution based on your description, but I just tried in Chrome 21, and it seems that Chrome scrolls first, then fires the popstate event, then fires the scroll event. But for reference, here's what I came up with:
function noScrollOnce(event) {
event.preventDefault();
document.removeEventListener('scroll', noScrollOnce);
}
window.onpopstate = function () {
document.addEventListener('scroll', noScrollOnce);
};​
Black magic such as pretending the page is scrolling by moving an absolute positioned element is ruled out by the screen repainting speed too.
So I'm 99% sure that the answer is that you can't, and you're going to have to use one of the compromises you've mentioned in the question. Both browsers scroll before JavaScript knows anything about it, so JavaScript can only react after the event. The only difference is that Firefox doesn't paint the screen until after the Javascript has fired, which is why there's a workable solution in Firefox but not in WebKit.

Now you can do
history.scrollRestoration = 'manual';
and this should prevent browser scroll. This only works right now in Chrome 46 and above, but it seems that Firefox is planning to support it too

The solution is to use position: fixed and specify top equal to scroll position of page.
Here is an example:
$(window).on('popstate', function()
{
$('.yourWrapAroundAllContent').css({
position: 'fixed',
top: -window.scrollY
});
requestAnimationFrame(function()
{
$('.yourWrapAroundAllContent').css({
position: 'static',
top: 0
});
});
});
Yes, you instead receive flickering scrollbar, but it is less evil.

The following fix should work in all browsers.
You can set scroll position to 0 on the unload event. You can read about this event here: https://developer.mozilla.org/en-US/docs/Web/Events/unload. Essentially, the unload event fires right before you leave the page.
By setting scrollPosition to 0 on unload means when you leave the page with a set pushState it sets scrollPosition to 0. When you return to this page by refreshing or pressing back it will not autoscroll.
//Listen for unload event. This is triggered when leaving the page.
//Reference: https://developer.mozilla.org/en-US/docs/Web/Events/unload
window.addEventListener('unload', function(e) {
//set scroll position to the top of the page.
window.scrollTo(0, 0);
});

Setting scrollRestoration to manual not worked for me, here is my solution.
window.addEventListener('popstate', function(e) {
var scrollTop = document.body.scrollTop;
window.addEventListener('scroll', function(e) {
document.body.scrollTop = scrollTop;
});
});

Create a 'span' element somewhere at the top of the page and set focus to this on load. The browser will scroll to the focussed element. I understand that this is a workaround and focus on 'span' doesn't work in all browsers ( uhmmm.. Safari ). Hope this helps.

Here is what I have implemented on a site that wanted the scroll position to focus to a specific element when the poststate is fired (back button):
$(document).ready(function () {
if (window.history.pushState) {
//if push supported - push current page onto stack:
window.history.pushState(null, document.title, document.location.href);
}
//add handler:
$(window).on('popstate', PopStateHandler);
}
//fires when the back button is pressed:
function PopStateHandler(e) {
emnt= $('#elementID');
window.scrollTo(0, emnt.position().top);
alert('scrolling to position');
}
Tested and works in firefox.
Chrome will scroll to position but then repositions back to original place.

Like others said, there is no real way to do it, only ugly hacky way. Removing the scroll event listener didn't work for me, so here's my ugly way to do it:
/// GLOBAL VARS ////////////////////////
var saveScrollPos = false;
var scrollPosSaved = window.pageYOffset;
////////////////////////////////////////
jQuery(document).ready(function($){
//// Go back with keyboard shortcuts ////
$(window).keydown(function(e){
var key = e.keyCode || e.charCode;
if((e.altKey && key == 37) || (e.altKey && key == 39) || key == 8)
saveScrollPos = false;
});
/////////////////////////////////////////
//// Go back with back button ////
$("html").bind("mouseout", function(){ saveScrollPos = false; });
//////////////////////////////////
$("html").bind("mousemove", function(){ saveScrollPos = true; });
$(window).scroll(function(){
if(saveScrollPos)
scrollPosSaved = window.pageYOffset;
else
window.scrollTo(0, scrollPosSaved);
});
});
It works in Chrome, FF and IE (it flashes the first time you go back in IE). Any improvement suggestions are welcome! Hope this helps.

Might want to try this?
window.onpopstate = function(event) {
return false;
};

Related

On Scroll fires automatically on page refresh

Im halfway down my page as I have an anchor. I also have a window scroll event:
$(window).scroll(function(){
});
When I refresh the page, the scroll event fires. Is there a way to prevent this on the refresh but still listen for the scroll event when the user scrolls?
I believe your scroll code only fires if you refresh the page and the page is scrolled. That's because the browser will reload the page, and then scroll to the original position.
The solution suggested by Arnelle does not work well, because the scroll event only fires initially if the page was scrolled when you refreshed it.
Hack Alert
What I found that does work is waiting to set the scroll handler. Be careful, I'm using magic numbers that may not work on all connections.
//Scroll the page and then reload just the iframe (right click, reload frame)
//Timeout of 1 was not reliable, 10 seemed to be where I tested it, but again, this is not very elegant.
//This will not fire initially
setTimeout(function(){
$(window).scroll(function(){
console.log('delayed scroll handler');
});
}, 10);
//This will fire initially when reloading the page and re-establishing the scroll position
$(window).scroll(function(){
console.log('regular scroll handler');
});
div {
height: 2000px;
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
</div>
And yes, I know I bashed Arnelle by saying they patched a problem without understanding, and here I am, suggesting a patch. The only difference is that I think I understand the problem.
My main question to the OP is. Most scroll handlers that I've written work fine if called multiple times, what is the problem with your handler being called an extra time?
You can use a flag variable to detect whether the scroll event is the initial scroll event or not, effectively stopping the callback function's execution for the scroll event on page reload.
var initialScrollEvent = true;
$(window).scroll(function() {
if (!initialScrollEvent) {
// callback function body
}
initialScrollEvent = false;
});

Disable taphold default event, cross device

I'm struggling to disable default taphold browser event. Nothing that I have found on Google provided any help. I have only Android 4.4.4 mobile and Chrome dev tools for testing. I tried CSS fixes, such as webkit-touch-callout and others, but apparently they don't work for Android, also they don't work in Chrome dev tools.
I also tried detecting right click, (e.button==2), it doesn't work.
I came up with a solution, but it solves one problem and creates another. I just want to have a custom action for 'long press' event for selected anchors and I don't want the default pop up to appear (open in a new tab, copy link address, etc.)
This is what I did:
var timer;
var tap;
$("body").on("touchstart", my_selector, function(e) {
e.preventDefault();
timer = setTimeout(function() {
alert('taphold!');
tap=false;
},500);
});
$("body").on("touchend", my_selector, function() {
if(tap) alert('tap');
else tap=true;
clearTimeout(timer);
});
It successfully disables the default taphold event and context menu doesn't appear. However it also disables useful events, such as swipe. The links are in a vertical menu and the menu is higher than the screen, so a user has to scroll it. If he tries to scroll, starting on an anchor, it won't scroll, it will alert 'tap!'
Any ideas how could I disable taphold default or how could I fix this code so it disables only tap events and leave default swipe events enabled?
Edit: Now I thought about setting a timeout, if the pointer is in the same place for lets say 100ms, then prevent default action. However e.preventDefault(); doesn't work inside setTimeout callback.
So now I'm just asking about the simplest example. Can I prevent default actions after certain amount of time has passed (while the touch is still there).
And this is my whole problem in a fiddle. http://jsfiddle.net/56Szw/593/
This is not my code, I got this from http://www.gianlucaguarini.com/blog/detecting-the-tap-event-on-a-mobile-touch-device-using-javascript/
Notice that while swiping the box up and down, scrolling doesn't work.
I got the solution. It was so simple! I had no idea there's an oncontextmenu event. This solves everything:
$("body").on("contextmenu", my_selector, function() { return false; });
For an <img> I had to use event.preventDefault() instead of return false.
document.querySelector('img').addEventListener('contextmenu', (event) => {
event.preventDefault();
}

In iOS 7 Safari, how do you differentiate popstate events via edge swipe vs. the back/fwd buttons?

In iOS 7 Safari there are now two ways to navigate back/forward -- using the traditional back/forward button arrows at the bottom or by swiping from the edge of the screen. I'm using an animation to transition between pages in my ajax app, but I don't want to fire that transition if users are navigating via the edge swipe, because that is an animation itself.
However, the popstate event objects appear to be identical for both types of navigation -- is there any way to differentiate between these two types of user navigations so we can respond accordingly?
UPDATE: I was able to use (what appears to be) a bug in iOS7 Safari to detect correctly the edge swipe vs. back button tap. The bug is that the touchend event is not triggered (until the next touch event) when using the edge swipe (but touchstart and touchmove are). So I set a shouldAnimate flag and disable it on touchmove -- then if the flag is disabled and the popstate occurs, I know it's an edge swipe.
It's correct 99% of the time -- the only time where it could potentially fail is when a user edge-swipes partially and but then lets go and lets the current page snap back into place (at which point my flag would still be disabled) and then taps the back button (which fires no touch events). To handle that last [edge] case I set a timer on touchmove to re-enable the flag after 50ms.
Yes it's "dirty" but for now it gets me what I want in almost every case so I'm ok with it -- until Apple fixes the bug, but hopefully they'll also provide an indicator in the popstate event object that tells us what kind of navigation it is.
You can track edge drag navigation by monitoring the touch events. If the user starts dragging within a certain threshold of the edge of their screen, it will trigger an edge drag navigation transition.
I wrote an extended explanation of how to monitor and act on this using React code here: https://gist.github.com/MartijnHols/709965559cbdb6b241c12e5866941e69. The essential detection part can be achieved in regular JavaScript, like so:
window.isEdgeDragNavigating = false
const IOS_EDGE_DRAG_NAVIGATION_THRESHOLD = 25
let timer
const handleTouchStart = (e) => {
if (
e.touches[0].pageX > IOS_EDGE_DRAG_NAVIGATION_THRESHOLD &&
e.touches[0].pageX <
window.innerWidth - IOS_EDGE_DRAG_NAVIGATION_THRESHOLD
) {
return
}
window.isEdgeDragNavigating = true
if (timer) {
clearTimeout(timer)
}
}
const handleTouchEnd = () => {
timer = setTimeout(() => window.isEdgeDragNavigating = false, 200)
}
document.addEventListener('touchstart', handleTouchStart)
document.addEventListener('touchend', handleTouchEnd)
Short and sad answer: No. This back/forward-swipes are not propagated to the actual page but happen on an OS-level.

How do I detect whether scroll events are fired *only once* like on touch devices?

iOS devices (and likely Android ones) have a different scrolling behavior: The scroll event is only fired once after the entire scroll is done.
How do I detect whether the browser behaves this way?
I could use window.Touch or Modernizr.touch but they don't tell me anything about the scroll behavior, it would be like asking if someone is French to understand whether they like croissants, right? :)
I think you're right about the detection because there will be some devices that will support both touch and mouse behaviors (like Windows 8 tablets), some will only support touch (phones) and some will only support mouse (desktops). Because of that, I don't think you can conclusively say that a device only has one behavior as some could have both.
Assuming that what you're really trying to do is to figure out whether you should respond immediately to every scroll event or whether you should use a short delay to see where the scroll destination ends up, then you could code a hybrid effect that could work well in either case.
var lastScroll = new Date();
var scrollTimer;
window.onscroll = function(e) {
function doScroll(e) {
// your scroll logic here
}
// clear any pending timer
if (scrollTimer) {
clearTimeout(scrollTimer);
scrollTimer = null;
}
var now = new Date();
// see if we are getting repeated scroll events
if (now - lastScroll < 500){
scrollTimer = setTimeout(function() {
scrollTimer = null;
doScroll(e);
}, 1000);
} else {
// last scroll event was awhile ago, so process the first one we get
doScroll(e);
}
lastScroll = now;
};
doScroll() would be your scroll processing logic.
This gets you a hybrid approach. It always fires on the first scroll event that arrives when there hasn't recently been a scroll event. If there are a series of scroll events, then it fires on the first one and then waits until they stop for a second.
There are two numbers that you may want to tweak. The first determines how close scroll events must be to consider them rapid fire from the same user action (current set to 500ms). The second determines how long you wait until you process the current scroll position and assume that the user stopped moving the scrollbar (currently set to 1s).

How to determine if a user is actually looking at a web page?

Is it possible to determine whether a user is active on the current web page or, say, focused on a different tab or window?
It seems that if you switch tabs, any JavaScript set on a timeout/interval continues running. It would be nice to be able to 'pause' the events when the user is not on the page.
Would something like attaching a mouseover event to the body work, or would that be too resource-intensive?
You can place onfocus/onblur events on the window.
There's wide support for those events on the window.
Example: http://jsfiddle.net/xaTt4/
window.onfocus = function() {
// do something when this window object gets focus.
};
window.onblur = function() {
// do something when this window object loses focus.
};
Open Web Analytics (and perhaps some other tracking tools) has action tracking
You could keep an alive variable going using mousemove events (assuming the user does not leave the mouse still on the page). When this variable (a timestamp likely) has not been updated in x seconds, you could say the page is not active and pause any script.
As long as you do not do a lot of processing in the body event handler you should be okay. It should just update the variable, and then have a script poll it at a certain interval to do the processing/checks (say every 1000ms).
Attach listeners to mousemove, keyup and scroll to the document.
I use this throttle/debounce function (which works without jQuery, even though it's a jQuery plugin if jQuery is present) to only run code in response to them once in ~250ms, so that you're not firing some code on every pixel of the mouse moving.
You can also use the visibilityState of the document:
document.addEventListener("visibilitychange", function() {
if( document.visibilityState === 'visible' ) {
// Do your thing
}
});
There is a wide acceptance of this API.

Categories