History back hook on JavaScript - javascript

Is there a way to provide a hook such as onHistoryBack? I'm currently using history.js with
History.Adapter.bind (window, 'statechange', function () {});
But I have no way to ask if user presed history.back() or if it's result of a History.pushState() call.. any idea?

The way I did this was to set a variable set to true on each click on the actual site. The statechange event would then check this variable. If it was true, they had used a link on the site. If it was false, they had clicked the browsers back button.
For example:
var clicked = false;
$(document).on('click','a',function(){
clicked = true;
// Do something
});
History.Adapter.bind (window, 'statechange', function () {
if( clicked ){
// Normal link
}else{
// Back button
}
clicked = false;
});
Hope that helps :)

All of the states are stored in History.savedStates. Each time back is pushed another state is added. So in theory you could test History.savedStates to see if History.savedStates[History.savedStates.length - 2] == currentState. That would indicate the user went from step a, to step b, back to step a. However the user could get there other ways than the back button - so you may need to use this in combination with user events.
You can also use the History.getStateByIndex method to return a saved state.

Related

How can I detect back button in the browser?

I have a function named back() which will be used for ajax calls. Actually I have an array stack contains last 5 search results and that back function will switch to the previous result set (according to that array stack) and it even changes the URL using window.history.pushState() when you click on the back button.
That back button I was talking about, is an element inside the browser which revokes back() function. Now I want to revoke back() function also when user click on the back button of the browser. Something like this:
window.onhashchange = function() {
back(); // this function also changes the url
}
But sadly window.onhashchange will be revokes twice when I click on the back of the browser. Because window.onhashchange will be revoked when you change the URL using window.history.pushState().
Anyway, how can I detect what things changes the URL? Either my JS code or the back button of the browser?
You can use performance.navigation.type
At any given point, for example on document.onload, you can read the value of type and, if it's:
0 The page was accessed by following a link, a bookmark, a form submission, a script, or typing the URL in the address bar.
1 The page was accessed by clicking the Reload button or via the Location.reload() method.
2 The page was accessed by navigating into the history.
255 any other way.
Just beware that support is limited according to the compatibilty table.
However, from the looks of it, it seems the table is outdated. It says it is not supported on chrome and I just tested it and works as expected on my chrome version (67.0)
One of solution is to implement onunload event with localstorage option.
This is from my head maybe you will need correction but this is base !
var history = [];
window.onload = function(){
var handler;
if ( localStorage.getItem('history') == null ) {
// FIRST TIME
history[0] = window.location.href;
localStorage.setItem("history", JSON.stringify(history));
}
else {
handler = localStorage.getItem('history');
handler = JSON.parse(handler);
history = handler;
// Just compare now
if (history[history.length-1] == window.location.href) {
// no change
} else {
history.push(window.location.href);
}
}
}
window.onunload = function(){
localStorage.setItem('history', JSON.stringify(history));
}
Note :
Since 25 May 2011, the HTML5 specification states that calls to
window.alert(), window.confirm(), and window.prompt() methods may be
ignored during this event. See the HTML5 specification for more
details.

Prevent user to leave the route in a single page app with hashbang

