javascriptbrowser-history

Detecting if navigation using history API is back or forward


Is there a consistent way to determine if a popstate is caused by the forward or the back button?

I am currently saving a time stamp in the state when a new page is added, which means that navigation is working correct for the most parts, but if for instance there are three pages loaded using history.push and the user navigates back to the second and refreshes it, the time stamp is renewed, and therefor the animation starts to get all weird, because the forward is now to a page with a lower timestamp, triggering the back animation.

var StateManager = function (wrapper) {
    'use strict';
    
    var me = this;
    
    me.add = function (url, elm, title) {
        var d = new Date(),
            time = d.getTime();
        
        removeOld();
        elm.setAttribute('data-time', time);
        wrapper.appendChild(elm);       
        invokeScriptsInView(elm);
        
        if (!title) {
            document.title = title;
        }
        
        history.pushState({
            screen: elm.innerHTML,
            time: time,
            title: title
        }, null, '?id=' + time);
    }
    
    function invokeScriptsInView(page) {
        var scripts = page.querySelectorAll('script');
        for (var i = 0; i < scripts.length; i++) {
            var f = new Function(scripts[i].innerHTML);
            f.apply(window);
        }
    }
    
    function addCurrent() {
        var d = new Date(),
            time = d.getTime();
        
        if (!!history.state && !!history.state.time) {
            time = history.state.time;
        }
        var current = wrapper.children[wrapper.children.length - 1];
        current.setAttribute('data-time', time);
        var content = !!current ? current.innerHTML : '';
        history.replaceState({
            screen: content,
            time: time
        }, null, location.href);
    }
    
    function removeOld(back) {
        if (wrapper.children.length < 1) return;
        var current = wrapper.children[wrapper.children.length - 1];
        if (!current) return;
        current.addEventListener("webkitAnimationEnd", function() {
            current.parentElement.removeChild(current);
        }, false);
        if (back === true) {
            current.classList.add('back');
        } else {
            current.classList.remove('back')
        }
        current.classList.add('remove');
    }
    
    window.addEventListener('popstate', function(e) {
        if (!e.state) {
            return;
        }
        var current = wrapper.children[wrapper.children.length - 1],
            currentTime = current.attributes['data-time'].value,
            state = e.state,
            found = false,
            elm = document.createElement('div');

        elm.innerHTML = state.screen;
        elm.setAttribute('data-time', state.time);
        elm.className = 'from-state';

        if (currentTime > e.state.time) {
            elm.classList.add('back');
        }
        
        if (!state.title) {
            document.title = state.title;
        }
        removeOld(currentTime > e.state.time);
        wrapper.appendChild(elm);
        invokeScriptsInView(elm);
    });
    
    addCurrent();
};

var stateManager = new StateManager(document.getElementById('wrapper'));


function getClockTime(timeZone) {
    var now = new Date();
    var hour = now.getHours() + timeZone;
    var minute = now.getMinutes();
    var second = now.getSeconds();
    if (hour == 0) hour = 12;
    if (hour < 10) hour = "0" + hour;
    if (minute < 10) minute = "0" + minute;
    if (second < 10) second = "0" + second;
    var timeString = hour + ':' + minute + ':' + second;

    return timeString;
}

function add(init) {
    var elm = document.createElement('div');
    elm.innerHTML = '<div><h1>' + getClockTime(1) + '</h1></div>';
    stateManager.add('?id=' + (new Date()).getTime(), elm, init);
}

function back() {
    history.back();
}

