Confirm beforeunload in Electron - javascript

i know that there are hundreds of questions like: "How can i prevent close event in electron" or something like that.
After implementing a confirmation box (electron message box) in the beforeunload event i was able to close my app and cancel the close event. Since the dev tools are always open, i didn't recognize that it doesn't work while the dev tools are closed...
window.onbeforeunload = e =>
{
// show a message box with "save", "don't save", and "cancel" button
let warning = remote.dialog.showMessageBox(...)
switch(warning)
{
case 0:
console.log("save");
return;
case 1:
console.log("don't save");
return;
case 2:
console.log("cancel");
return false;
// e.returnValue = "false";
// e.returnValue = false;
}
};
So, when the dev tools are opened, i can close the app with saving, without saving and cancel the event.
When the dev tools are closed, the cancel button doesn't work anymore.
Btw.:
window.onbeforeunload = e =>
{
return false;
alert("foo");
};
will cancel the close event and obviously wouldn't show the message (doesn't matter if dev tools are open or closed)
window.onbeforeunload = e =>
{
alert("foo");
return false;
};
will cancel the close event after pressing ok if dev tools are open and will close the app after pressing ok if dev tools are closed
Intentionally i'm using the synchronous api of the message box and while i'm writing this question i figured out that a two windowed app (new remote.BrowserWindow()) will behave exactly like with the dev tools.
Has anyone an idea how i can resolve this problem?
Many thanks in advance

Instead of onbeforeunload prefer working with the event close. From this event, you'll be able to catch the closing event before the whole closure process is completed (event closed). With close, you'll be able to take the control and stop whenever you need the completion of the closure.
This is possible when you create your BrowserWindow, preferably in the main process:
// Create the browser window.
window = new BrowserWindow({});
// Event 'close'
window.on('close', (e) => {
// Do your control here
if (bToStop) {
e.preventDefault();
}
})
// Event 'closed'
window.on('closed', (e) => {
// Fired only if you didn't called e.preventDefault(); above!
})
In addition, be aware that the function e.preventDefault() is spreading in the whole code. If you need to be back to the natural behaviour of Electron, you need to toggle the variable e.defaultPrevented to false.
Actually, it seems e.preventDefault() function is handling the variable e.defaultPrevented to true until any change on it.

Maybe this will help someone with similar needs as i had, i have a react app wrapped in an electron app, the react app is agnostic to electron and can also run in the browser and the requirements i had was to show the default browser prompt, the infamous Leave Site? alert.
In the browser this is easy, for example with react i just do this:
useEffect(() => {
window.onbeforeunload = promptOnProjectLeave ? () => true : undefined;
return () => {
window.onbeforeunload = undefined;
}
}, [promptOnProjectLeave]);
Which will show the default browser Leave Site? prompt, but in electron this will only prevent the window from being closed without any action prompt asking you if you are sure, so my approach was a mix of this post and another post.
This is the solution
mainWindow.webContents.on('will-prevent-unload', (event) => {
const options = {
type: 'question',
buttons: ['Cancel', 'Leave'],
message: 'Leave Site?',
detail: 'Changes that you made may not be saved.',
};
const response = dialog.showMessageBoxSync(null, options)
if (response === 1) event.preventDefault();
});
This will allow me to use window.onbeforeunload in my react code as i would in the browser, in the browser i will get the default browser prompt and in electron i will get a message box :)
This is my first time working with electron so might be some ways to improve this but either way hope this helps someone, i know it would have helped me when i started with this task.

Update:
As I mentioned above in the comments of the accepted answer, the preventDefault was ignored on Windows. To be precise, I had it placed in the callback of a native electron dialog that opened when the user closed the app.
Therefore I have implemented a different approach:
let close: boolean = false
win.on('close', (ev: any) => {
if (close === false) {
ev.preventDefault()
dialog.showMessageBox({
type: 'warning',
buttons: ['Cancel', 'Ok'],
title: 'Do not forget to safe your changes',
cancelId: 0,
defaultId: 1,
noLink: true
}).then((val) => {
if (val.response === 0) {
// Cancel the close process
} else if (win) {
close = true
app.quit()
}
})
}
})

