I'm creating an ASP.NET website and I want to implement logic to warn the user when they are navigating away from a page they've edited.
I found quite a bit of info on the web, although most of it seems quite outdated. Note that I still have a lot to learn about jQuery.
The following code displays the message as expected.
window.onbeforeunload = function () {
return 'You have unsaved changes!';
}
However, the following code--which is supposed to be equal to the code above only when a change is made--does not work. No warning is ever displayed.
$('input').change(function () {
window.onbeforeunload = function () { return "Your changes have not been saved?" };
});
Can someone say why the second snippet doesn't work? Or perhaps you can point me to a reference that is more recent.
Your input elements probably do not exist when the code is executed. Try using the .live function to detect changes on all input elements, or wrap your code in a $(document).ready() handler.
Example:
$('input').live("change", function () {
window.onbeforeunload = function () { return "Your changes have not been saved?" };
});
or
$(document).ready(function () {
$('input').change(function () {
window.onbeforeunload = function () { return "Your changes have not been saved?" };
});
});
Related
I've got this listener setup on a form of mine that checks for a state transition to occur via angular router. When the listener is tripped it checks if the form is dirty, if it is it throws a window.confirm alert up saying the user may have unsaved changes.
All of that looks like this
this.setListener('form.dirty-check', this.setExitCheck);
setListener = (el, cb) => {
if ($(el).length) {
cb();
} else {
setTimeout(() => {
this.setListener(el, cb);
}, 500);
}
};
setExitCheck = () => {
this.$transitions.onStart({}, () => {
if ($('#compForm').hasClass('ng-dirty')) {
if (window.confirm('You may have unsaved changes! Press ok to continue, or press cancel to go back and save your work.') === false) {
return false;
} else {
return true;
}
}
});
};
This code is working pretty well, except for a singular bit of unexpected behaviour.
For some reason, when I hit, "Ok" to leave the page the transition will fire off just fine, but if I go back to the page and try it again, I now have to hit okay twice, and get two window.confirm alerts. If I go back a third time to try, I get three window.confirm alerts, and have to hit Ok on all three of them. I tried this up to the point of receiving 10 alerts, and have to press ok 10 times.
Once I refresh the page though, it seems to reset, and I start it all over again. Works right away, then takes two Ok's, then three, and so on.
Does anyone know what might be going on causing this incremental behaviour?
ui-router won't clear listeners automatically, so you have to clear it manually.
and $transitions.onStart returns a function which will destroy the listener's hook when it's called. documentation here(the last line).
the syntax is the same as deregister events of $rootScope, refer How can I unregister a broadcast event to rootscope in AngularJS?
$scope.onStartHandler = this.$transitions.onStart(...);
$scope.$on('destroy', function() {
$scope.onStartHandler();
});
Following the answer here, I have the code below:
.directive('confirmOnExit', function($window) {
return {
scope: {},
link: function(scope) {
var message = "Changes you made may not be saved.";
$window.onbeforeunload = function(){
return message;
};
scope.$on('$locationChangeStart', function(event) {
if(!$window.confirm("Do you want to leave this page? "+message))
event.preventDefault();
});
scope.$on('$destroy',function(){
$window.onbeforeunload = null;
});
}
};
})
On Chrome, everything is fine. However, on Firefox, almost every time I click the button of the confirm dialog, the error occurs:
Error: [$rootScope:inprog] $digest already in progress
The solutions I found online mostly suggest to use $timeout. However, event.preventDefault() inside a $timeout function seems not preventing URL change. What should I do?
I just ran into this same problem. For me, calling confirm causes an error in firefox and IE. To get around it, I prevent default immediately if there's a message to show, and run the confirm in a timeout. Then, if the user clicks "leave page", I clear the onbeforeunload and use the $location service to set the url again. If your app is a single page app, $locationChangeStart will be called on the first page load, so you'll want to add in a flag at the top like: if (!hasLoaded) { hasLoaded = true; return; }
$rootScope.$on('$locationChangeStart', function (e, newUrl, oldUrl) {
// Call the function and show the confirm dialogue if necessary
if ($window.onbeforeunload) {
let message = $window.onbeforeunload();
if (message) {
// Since we're going to show a message, cancel navigation
e.preventDefault();
$timeout(() => {
if (confirm(message)) {
// User chose not to stay. Unregister the function so that it doesn't fire again.
$window.onbeforeunload = undefined;
// Run the url again. We've cleared onbeforeunload, so this won't create a loop
$location.url(newUrl.replace($window.location.protocol + '//' + $window.location.host, ''));
}
});
}
}
})
Here's my problem...
I want to preform an action once the user decides to stay.
Since I don't have a direct way of doing so, I wanted to use the setTimeout and setInterval as following:
I'm using a setInterval to check the page every seconds, and if the isOnBeforeUnload var is true, it means the user triggered the onbeforeunload event by tying to leave.
What I want to happen is counting to 1 second, assuming after a second if the page is not unloaded yet - the user stayed.
window.onbeforeunload = function() {
isOnBeforeUnload = true;
return "You have an unsaved update!";
};
window.timeout = setInterval(function() {
if (isOnBeforeUnload) {
var timeoutID = window.setTimeout(doClick, 1000);
isOnBeforeUnload = false;
}
}, 1000);
function doClick() {
//do some actions...
}
This code works fine on Chrome and IE, but not on FF... Can someone please help? I need it to work on IE9,10,11, Chrome and FF.
Thank you!
Here's a simpler solution:
window.onbeforeunload = function () {
setTimeout(function () {
console.log('Stayed');
// The user stayed, so do whatever you want
}, 100);
return 'Dont go!';
};
Demo. It works in FF and Chrome. I can't test in IE, though.
I'm trying to do page automation with PhantomJS. My goal is to be able to go to a website, click an image, and continue with other code once the page has loaded from the click. To test this I'm trying to write a script that will go to the url of the quick start guide on the PhantomJS website and then click on the PhantomJS logo bringing the page to the PhantomJS homepage. Also to render a picture of the website before and after the click to make sure the click worked. This is my current code:
var page = require('webpage').create();
page.open('http://phantomjs.org/quick-start.html', function(status) {
console.log(status);
page.render('websiteBeforeClick.png');
console.log(page.frameUrl); //check url before click
var element = page.evaluate(function() {
return document.querySelector('img[alt="PhantomJS"]');
});
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl); //check url after click
}, 3000);
console.log('element is ' + element); //check that querySelector() is returning an element
page.render('websiteAfterClick.png');
phantom.exit();
});
Problem is my before and after pictures are the same. This is my output when I run it.
success
element is [object Object]
Im using their sendEvent method from here "http://phantomjs.org/api/webpage/method/send-event.html" but I'm not sure if its working.
Also why doesnt the console.log(page.frameUrl) in my window.setTimeout() get executed?
I was looking at their page automation examples on the PhantomJS website. Particularly this one "https://github.com/ariya/phantomjs/blob/master/examples/imagebin.js".
I noticed their examples used
document.querySelector('input[name=disclaimer_agree]').click()
But when I tried it with my code I got an error.
document.querySelector('img[alt="PhantomJS"]').click();
TypeError: 'undefined' is not a function
EDIT#1:
I changed the end section of my code to this:
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl);
page.render('websiteAfterClick.png');
phantom.exit();
}, 3000);
console.log('element is ' + element);
});
Now my after image is correct. But now my question is, If I want to continue on with my code i.e. click on another element on the site, will my new code have to be all nested inside of the timeout function?
There is an example function phantom.waitFor(callback) that I explain on the following post, it goes as follows:
phantom.waitFor = function(callback) {
do {
// Clear the event queue while waiting.
// This can be accomplished using page.sendEvent()
this.page.sendEvent('mousemove');
} while (!callback());
}
This can help streamline your code and avoid nested calls to window.setTimeout(), which are not very reliable anyway as you are waiting for a pre-set amount of time instead of waiting for the element to become visible. An example would be as follows:
// Step 1: Open and wait to finish loading
page.open('http://localhost/');
phantom.waitFor(function() {return !page.loading;});
// Step 2: Click on first panel and wait for it to show
page.evaluate(function() { $("#activate-panel1").click(); });
phantom.waitFor(function() {
return page.evaluate(function() {return $("#panel1").is(":visible");})
});
// Step 3: Click on second panel and wait for it to show
page.evaluate(function() { $("#activate-panel2").click(); });
phantom.waitFor(function() {
return page.evaluate(function() {return $("#panel2").is(":visible");})
});
console.log('READY!');
phantom.exit();
This will load each panel in succession (ie synchronously) while keeping your code simple and avoiding nested callbacks.
Hope it makes sense. You could also use CasperJS as an alternative, its aimed at making this stuff simpler.
Yes, your new code will be called from inside of the setTimeout callback. You can nest the code directly or write a function which capsules the code for you and call that function inside setTimeout.
function anotherClick(){
// something
}
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl);
page.render('websiteAfterClick.png');
anotherClick();
phantom.exit();
}, 3000);
There is another way. You can also write it completely with multiple setTimeout, but then you cannot react to sudden conditions in previous calls.
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl);
page.render('websiteAfterClick.png');
}, 3000);
window.setTimeout(function () {
// some more actions
}, 6000); // you cannot know if this delay is sufficient
window.setTimeout(function () {
phantom.exit();
}, 9000); // you cannot know if this delay is sufficient
I would suggest using CasperJS, if you want to do many actions/navigation steps.
I need to test if specific methods are called when user scrolls the window to a certain point. In my source code I have windows listener attached, something like:
$(window).on("scroll.singleJob",function(e)
{
// code here, check if the window is scrolled past certain point etc. and then I need to call this method
LozengesPanel.makeFixed();
}
Now, in my Jasmine test I'm trying to confirm that the method is being called when the window is scrolled to a certain point. So I set up the test:
describe("SingleJob page", function() {
beforeEach(function() {
loadFixtures('my_fixture.html');
});
it("panel sticks to top when page scrolled down", function() {
spyOn(mycompany.singleJobTestable.LozengesPanel, "makeFixed");
window.scroll(0,1000);
expect(mycompany.singleJobTestable.LozengesPanel.makeFixed).toHaveBeenCalled();
});
});
But the test fails, all I get is Expected spy makeFixed to have been called.
How can I trigger window scroll so I can test methods inside of this callback?
EDIT:
Finally it all makes sense.. It seems that scroll event was put in a tasks queue only to be executed after the current thread finishes. Adding $(window).trigger("scroll"); did the trick. I posted short blog post about it that explains the issue http://spirytoos.blogspot.com.au/2014/02/testing-windowscroll-with-qunitjasmine.html
EDIT: This answer does not satisfy the question. See the comments for the reason.
Actually, it looks like you are triggering the scroll event from your Jasmine spec. I tried very similar code, which I include below. However, my expect still fails, like yours (I'm still getting familiar with Jasmine, so I can't explain with certainty why that is).
var fun = {
scrollEventCallback: function() {
console.log('scroll event triggered');
}
};
$(window).on('scroll', fun.scrollEventCallback);
describe("A test suite", function() {
it("should trigger f", function() {
spyOn(fun, "scrollEventCallback");
$(window).trigger('scroll'); // my callback function is executed because it logs to the console
expect(fun.scrollEventCallback).toHaveBeenCalled(); // this fails anyway
});
});
Maybe your window.scroll(0, 1000) is not actually pushing the viewport low enough to trigger your Lozenges.makeFixed() call. That would be the case if the page (your fixture, I think) wasn't long and it didn't actually have anywhere to scroll.
Also, I got code similar to your provided code to work. The expect(...) succeeds. It is pasted below.
var LozengesPanel = {
makeFixed: function() {
console.log('makeFixed was called with its original function definition');
}
};
$(window).on("scroll.singleJob",function(e) {
LozengesPanel.makeFixed();
});
describe("A test suite", function() {
it("should trigger callback", function() {
spyOn(LozengesPanel, "makeFixed");
$(window).trigger('scroll'); // nothing is logged to the console
expect(LozengesPanel.makeFixed).toHaveBeenCalled(); // succeeds
});
});