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');
Related
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?
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.
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);
}
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.
Let's say I want to have a key event only on my compiled application with atom shell.
var app = require('app');
var BrowserWindow = require('browser-window');
require('crash-reporter').start();
app.on('ready', function() {
win = new BrowserWindow({ fullscreen: true, frame: false });
win.hide();
win.loadUrl("http://localhost:3000");
win.webContents.on('did-finish-load', function() {
win.show();
win.focus();
});
process.on('uncaughtException', app.quit);
});
How could I bind a keyboard event on the web browser? Eg,
win.on('keypress', 'left-arrow', function() {
win.webContents.goBack();
});
Also, apparently left arrow fires on key down, not key press. Credit: Detecting arrow key presses in JavaScript
I'm just learning about atom-shell, but couldn't you catch the keypress inside your UI (I think it's called the rendererer process), like you would in a typical web page, then use the remote() API to call back to the renderer process and do whatever logic you wanted?