You can simply use 'pagehide' event. It seems to be working fine for electron apps. It works slightly different from 'beforeunload' as it can't prevent closing window/tab, but if you only need to do something before the page is closed(send some async request with navigator.sendBeacon(), etc) then this event might suit your needs.
You can read more info about it here, here and in the docs
Example of usage:
window.addEventListener('pagehide', () => {
window.navigator.sendBeacon(url, data);
}

Related

Capturing window close or refresh not working in reactjs [duplicate]

This question already has answers here:
window.onbeforeunload not displaying the alert box
(2 answers)
Closed 4 months ago.
I hope you are doing well!
I am trying to catch the window close or tab close or refresh event in my project and I tried all possible solutions but haven't succeeded.
I tried using:
useEffect(() => {
return () => {
window.alert("Alert");
};
});
and I tried:
useEffect(() => {
window.onbeforeunload = () => {
window.alert("alert");
};
return () => {
window.onbeforeunload = null;
};
});
which seems to only trigger if I have my window in the background for a while.
and I tried:
window.addEventListener("onbeforeunload", () => {
window.alert("alert");
});
but haven't been able to capture it.
I will use this functionality to send data to a specific API whenever the user closes the window or tab or refreshes (and possibly turns off the PC while on the page if that is event possible). But all these methods weren't working for me.
Is there any other way or is there a reason they aren't working?
Thank you for your time!
You might need to call preventDefault, and I think the event is called beforeunload, try:
window.addEventListener("beforeunload", ev => {
ev.preventDefault()
return (ev.returnValue = "Are you sure you want to close?")
})
When registering event listeners, you should do this with useEffect so you properly remove the listeners.
useEffect(() => {
const onUnload = (e: any) => {
e.preventDefault()
return (e.returnValue = "Are you sure you want to close?")
}
window.addEventListener("beforeunload", onUnload)
return () => window.removeEventListener("beforeunload", onUnload)
}, [])
Some things to know about beforeunload:
It does not call blocking functions such as alert, prompt or confirm. It is evident from a user perspective.
And it is fired only if there has been ANY user interaction with the site. Without ANY interaction (even one click anywhere) event beforeunload won't be fired.
It is impossible to catch a tab/browser close and even if it was, it was not going to be reliable as the user might force close the browser or kill the process, etc.
So the best option I found, I wrapped in a NPM package here: https://www.npmjs.com/package/#garage-panda/use-before-unload
Take a look and let me know if it works for you.
This is how you use it:
const setEnabledBeforeUnload = useBeforeUnload({
initEnable: false, // do you need to be enabled by default
onRefresh: () => {
// the page has been refreshed (the user has clicked Reload)
},
onCancel: () => {
// the user has clicked Cancel
}
});
And the only possible (and the correct way) to know if the user has left your page is to actually catch the event when he lands on your page (e.g. the sessionStorage is empty).

Vite PWA plugin - add to home screen event

I'm trying to add an installable button to my PWA application. Everywhere I find information about beforeinstallprompt event but i cant listen it. I tried this code:
window.addEventListener("beforeinstallprompt", e => {
console.log("???")
e.preventDefault();
// Stash the event so it can be triggered later.
this.deferredPrompt = e;
});
into my component - in created hook and mounted, into my App.vue, into my service worker but every time it has no effect my app does not go inside listener, i tried console.log something and it don't runs.
I used Vite PWA Plugin is there any other way to add this button?
regards.
UPDATE
here is my code:
data: () => ({
deferredPrompt: null,
}),
mounted() {
this.captureEvent();
},
methods: {
captureEvent() {
window.addEventListener("beforeinstallprompt", (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
this.deferredPrompt = e;
});
},
clickCallback() {
// Show the prompt
this.deferredPrompt.prompt();
// Wait for the user to respond to the prompt
this.deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === "accepted") {
// Add analyticcs event
this.$gtag.event("add_to_home_screen");
}
this.deferredPrompt = null;
});
},
},
and it runs only after lighthouse audit... dont work in desktop chrome, firefox, dont work on android, iphone. I'm using https btw.
do you guys have an idea why it might be like this?

Is there a work-around for the Chrome error: "Require user gesture for beforeunload dialogs" during Cypress tests