I am using Sammy.js for my single page app. I want to create functionality similar to SO (the one when you type your question and try to leave the page and it is asking you if you are sure).
If it would not be a single page app, I would just do something like:
$(window).bind('beforeunload', function(){
return 'Are you sure you want to leave?';
});
The problem is that in single page app user do not actually leave the page, but rather changing his document.location.hash (he can leave the page by closing it). Is there a way to make something similar for a SPA, preferably with sammy.js?
We had a similar problem to solve in our Single Page Webapp at my work. We had some pages that could be dirty and, if they were, we wanted to prevent navigation away from that page until a user verifies it's okay to do so. Since we wanted to prevent navigation, we couldn't listen for the onhashchange event, which is fired after the hash is changed, not before. Therefore, we decided to override the default LocationProxy to include logic that allowed us to optionally prevent the navigation before the location was changed.
With that in mind, here is the proxy that we used:
PreventableLocationProxy = (function () {
function PreventableLocationProxy(delegateProxy, navigationValidators) {
/// <summary>This is an implementation of a Sammy Location Proxy that allows cancelling of setting a location based on the validators passed in.</summary>
/// <param name="delegateProxy" type="Sammy.DefaultLocationProxy">The Location Proxy which we will delegate all method calls to.</param>
/// <param name="navigationValidators" type="Function" parameterArray="true" mayBeNull="true">One or more validator functions that will be called whenever someone tries to change the location.</param>
this.delegateProxy = delegateProxy;
this.navigationValidators = Array.prototype.slice.call(arguments, 1);
}
PreventableLocationProxy.prototype.bind = function () {
this.delegateProxy.bind();
};
PreventableLocationProxy.prototype.unbind = function () {
this.delegateProxy.unbind();
};
PreventableLocationProxy.prototype.getLocation = function () {
return this.delegateProxy.getLocation();
};
PreventableLocationProxy.prototype.setLocation = function (new_location) {
var doNavigation = true;
_.each(this.navigationValidators, function (navValidator) {
if (_.isFunction(navValidator)) {
// I don't just want to plug the result of the validator in, it could be anything!
var okayToNavigate = navValidator(new_location);
// A validator explicitly returning false should cancel the setLocation call. All other values will
// allow navigation still.
if (okayToNavigate === false) {
doNavigation = false;
}
}
});
if (doNavigation) {
return this.delegateProxy.setLocation(new_location);
}
};
return PreventableLocationProxy;
}());
This code is pretty simple in and of itself, it is a javascript object that takes a delegate proxy, as well as one or more validator functions. If any of those validators explicitly return false, then the navigation is prevented and the location won't change. Otherwise, the navigation is allowed. In order to make this work, we had to override our anchor tags' default onclick handling to route it through Sammy.Application.setLocation. Once done, though, this cleanly allowed our application to handle the dirty page logic.
For good measure, here is our dirty page validator:
function preventNavigationIfDirty(new_location) {
/// <summary>This is an implementation of a Sammy Location Proxy that allows cancelling of setting a location based on the validators passed in.</summary>
/// <param name="new_location" type="String">The location that will be navigated to.</param>
var currentPageModels = [];
var dirtyPageModels = [];
//-----
// Get the IDs of the current virtual page(s), if any exist.
currentPageModels = _.keys(our.namespace.currentPageModels);
// Iterate through all models on the current page, looking for any that are dirty and haven't had their changes abored.
_.forEach(currentPageModels, function (currentPage) {
if (currentPage.isDirty() && currentPage.cancelled === false) {
dirtyPageModels.push(currentPage);
}
});
// I only want to show a confirmation dialog if we actually have dirty pages that haven't been cancelled.
if (dirtyPageModels.length > 0) {
// Show a dialog with the buttons okay and cancel, and listen for the okay button's onclick event.
our.namespace.confirmDirtyNavigation(true, function () {
// If the user has said they want to navigate away, then mark all dirty pages with the cancelled
// property then do the navigating again. No pages will then prevent the navigation, unlike this
// first run.
_.each(dirtyPageModels, function (dirtyPage) {
dirtyPage.cancelled = true;
});
our.namespace.sammy.setLocation(new_location);
});
// Returns false in order to explicitly cancel the navigation. We don't need to return anything in any
// other case.
return false;
}
}
Remember, this solution won't work if the user explicitly changes the location, but that wasn't a use case that we wanted to support. Hopefully this gets you closer to a solution of your own.

How to handle back button while changing the browser-URL with HTML5 pushState

