history.js icon indicating copy to clipboard operation
history.js copied to clipboard

Pushing a state with an anchor in the url fails

Open balupton opened this issue 13 years ago • 11 comments

@markjaquith found a bug with History.js which prevents the statechange event from being fired if the pushed state contains a anchor in the url. It is known to affect Chrome.

balupton avatar Apr 01 '11 20:04 balupton

As pushing a URL with a anchor won't work in a HTML4 browser, as they will already have a hash (the state!) we have to handle this specially. Here is the proposed solution:

Lets say I do: History.pushState(null,null,'?myState#myAnchor'); HTML5 Browser will get: mysite.com/?myState#myAnchor HTML4 Browser will get: mysite.com/#?myState&_anchor=myAnchor

In statechange, both will get: History.getState().anchor === 'myAnchor'

Which they can then choose to scroll to if they wish in the own code using something like: if ( History.getState().anchor && $.fn.ScrollTo ) $('#'+History.getState().anchor).ScrollTo();

This is the solution adopted by jQuery Ajaxy, as seen by the implementation on the balupton.com website: http://balupton.com/#/welcome?anchor=home-featured

balupton avatar Apr 01 '11 21:04 balupton

This is actually going to be a really big update. It will require the elimination of the anchorchange event and also require the change in issue #49 to be completed.

The statechange handler would look something like this:

$(window).bind('statechange',function(){
    var
        State = History.getState(),
        anchor = State.anchor,
        same = State.same,
        url = State.url;

    // Load In Content
    if ( same ) {
        if ( anchor && $.fn.ScrollTo ) {
            $('#'+anchor).ScrollTo();
        }
        return;
    }
    else {
        $.ajax({
            url: url,
            success: function(data){
                $content.html(data);
                if ( $.fn.ScrollTo ) {
                    if ( anchor ) {
                        $('#'+anchor).ScrollTo();
                    }
                    else {
                        $content.ScrollTo();
                    }
                }
            },
            error: function(){
                document.location.href = url;
            }
        });
    }
});

balupton avatar Apr 16 '11 14:04 balupton

To add onto this.

The new flags are completed for HTML5 browsers - though in persistance mode I am having some issues.

For HTML4 browsers there will be a counter-intuitive but necessary change:

  1. Say if I'm on the state: http://mysite.com/#/page/one
  2. And I say I do: document.location.hash = 'top'; which will go here: http://mysite.com/#top
  3. That will be transformed into: http://mysite.com/#/page/one?_anchor=top
  4. And the http://mysite.com/#top state will be discarded

balupton avatar Apr 19 '11 18:04 balupton

This sound good to me and I am in need of it.

cadorn avatar Jun 10 '11 17:06 cadorn

I'm thinking of writing a function to recursively strip #s from the state object prior to a push (or replace). I have some high-priority HTML4 devices that are causing the "states / fragments with hashes are unsupported" message, and unfortunately have a deadline.

jokeyrhyme avatar Oct 24 '11 20:10 jokeyrhyme

For anyone interested, here's how I've temporarily fixed this issue: ` function trimHistoryState(state) { var index, array, object, type = $.type(state); /* END: var */ if (type === 'string' && state.length > 0) { index = state.indexOf('#'); if (index !== -1) { state = state.substring(0, index); } } else if (type === 'array' && state.length > 0) { array = []; $.each(state, function(index, value) { array.push(trimHistoryState(value)); }); state = array; } else if (type === 'object') { object = {}; $.each(state, function(key, value) { object[key] = trimHistoryState(value); }); state = object; } return state; }

        _pushState = History.pushState;
        History.pushState = function() {
            arguments = $.makeArray(arguments);
            if (arguments.length >= 1) {
                arguments[0] = trimHistoryState(arguments[0]);
            }
            if (arguments.length >= 3) {
                arguments[2] = trimHistoryState(arguments[2]);
            }
            _pushState.apply(History, arguments);
        }
        _replaceState = History.replaceState;
        History.replaceState = function() {
            arguments = $.makeArray(arguments);
            if (arguments.length >= 1) {
                arguments[0] = trimHistoryState(arguments[0]);
            }
            if (arguments.length >= 3) {
                arguments[2] = trimHistoryState(arguments[2]);
            }
            _replaceState.apply(History, arguments);
        }

`

This requires jQuery (hence the $s) and it seems to be working okay. I put this in my own code, and run it after the HistoryJS library is loaded but before I start using it.

This is definitely not a long term solution, and probably breaks other things, but it'll do me until the 1.8 release that solves all the world's hunger problems with a piece of string. :)

jokeyrhyme avatar Oct 25 '11 05:10 jokeyrhyme

I tried this solution, but I need the page to scroll to my anchor, not just strip them out. In the end I did it this way:

        $(window).bind('anchorchange',function (event) {
            var state = History.getState().hash,
                stateWithoutHash,
                index = state.indexOf('#');
            if (index === -1) {
                window.location.reload(); //there is no hash in the current state but there is in the window, so this should fire a reload
            } else {
                stateWithoutHash = state.substring(0, index);
            }
            if(window.location.pathname !== stateWithoutHash) {
                window.location.reload(); //the url does have a hash, but it's not on this page, it's on a different page, so we need to reload
            }
        });

This fires when the anchor changes (which is what happens when you try and go to a url with an anchor) and then (unless this anchor is on the current page/state, it forces a reload of the page. It's not pretty but it works!

scruffian avatar Mar 09 '12 13:03 scruffian

I use this code to fix this issue :

History.Adapter.bind(window, "anchorchange", function(){
    var State = History.getLastStoredState(); 
    History.saveState(State);  
    History.Adapter.trigger(window, "statechange");
});

History.Adapter.bind(window, "statechange", function(){
    var State = History.getState(), 
         url = State.url, 
         hashIndex = url.indexOf('#'),   
         hash = '';   

    // ....

    // and in the ajax load function
        ....
        if (hashIndex > 0) {   
            hash = url.substr(hashIndex + 1);
            win.scrollIntoView(hash);  // Use your scroll function   
        }
        ....
});

Is this way accepted?

kiddo13 avatar Jun 14 '12 10:06 kiddo13

Looks better than what I did! I need to look in more detail....

scruffian avatar Jun 14 '12 10:06 scruffian

I'm having the same issue. I couldn't get kiddo13's code to work.

cvn avatar Sep 23 '12 07:09 cvn

Hi, any progress on this? Still experiencing the issue on Chrome

jghlt avatar Jan 09 '13 23:01 jghlt