https://www.chromestatus.com/feature/5082396709879808
Require user gesture for beforeunload dialogs
The beforeunload dialog will only be shown if the frame attempting to
display it has received a user gesture or user interaction (or if any
embedded frame has received such a gesture). (There will be no change
to the dispatch of the beforeunload event, just a change to whether
the dialog is shown.)
Here is the problem we're running into. In our single page app, business rules dictate that we alert the user if they click the browser back button.
Which is what this code does below in our app.js:
componentDidMount = () => {
window.addEventListener('beforeunload', event => {
event.returnValue = `Are you sure you want to leave?`;
});
}
If the user navigates away on any page, the default alert box will pop up.
However in our Cypress tests we we have a beforeEach which goes back to the start of the app's flow before each test. This triggers the beforeunload event since we're leaving the page, but we don't see the alert, rather we get that chrome error:
Require user gesture for beforeunload dialogs
Anyone run into this before or have a clue on a work around?
Only thing I can think off at the moment is to remove the beforeEach but then we will need individual tests for each thing we want to test. Rather than just a few page test files...
As far as I know there's currently no way to interact with a website in a way that would qualify as a "user gesture", because Cypress currently uses programmatic browser APIs which Chrome doesn't consider as genuine user interaction (this will be possible when native events are implemented).
EDIT: re-reading the question, I'm not actually sure what you're after. If you really want to prevent the redirect, even during tests, then the following won't help. If you instead want to assert that the event was properly registered, and is doing what it's supposed to be doing, then see the below.
That being said, you don't need the unload event to be actually prevented (it's something you actually don't even want because then you'd need to manually confirm/cancel the dialog, which isn't possible ATM, although Cypress does that automatically in some cases). The callback is still called, and you can assert on that.
Thus, you can monkey-patch the event handler, cache the return value, and assert on it after the redirect:
// cypress/support/index.js
const beforeUnloadRets = [];
// command used to assert on beforeunload event return values. Callback is
// retried until it doesn't throw, and is invoked with the value
// potentially-registered beforeunload handler return value. If handler was
// registered, but didn't return anything (i.e. doesn't prevent the event),
// the value is `null`. If no handler was registered, value is `undefined`.
Cypress.Commands.add('assertBeforeUnload', ( cb ) => {
cy.wrap(null, { log: false }).should(() => cb(beforeUnloadRets.shift()));
});
beforeEach(() => {
cy.on('window:before:load', ( win ) => {
// monkey-patch `window.addEventListener` in case the `beforeunload` handler
// is registered using this API
// -------------------------------------------------------------------------
const _addEventListener = win.addEventListener;
win.addEventListener = function (eventName, listener, ...rest) {
if ( eventName === 'beforeunload' ) {
const _origListener = listener;
listener = (...args) => {
const ret = _origListener(...args);
beforeUnloadRets.push(ret === undefined ? null : ret);
return ret;
}
}
return _addEventListener.call(this, eventName, listener, ...rest);
};
// monkey-patch `window.onbeforeload` in case it's registered in that way
// -------------------------------------------------------------------------
let _onbeforeunloadHandler;
win.onbeforeunload = ( ev ) => {
if ( _onbeforeunloadHandler ) {
const ret = _onbeforeunloadHandler.call(win, ev);
beforeUnloadRets.push(ret === undefined ? null : ret);
return ret;
}
};
Object.defineProperty(win, 'onbeforeunload', {
set ( handler ) {
_onbeforeunloadHandler = handler;
}
})
});
});
Usage (note, for demonstration purposes, I register the beforeunload events inside the test, but in real scenario that's what your app will do):
describe('test', () => {
it('test', () => {
// page one. Register 1 beforeunload event, and prevent the unload event.
// -------------------------------------------------------------------------
cy.visit('/a');
cy.window().then( window => {
window.addEventListener('beforeunload', () => {
return 'one';
});
});
// redirect to page two. Assert a prevented unload event.
// Register another, but don't prevent unload.
// -------------------------------------------------------------------------
cy.visit('/b');
cy.assertBeforeUnload( ret => {
expect(ret).to.eq('one');
});
cy.window().then( window => {
// register, but don't prevent
window.onbeforeunload = () => {};
});
// page three. Assert a non-prevented unload event. Register none.
// -------------------------------------------------------------------------
cy.visit('/c');
cy.assertBeforeUnload( ret => {
// assert an event fired, but returned nothing (indicated by `null`)
expect(ret).to.eq(null);
});
// page four. Assert no beforeunload event was fired.
// -------------------------------------------------------------------------
cy.visit('/d');
cy.assertBeforeUnload( ret => {
expect(ret).to.eq(undefined);
});
});
});
We were not able to disable the Chrome action for checking if the user has interacted so we came up with a simple work around:
/* istanbul ignore next */
componentDidMount = () => {
if (process.env.NODE_ENV === 'production') {
window.addEventListener('beforeunload', onBrowserBack);
}
}
/* istanbul ignore next */
componentDidUpdate() {
// If another error modal is up, DO NOT trigger the beforeunload alert
if (process.env.NODE_ENV === 'production' && this.props.hasError) {
window.removeEventListener('beforeunload', onBrowserBack);
}
}
Basically the beforeunload alert modal will now only display in the production env, and when we are cypress testing, they will not.

Using the 'before-quit' event in electron (atom-shell)

I have a application which needs to make an API call before it quits (something like logout). As I still need access to some app data (redux store) for the API call so I decided to listen to the 'before-quit' event on app.
I tried the following code:
import {remote} from 'electron';
let loggedout = false;
remote.app.on('before-quit', (event) => {
if (loggedout) return; // if we are logged out just quit.
console.warn('users tries to quit');
// prevent the default which should cancel the quit
event.preventDefault();
// in the place of the setTimout will be an API call
setTimeout(() => {
// if api call was a success
if (true) {
loggedout = true;
remote.app.quit();
} else {
// tell the user log-out was not successfull. retry and quit after second try.
}
}, 1000);
});
The event never seems to fire or preventing shutdown does not work.
When I replace before-quit with browser-window-blur the event does fire and the code seems to work.
For reference I use Electron 1.2.8 (Due to some dependencies I cannot upgrade). I've double checked and before-quit event was already implemented in that version.
Any Ideas why this event does not seem to be fired?
Thanks in advance and happy holidays!
I had the same issue, this was my solution:
In renderer:
const { ipcRenderer } = require('electron')
window._saved = false
window.onbeforeunload = (e) => {
if (!window.saved) {
callSaveAPI(() => {
// success? quit the app
window._saved = true
ipcRenderer.send('app_quit')
window.onbeforeunload = null
})
}
e.returnValue = false
}
In main:
const { ipcMain } = require('electron')
// listen the 'app_quit' event
ipcMain.on('app_quit', (event, info) => {
app.quit()
})
There are 2 problems which prevented the code from working:
Somehow the 'before-quit' event does not fire in the rerender process. (not main.js).
Once I moved the eventlistener into the main-process preventing the default did not stop the windows from closing. This can only be done through adding an window.onbeforeunload function which returns false. Like suggested in this thread.
One caveat is that the return statement of onbeforeunload does not get updated. In my case I first returned false (to prevent the closing of the window). The second time it did not return false but it kept preventing the closing of the window.
I got around that through overriding the window.onbeforeunload with a new function which did not return false.

Disable backspace in Atom-shell

I've been scouring the interwebz and Atom-shell documentation trying to find out how to disable the back() functionality of the backspace key within a browser window.
I would prefer not to have to resort to a javascript onkeydown listener (which works) and rather use something more native and at more of the application level instead of the browser window level.
The only way I have figured out to do this without the onkeydown listener is with a global-shortcut and the ipc events in the Electron api.
First a disclaimer...
Disabling any key with a global shortcut really does disable it GLOBALLY on your computer! PLEASE BE CAREFUL WHEN USING GLOBAL SHORTCUTS!
If you forget to unregister your shortcut, or do not handle it properly, you will find it difficult to fix your mistake without backspace!
That said this is what worked for me...
const { app, ipcMain,
globalShortcut,
BrowserWindow,
} = require('electron');
app.on('ready', () => {
// Create the browser window
let mainWindow = new BrowserWindow({width: 800, height: 600});
// and load the index.html of the app
mainWindow.loadUrl('file://' + __dirname + '/index.html');
// Register a 'Backspace' shortcut listener when focused on window
mainWindow.on('focus', () => {
if (mainWindow.isFocused()) {
globalShortcut.register('Backspace', () => {
// Provide feedback or logging here
// If you leave this section blank, you will get no
// response when you try the shortcut (i.e. Backspace).
console.log('Backspace was pressed!'); //comment-out or delete when ready.
});
});
});
// ** THE IMPORTANT PART **
// Unregister a 'Backspace' shortcut listener when leaving window.
mainWindow.on('blur', () => {
globalShortcut.unregister('Backspace');
console.log('Backspace is unregistered!'); //comment-out or delete when ready.
});
});
Alternatively you could add the shortcut inside an ipc "Toggle" event handler like this...
// In the main process
ipcMain.on('disableKey-toggle', (event, keyToDisable) => {
if (!globalShortcut.isRegistered(keyToDisable){
globalShortcut.register(keyToDisable, () => {
console.log(keyToDisable+' is registered!'); //comment-out or delete when ready.
});
} else {
globalShortcut.unregister(keyToDisable);
console.log(keyToDisable+' is unregistered!'); //comment-out or delete when ready.
}
});
// In the render process send the accelerator of the keyToDisable.
// Here we use the 'Backspace' accelerator.
const { ipcRenderer } = require('electron');
ipcRenderer.send('disableKey-toggle', 'Backspace');

Categories

Resources