I’ve made a one page site. When user clicks on the menu buttons, content is loaded with ajax.
It works fine.
In order to improve SEO and to allow user to copy / past URL of different content, i use
function show_content() {
// change URL in browser bar)
window.history.pushState("", "Content", "/content.php");
// ajax
$content.load("ajax/content.php?id="+id);
}
It works fine. URL changes and the browser doesn’t reload the page
However, when user clicks on back button in browser, the url changes and the content have to be loaded.
I've done this and it works :
window.onpopstate = function(event) {
if (document.location.pathname == '/4-content.php') {
show_content_1();
}
else if (document.location.pathname == '/1-content.php') {
show_content_2();
}
else if (document.location.pathname == '/6-content.php') {
show_content_();
}
};
Do you know if there is a way to improve this code ?
What I did was passing an object literal to pushState() on page load. This way you can always go back to your first created pushState. In my case I had to push twice before I could go back. Pushing a state on page load helped me out.
HTML5 allows you to use data-attributes so for your triggers you can use those to bind HTML data.
I use a try catch because I didn't had time to find a polyfill for older browsers. You might want to check Modernizr if this is needed in your case.
PAGELOAD
try {
window.history.pushState({
url: '',
id: this.content.data("id"), // html data-id
label: this.content.data("label") // html data-label
}, "just content or your label variable", window.location.href);
} catch (e) {
console.log(e);
}
EVENT HANDLERS
An object filled with default information
var obj = {
url: settings.assetsPath, // this came from php
lang: settings.language, // this came from php
historyData: {}
};
Bind the history.pushState() trigger. In my case a delegate since I have dynamic elements on the page.
// click a trigger -> push state
this.root.on("click", ".cssSelector", function (ev) {
var path = [],
urlChunk = document.location.pathname; // to follow your example
// some data-attributes you need? like id or label
// override obj.historyData
obj.historyData.id = $(ev.currentTarget).data("id");
// create a relative path for security reasons
path.push("..", obj.lang, label, urlChunk);
path = path.join("/");
// attempt to push a state
try {
window.history.pushState(obj.historyData, label, path);
this.back.fadeIn();
this.showContent(obj.historyData.id);
} catch (e) {
console.log(e);
}
});
Bind the history.back() event to a custom button, link or something.
I used .preventDefault() since my button is a link.
// click back arrow -> history
this.back.on("click", function (ev) {
ev.preventDefault();
window.history.back();
});
When history pops back -> check for a pushed state unless it was the first attempt
$(window).on("popstate", function (ev) {
var originalState = ev.originalEvent.state || obj.historyData;
if (!originalState) {
// no history, hide the back button or something
this.back.fadeOut();
return;
} else {
// do something
this.showContent(obj.historyData.id);
}
});
Using object literals as a parameter is handy to pass your id's. Then you can use one function showContent(id).
Wherever I've used this it's nothing more than a jQuery object/function, stored inside an IIFE.
Please note I put these scripts together from my implementation combined with some ideas from your initial request. So hopefully this gives you some new ideas ;)

Prevent more tap events on a listview (once tapped)