function forward() {
    history.forward();
}
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: normal;
  src: url(https://fonts.gstatic.com/s/opensans/v43/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf) format('truetype');
}
@font-face {
  font-family: 'Orbitron';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/orbitron/v34/yMJMMIlzdpvBhQQL_SC3X9yhF25-T1nyGy6BoWg2.ttf) format('truetype');
}
@-webkit-keyframes in-right {
  0% {
    transform: translateX(200%);
  }
  100% {
    transform: none;
  }
}
@keyframes in-right {
  0% {
    transform: translateX(200%);
  }
  100% {
    transform: none;
  }
}
@-webkit-keyframes in-left {
  0% {
    transform: translateX(-200%);
  }
  100% {
    transform: none;
  }
}
@keyframes in-left {
  0% {
    transform: translateX(-200%);
  }
  100% {
    transform: none;
  }
}
@-webkit-keyframes out-left {
  0% {
    transform: none;
  }
  100% {
    transform: translateX(-200%);
  }
}
@keyframes out-left {
  0% {
    transform: none;
  }
  100% {
    transform: translateX(-200%);
  }
}
@-webkit-keyframes out-right {
  0% {
    transform: none;
  }
  100% {
    transform: translateX(200%);
  }
}
@keyframes out-right {
  0% {
    transform: none;
  }
  100% {
    transform: translateX(200%);
  }
}
body {
  background: #2c3e50;
}
.buttons {
  position: fixed;
  bottom: 25px;
  left: 0;
  width: 100%;
  text-align: center;
}
button {
  background: #4aa3df;
  border: none;
  color: #fff;
  font-family: sans-serif;
  padding: 10px 20px;
  font-weight: bold;
  text-transform: uppercase;
  border-radius: 5px;
  margin-right: 10px;
  border-bottom: solid 5px #2980b9;
  outline: none;
  transition: transform 0.2s, border 0.2s;
  z-index: 10;
}
button:active {
  border-bottom: solid 1px #2980b9;
  transform: translateY(4px);
  outline: none;
}
#wrapper {
  position: fixed;
  top: 75px;
  left: 75px;
  right: 75px;
  bottom: 120px;
}
#wrapper > div {
  margin-top: 10px;
  padding: 10px;
  position: absolute;
  left: 0%;
  height: 100%;
  width: 100%;
  -webkit-animation: in-right 1s;
          animation: in-right 1s;
  background: #34495e;
  border-radius: 5px;
  box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3);
  font-family: 'Open Sans', sans-serif;
  overflow: auto;
}
#wrapper > div h1,
#wrapper > div h2 {
  font-family: 'Orbitron', sans-serif;
}
#wrapper > div code {
  background: #fff;
  color: #000;
  padding: 15px;
  display: block;
  margin: 15px 0;
  border-radius: 4px;
}
#wrapper > div > div {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #fff;
  font-size: 1em;
}
#wrapper > div.back {
  -webkit-animation: in-left 1s;
          animation: in-left 1s;
}
#wrapper > div.remove {
  -webkit-animation: out-left 1s;
          animation: out-left 1s;
}
#wrapper > div.remove.back {
  -webkit-animation: out-right 1s;
          animation: out-right 1s;
}
#wrapper > div.from-state {
  border-color: blue;
}
<div class="buttons">
    <button onclick="add()">Add Screen</button>
    <button onclick="back()">Back</button>
    <button onclick="forward()">Forward</button>
</div>
<div id="wrapper">
    <div>
        <div>
            <h1>Server render</h1>
            
            <!--div>
                <h2>Usage:</h2>
                <p>
                Create a new instance of the state manager, with the element you want to use as your dynamic container as parameter <code>var stateManager = new StateManager(document.body);</code>
                </p>
                <p>
                    Fetch your view async using XHR, and insert it into
                    the view using an element you create, the url and the title of the page <code>stateManager.add(myurl, myelement, title);</code>
                </p>
                <p>
                    That's it. your are up and running with async
                </p>
            </div-->
        </div>
    </div>
</div>

Codepen link: https://codepen.io/morten-olsen/pen/zGZdRb


Solution

  • Okay i found a way to fix the bug, which was to look at the current history.state when the script is invoked, and if it contained a time stamp, then use it as the current views time stamp, as the current state are actually transferred between refreshes