1
\$\begingroup\$

I've discovered that Google Chrome on iOS doesn't fire the hashchange event when the back / forward buttons are pressed.

Reference:

To work around this problem, I wrote code that uses setInterval() to check location.hash and trigger hashchange when it changes. This seems to have a lot of potential points of failure and, with setInterval() I'm always worried about performance.

I didn't try any polyfills (discussed here) because they all seem to target issues in old browsers. Chrome is often treated as one user agent and functionality on different operating systems isn't compared.

Could someone take a look at this and tell me if you see any potential pitfalls?

The Javascript code is below. The HTML and CSS are mostly filler created specifically to test this functionality, but you can look at it on CodePen: http://codepen.io/Ghodmode/pen/aNbKZp

$(function(){
  // Poll for hash changes on browsers that don't fire the
  // hashchange event.
  pollHashChange();

  // Set a click event handler for each of the navigation
  // links to use window.scrollTo and history.pushState
  // instead of the default action.
  $('#nav a').each(function() {
    $(this).on('click', function($e) {
      $e.preventDefault();
      var href = $(this).attr('href');

      // Turn off hash polling so that it doesn't catch
      // this change.
      clearInterval(hashPollInterval);

      // Only push the hash onto the URL if we didn't click
      // on the same item twice in a row.
      if (href != lastHash) {
        history.pushState(null, null, href);
      }

      lastHash = href;
      scrollToTarget(href);

      // Turn hash polling back on.
      pollHashChange();
    });
  });

  // Set the value of the current scroll position whenever
  // we scroll. This value is used later to prevent
  // jumping to a location when the back / forward button is
  // clicked.
  $(window).on('scroll', function() {
    currentScrollPosition = window.pageYOffset;
  });

  $(window).on('hashchange', function() {
    // Turn off hash change polling here so that it doesn't
    // fire again before the animated scrolling is complete.
    clearInterval(hashPollInterval);

    var hash = location.hash;
    lastHash = hash;

    // Force the scroll position back to the current scroll
    // position. This prevents the back / forward button
    // from jumping to a location so that we can animate
    // scrolling.
    window.scrollTo(0, currentScrollPosition);
    scrollToTarget(hash);
    pollHashChange();
  });
});

var hashPollInterval;
var lastHash = '';
var currentScrollPosition = 0;

var scrollToTarget = function(target) {
  if (target != '') {
    $('html, body').animate({
      'scrollTop': $(target).offset().top + 'px'
    });
  } else {
    $('html, body').animate({
      'scrollTop': 0 + 'px'
    });
  }
};

var pollHashChange = function() {
  var checkHashChange = function() {
    if (location.hash != lastHash) {
      console.info('hashchange!!');
      console.info('location.hash: "' + location.hash);
      console.info('lastHash: "' + lastHash);
      $(window).trigger('hashchange');
    }
  };
  hashPollInterval = setInterval(checkHashChange, 200);
};
\$\endgroup\$
1
\$\begingroup\$
hashPollInterval = setInterval(checkHashChange, 200);

200ms seems overkill. I read somewhere that 200ms is "instant" to a human. However, the 400-600ms range is considered "acceptable" for a delay. Besides, how often do you actually need the event happening anyways?

console.info('hashchange!!');
console.info('location.hash: "' + location.hash);
console.info('lastHash: "' + lastHash);

I wouldn't litter code with console calls. While it's not wrong, one can easily forget that they're there. Use breakpoints instead.

$(window).trigger('hashchange');

First, using jQuery for this operation is overkill. Suggesting you build a little event mechanism instead, where you can register callbacks.

Second, every call to $ wraps window and generates a new jQuery object. This can kill performance. Put the result of $(window) in a variable and put it outside the operation. The same applies to $('html, body') in your other function.

Lastly, falsely triggering hashchange can have consequences. If your implementation of hashchange jQuery event differs from the one jQuery provides, you can break code that expects the jQuery implementation. AFAIK, jQuery provides an event object on any event. If you fail to provide something similar and some code expects it to be there, then that code will break.

\$\endgroup\$
  • \$\begingroup\$ Yes, 200ms is too often. I'll try 500ms. I forgot those console calls were there :P I put $(window) and $('html, body') into variables... good idea! .trigger() is much easier for me than a custom event with vanilla Js. In this case, I'll favor easy over efficient. I wouldn't use all of jQuery just to trigger an event, but I'm already using it for animation. I don't see the problem with falsely triggering hashchange. There's no default action for the DOM event and the handler doesn't even use the Event object. \$\endgroup\$ – Vince Feb 28 '16 at 23:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.