I have a list of items in a listview. Clicking on one li sends a JSON request and opens a description page.
Since the opening of the description page takes 1 or 2 seconds there is time to click on another item in the list which then triggers more events, which I don't want. This eventually makes the scrolling (with iscrollview) messy with the bottom bar going up and down when going back to the list.
How can I stop listening to more taps on the listview while processing the opening of the description page?
Without any to look at, it's very difficult for us to help you.
However, the simplest method of avoiding this is to use a global variable as a flag.
You would set the global variable (ie: in the root-level of your JavaSCript file), as false:
tapProcessing = false;
Then, whenever you start processing you, check against this flag and - if not true, then process.
Here's a rudimentary example to show you what I mean:
$('.selector').click(function(e){
if(!tapProcessing){
//the function is not processing, therefore set the flag to true:
tapProcessing = true;
//do your load/etc, and reset the flag to false in the callback (when finished):
$.get("test.php", function(data) {
// process your data here
// set your flag back to false:
tapProcessing = false;
});
}else{
//the function is already being processed from a previous click
}
e.preventDefault(); //assuming it's a link
});
Here are the bits of code I added :
el is my list, event is the landing page
setting tapProcessing to false should only be done when "after the changePage() request has finished loading the page into the DOM and all page transition animations have completed" (JQM doc : http://jquerymobile.com/demos/1.2.0/docs/api/events.html )
$('#el').click(function(e){
if(tapProcessing) {
alert('prevent');
e.preventDefault(); //assuming it's a link
} else {
alert('oktap');
tapProcessing = true;
}
});
$(document).bind("pagechange", function(event, options) {
if(options['toPage']['selector'] == '#event') {
tapProcessing = false;
}
});
I also set tapProcessing to false if I receive a disconnect event or on timeout.

browser back and forward button does not invoke callback method with statechange event of history.js

I used (https://github.com/browserstate/history.js) and have a piece of code like this
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState();
alert('Inside History.Adapter.bind: ' + State.data.myData);
});
function manageHistory(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.replaceState({myData: data}, null, '?stateHistory=' + uniqueId);
}
if I invoke manageHistory() after my ajax call, then History.Adapter.bind callback method get invoked correctly. However, if I click the browser back and then click forward button that result in page navigation from B to A, then A back to B, call back method inside History.Adapter.bind does not get invoked. This happen on both chrome and IE9. Anyone know how to fix this issue, I need to get the State after click browser back and then forward, to update my DOM. Please help
Note: I use version 1.7.1 jquery.history.js for html4 browser IE9
UPDATE (May 3 2013): Talk a bit more about my requirement
Sorry I was busy with other task, that not until now that I have sometimes to look at this issue. So oncomplete of an ajax call, I took that information that return to me, and push it into state. My requirement is: that if I navigate from page A to page B, and then on page B, I execute numbers of ajax requests that result in DOM manipulation (let call each ajax request that result in DOM manipulation a state). If I click the back button, I should go back to page A, and if I click the forward button, I should go to page B with the last state that I was in.
So my thought was, oncomplete of my ajax request, I would replace the last state in history with my current state (my json object is the html that I receive back from the ajax request). If I use push state, let say I am on page B, and I click one button, my page now change to aaa, I pushState aaa. Then if I click other button, my page now change to bbb, I pushState bbb. If I click the back button now, I would still be on page B, with my state in History.Adapter.bind(window, 'statechange', function() {}) show as aaa, if I click back button again, i would go to page A. I do not want this behavior. I want that if I am on page B with state bbb, and click back, I would go to page A, and if I click forward, I would go to page B with state bbb. I hope this make more sense. Please help
What you need to do is as follows:
If the page you came from is another page (e.g. you open page B and the previous page was page A) you do a pushState.
If on the other hand the previous page was the same page as you're currently on you will simply do a replaceState, replacing the information with information about your current state.
(the tracking of the previous page you will need to do manually in some variable)
This means that if you go from A to B (pushState) to new state of B (replaceState), back button (goes to state info of A) and next forward (goes to the newest state of B). Additionally if you open A the first time you need to do a replaceState with information about the state of A so that A will have some state at least (otherwise it will have an empty data object).
It might be useful to look at this answer of mine, which is simply about the way you would built an ordered history stack with pushStates. Not exactly what you want of course, but it's a bit simpler written and together with the information in this answer it should get you the behaviour you want.
I see that you are using History.replaceState which remove the last history state in the stack and replace it by the state given in parameters.
I am using History.pushState in my website, and doesn't face such an issue because this function doesnt pull the last state but add the new state above it. It is making the back-forward buttons work correcly.
I hope it helps you.
Edit: Using the example of change of select tag as event listenner:
function manageHistory(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.replaceState({myData: data}, null, '?stateHistory=' + uniqueId);
}
$('select.select').change(function(e) {
e.preventDefault();
// get url
// get data
// get uniqueId
manageHistory(url, data, uniqueId)
});
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState();
// Launch the ajax call and update DOM using State.data.myData
});
Also,
relpaceState will always erase B state.
Use pushState for A and B states.
And use replacestate for aaa, bbb, ccc states.
function manageHistoryBack(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.replaceState({myData: data}, null, '?stateHistory=' + uniqueId);
}
function manageHistory(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.pushState({myData: data}, null, '?stateHistory=' + uniqueId);
}
// Event listenner triggering aaa,bbb,ccc
$('select.select').change(function(e) {
e.preventDefault();
// get url
// get data
// get uniqueId
manageHistoryBack(url, data, uniqueId)
});
// Event listenner triggering A, B
$('select.select').change(function(e) {
e.preventDefault();
// get url
// get data
// get uniqueId
manageHistory(url, data, uniqueId)
});
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState();
// Launch the ajax call and update DOM using State.data.myData
});
Good luck

Categories