I need to set up a custom script for tracking a users click through on a form submission field. This is what I've got so far. As the user navigates down through the form fields the counter variable (base) totals up how far along the path the user has reached. I want to send the results off when the user leaves the page by sending out the base variable. I'm thinking of using the .unload function in jQuery. However for some reason unload isn't responding the way I think it should. Any ideas?
var base = 0; //declares a variable of 0. This should refresh when a new user lands on the form page.
function checkPath(fieldNo, path) { //this function should check the current base value of the base variable vs the current fieldNo
if (fieldNo >= path) { //checks to see if base if lower than fieldNo
base = fieldNo; //if true, base is set to current fieldNo
return base;
} else {
return base; //if false, simply returns base.
}
};
$('#order_customer_fields_forename').focus(function () { //when the form box is selected should run checkPath then alert result.
checkPath(1, base);
});
$('#order_customer_fields_surname').focus(function () {
checkPath(2, base);
});
$('#order_customer_fields_postcode').focus(function () {
checkPath(3, base);
});
$('#order_customer_fields_address1').focus(function () {
checkPath(4, base);
});
$('#order_customer_fields_address2').focus(function () {
checkPath(5, base);
});
$(window).unload(function () {
alert(base);
});
The unload event fires too late for the effect you need. You should try using the onbeforeunload event using either vanilla Javascript:
window.onbeforeunload = function (e) {
// Your code here
};
Or jQuery:
$(window).bind('beforeunload', function (e) {
// Your code here
});
Either way, you should be aware that this is not an ideal solution for what you are trying to achieve. This event is implemented unevenly across browsers. Chrome seems to be the most restrictive, and IE the most permissive, in its implementation.
A different direction you may want to take is sending the data to the server by XHR whenever the user completes a field.
Related
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.
I tried using a hack described in various locations which uses:
document.body.onfocus = checkOnCancel();
An example:
var fileSelectEle = document.getElementById('fileinput');
fileSelectEle.onclick = charge;
function charge()
{
document.body.onfocus = checkOnCancel;
}
function checkOnCancel()
{
alert("FileName:" + fileSelectEle.value + "; Length: " + fileSelectEle.value.length);
if(fileSelectEle.value.length == 0) alert('You clicked cancel!')
else alert('You selected a file!');
document.body.onfocus = null;
}
Is there something wrong here? Because fileSelectedEle.value always returns the previous execution value and NOT the one selected by the user.
Is this the expected behavior of input file? How to resolve this to read the actual file selected?
http://jsfiddle.net/smV9c/2/
You can reproduce the error by:
Step 1: SelectFile - some select some file (and notice the output)
Step 2: SelectFile - press cancel (and notice the output)
One solution is to use the onchange event of the input.
var fileSelectEle = document.getElementById('fileinput');
fileSelectEle.onchange = function ()
{
if(fileSelectEle.value.length == 0) {
alert('You clicked cancel - ' + "FileName:" + fileSelectEle.value + "; Length: " + fileSelectEle.value.length);
} else {
alert('You selected a file - ' + "FileName:" + fileSelectEle.value + "; Length: " + fileSelectEle.value.length);
}
}
This responds correctly to changes in the selected filename, as you can test here: http://jsfiddle.net/munderwood/6h2r7/1/
The only potential difference in behaviour from the way you were trying to do it, is that if you cancel right away, or twice in a row, or select the same file twice in a row, then the event won't fire. However, every time the filename actually changes, you'll detect it correctly.
I don't know for sure why your original attempt didn't work, although my best guess is that it's a timing issue with the onfocus event firing asynchronously, and before the input control's properties have finished updating.
UPDATE: To determine what the user has selected every time they close the file dialog, even if nothing has changed, the timing issue can be skirted by adding a brief delay between receiving focus again, and checking the value of the file input. Instead of calling checkOnCancel immediately upon receiving focus, the following version of charge causes it to be called a tenth of a second later.
function charge() {
document.body.onfocus = function () { setTimeout(checkOnCancel, 100); };
}
Here's a working version: http://jsfiddle.net/munderwood/6h2r7/2/.
You can hook into the window.focus event which gets fired when they cancel window's file select box. Then check to see if it actually has a file selected.
//This code works in chrome for file selection try it
<--write this line in HTML code-->
<input type='file' id='theFile' onclick="initialize()" />
var theFile = document.getElementById('theFile');
function initialize() {
document.body.onfocus = checkIt;
console.log('initializing');
}
function checkIt() {
setTimeout(function() {
theFile = document.getElementById('theFile');
if (theFile.value.length) {
alert('Files Loaded');
} else {
alert('Cancel clicked');
}
document.body.onfocus = null;
console.log('checked');
}, 500);
}
It gets tricky to handle all of the various ways that a user can cancel file input.
On most browsers, the file picker immediately opens and takes the user out of the browser. We can use the window.focus event to detect when they come back without selecting anything to detect cancellation
On ios browsers, the user first sees an ios modal that lets them pick between camera -vs- gallery. User's can cancel from here by clicking away from the modal. So, we can use the window.touchend to detect this
there are likely other browsers and cases that act differently on cancellation, that this hasn't caught yet, too
Implementation wise, you can use addEventListener to make sure that you dont replace other event listeners that may already be on the window - and to easily clean up the event listener after it fires. For example:
window.addEventListener('focus', () => console.log('no file selected'), { once: true });
Here is an example of how you can use this to get images programatically, handling the considerations listed above (typescript):
/**
* opens the user OS's native file picker, returning the selected images. gracefully handles cancellation
*/
export const getImageFilesFromUser = async ({ multiple = true }: { multiple?: boolean } = {}) =>
new Promise<File[]>((resolve) => {
// define the input element that we'll use to trigger the input ui
const fileInput = document.createElement('input');
fileInput.setAttribute('style', 'visibility: hidden'); // make the input invisible
let inputIsAttached = false;
const addInputToDom = () => {
document.body.appendChild(fileInput); // required for IOS to actually fire the onchange event; https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only
inputIsAttached = true;
};
const removeInputFromDom = () => {
if (inputIsAttached) document.body.removeChild(fileInput);
inputIsAttached = false;
};
// define what type of files we want the user to pick
fileInput.type = 'file';
fileInput.multiple = multiple;
fileInput.accept = 'image/*';
// add our event listeners to handle selection and canceling
const onCancelListener = async () => {
await sleep(50); // wait a beat, so that if onchange is firing simultaneously, it takes precedent
resolve([]);
removeInputFromDom();
};
fileInput.onchange = (event: any) => {
window.removeEventListener('focus', onCancelListener); // remove the event listener since we dont need it anymore, to cleanup resources
window.removeEventListener('touchend', onCancelListener); // remove the event listener since we dont need it anymore, to cleanup resources
resolve([...(event.target!.files as FileList)]); // and resolve the files that the user picked
removeInputFromDom();
};
window.addEventListener('focus', onCancelListener, { once: true }); // detect when the window is refocused without file being selected first, which is a sign that user canceled (e.g., user left window into the file system's file picker)
window.addEventListener('touchend', onCancelListener, { once: true }); // detect when the window is touched without a file being selected, which is a sign that user canceled (e.g., user did not leave window - but instead canceled the modal that lets you choose where to get photo from on ios)
// and trigger the file selection ui
addInputToDom();
fileInput.click();
});
Is there something wrong here? Because fileSelectedEle.value always returns the previous execution value and NOT the one selected by the user. Is this the expected behavior of input file? How to resolve this to read the actual file selected?
There's nothing wrong, this is expected behaviour. If the user cancels the file selection process, then it's as if they never started it. So the previous value is left in place.
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 ;)
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.
How to prevent a webpage from navigating away using JavaScript?
Using onunload allows you to display messages, but will not interrupt the navigation (because it is too late). However, using onbeforeunload will interrupt navigation:
window.onbeforeunload = function() {
return "";
}
Note: An empty string is returned because newer browsers provide a message such as "Any unsaved changes will be lost" that cannot be overridden.
In older browsers you could specify the message to display in the prompt:
window.onbeforeunload = function() {
return "Are you sure you want to navigate away?";
}
Unlike other methods presented here, this bit of code will not cause the browser to display a warning asking the user if he wants to leave; instead, it exploits the evented nature of the DOM to redirect back to the current page (and thus cancel navigation) before the browser has a chance to unload it from memory.
Since it works by short-circuiting navigation directly, it cannot be used to prevent the page from being closed; however, it can be used to disable frame-busting.
(function () {
var location = window.document.location;
var preventNavigation = function () {
var originalHashValue = location.hash;
window.setTimeout(function () {
location.hash = 'preventNavigation' + ~~ (9999 * Math.random());
location.hash = originalHashValue;
}, 0);
};
window.addEventListener('beforeunload', preventNavigation, false);
window.addEventListener('unload', preventNavigation, false);
})();
Disclaimer: You should never do this. If a page has frame-busting code on it, please respect the wishes of the author.
The equivalent in a more modern and browser compatible way, using modern addEventListener APIs.
window.addEventListener('beforeunload', (event) => {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = '';
});
Source: https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
I ended up with this slightly different version:
var dirty = false;
window.onbeforeunload = function() {
return dirty ? "If you leave this page you will lose your unsaved changes." : null;
}
Elsewhere I set the dirty flag to true when the form gets dirtied (or I otherwise want to prevent navigating away). This allows me to easily control whether or not the user gets the Confirm Navigation prompt.
With the text in the selected answer you see redundant prompts:
In Ayman's example by returning false you prevent the browser window/tab from closing.
window.onunload = function () {
alert('You are trying to leave.');
return false;
}
The equivalent to the accepted answer in jQuery 1.11:
$(window).on("beforeunload", function () {
return "Please don't leave me!";
});
JSFiddle example
altCognito's answer used the unload event, which happens too late for JavaScript to abort the navigation.
That suggested error message may duplicate the error message the browser already displays. In chrome, the 2 similar error messages are displayed one after another in the same window.
In chrome, the text displayed after the custom message is: "Are you sure you want to leave this page?". In firefox, it does not display our custom error message at all (but still displays the dialog).
A more appropriate error message might be:
window.onbeforeunload = function() {
return "If you leave this page, you will lose any unsaved changes.";
}
Or stackoverflow style: "You have started writing or editing a post."
If you are catching a browser back/forward button and don't want to navigate away, you can use:
window.addEventListener('popstate', function() {
if (window.location.origin !== 'http://example.com') {
// Do something if not your domain
} else if (window.location.href === 'http://example.com/sign-in/step-1') {
window.history.go(2); // Skip the already-signed-in pages if the forward button was clicked
} else if (window.location.href === 'http://example.com/sign-in/step-2') {
window.history.go(-2); // Skip the already-signed-in pages if the back button was clicked
} else {
// Let it do its thing
}
});
Otherwise, you can use the beforeunload event, but the message may or may not work cross-browser, and requires returning something that forces a built-in prompt.
Use onunload.
For jQuery, I think this works like so:
$(window).unload(function() {
alert("Unloading");
return falseIfYouWantToButBeCareful();
});
If you need to toggle the state back to no notification on exit, use the following line:
window.onbeforeunload = null;