Electron | onbeforeunload prevents reloading - electron

I'm trying to develop an app with a separate settings window (in electron). When the settings window is open and the main window is closed, I want to prevent the closing of the main window and focus the settings window.
Right now I do this as following:
window.onbeforeunload = e => {
console.log(e);
if(settingsWindow) {
settingsWindow.focus();
e.returnValue = false;
}
else e.returnValue = true;
};
This although prevents reloading the window, which I don't want.
So I'm asking for a different preventing method or a way to detect if it is a reload or a close.
Greeting Elias =)
EDIT/Solution:
utilize ipcMain and ipcRenderer to handle everything with creating windows in the main process. There you can catch and prevent the close event.
MAIN.JS
const {ipcMain} = require("electron");
...
ipcMain.addListener("ASYNC_MSG", (event, arg) => {
switch(arg) {
case "OPEN_SETTINGS": createSettingsWindow(); break;
}
});
...
mainWindow.addListener("close", e => {
if(settingsWindow) {
e.preventDefault();
e.returnValue = false;
settingsWindow.focus();
}
else e.returnValue = true;
});
RENDERER.JS
const {ipcRenderer} = require("electron");
...
ipcRenderer.send("ASYNC_MSG", "OPEN_SETTINGS");

You can use 'close' event, which is called before onbeforeunload and doesn't collide with reloading
const { app, BrowserWindow } = require('electron')
app.once('ready', () => {
let main = new BrowserWindow({title: 'main'})
let settings = new BrowserWindow({title: 'settings'})
main.on('close', (event) => {
if (settings) {
event.preventDefault()
event.returnValue = false
settings.focus()
} else {
event.returnValue = true
}
})
settings.on('closed', () => {
settings = null
})
})

Soo after some more trial and error I found out that this works:
window.onbeforeunload = e => {
console.log(e);
if(settingsWindow) {
settingsWindow.focus();
e.returnValue = false;
}
- else e.returnValue = true;
+ else window.destroy();
};
Glad I finally found a solution :)

Related

contextmenu event not working in webview [Electron]

I'm using Electron with React+Typescript but this can be tested using just Electron+Javascript. The dom-ready event is working, I can see it in the console, but the contextmenu event is not triggered when I right click.
export default Main() {
const browserRef = useRef<WebViewProps>(null);
useEffect(() => {
const domReady = () => {
console.log("Dom is ready!");
};
const contextMenu = () => {
console.log("Context menu is working!");
};
if(browserRef.current) {
browserRef.current.addEventListener("dom-ready", domReady);
browserRef.current.addEventListener("contextmenu", contextMenu);
};
return () => {
if (browserRef.current) {
browserRef.current?.removeEventListener("dom-ready", domReady);
browserRef.current?.removeEventListener("contextmenu", contextMenu);
};
};
}, []);
return (
<webview ref={browserRef} src="https://www.google.com/" />
);
};
My contextmenu is not being called when I right click, this makes me think it doesn't fire when I right click but i don't know...

Electron show devtools on new window when using setWindowOpenHandler

With the new electron, event 'new-window' is deprecated and replaced with BrowserContent.setWindowOpenHandler.
How to setup various events, including dom-ready for opening devtools?
app.on('ready', () => {
mainWindow = createMainWindow();
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// Set event listener on to-be-created WebContent
// { ..., overrideBrowserWindowOptions: { events, enableDevTools: true, etc } }
return { action: 'allow' };
});
});
You'd need to additionally intercept the did-create-window event which fires when the window is created:
mainWindow.webContents.on("did-create-window", (window, details) => {
window.webContents.once("dom-ready", () => window.webContents.openDevTools());
});

Puppeteer in Electron: Error: Passed function is not well-serializable

Trying to come up with a GUI for Puppeteer project.
I thought about using Electron, but run into error:
Error: Passed function is not well-serializable!
when running Puppeteer functions like:
await page.waitForSelector('.modal', { visible: true });
I found a proper way to serialize when dealing with page.evaluate() but how to proceed in case of page.waitForSelector()?
Is there a work around for Puppeter's API functions to be properly serialized when required?
EDIT
I decided to rewrite
await page.waitForSelector('.modal', { visible: true });
using page.evaluate, here is the code:
// first recreate waitForSelector
const awaitSelector = async (selector) => {
return await new Promise(resolve => {
const selectorInterval = setInterval(() => {
if ($(selector).is(':visible')) {
console.log(`${selector} visible`);
resolve();
clearInterval(selectorInterval);
};
}, 1000);
});
}
and later call that function using page.evaluate():
// remember to pass over selector's name, in this case it is ".modal"
await page.evaluate('(' + awaitSelector.toString() + ')(".modal");');
Firstly context:
Generally you can not run puppeteer from browser environment, it works solely in nodejs. Electron provides 2 processes renderer and main. Whenever you want to use node you have to do it in main one.
About communication between both procesess you can read in docs, there are many ways of handling it. From what I know the best practice is to declare it in preload and use ipc bridge. Other solutions violate contextIsolation rule.
I was w wandering aound from one problem to another: like not serializable function, require not defined and many others.
Finally I rewrote everything from scratch and it works here's my solution:
main.js
const { app, BrowserWindow } = require('electron')
const path = require('path')
const { ipcMain } = require('electron');
const puppeteer = require("puppeteer");
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: true,
},
})
ipcMain.handle('ping', async () => {
await checkPup()
})
async function checkPup() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
page
.waitForSelector('h1', { visible: true })
.then(() => {
console.log('got it')
});
const [button] = await page.$x("//button[contains(., 'Create account')]");
if (button) {
console.log('button: ', button)
await button.click();
await page.screenshot({ path: 'tinder.png' });
const [button] = await page.$x("//button[contains(., 'Create account')]");
if (button) {
console.log('button: ', button)
await button.click();
await page.screenshot({ path: 'tinder.png' });
}
}
await browser.close();
}
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// Attach listener in the main process with the given ID
ipcMain.on('request-mainprocess-action', (event, arg) => {
// Displays the object sent from the renderer process:
//{
// message: "Hi",
// someData: "Let's go"
//}
console.log(
arg
);
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
preload.js
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
const { contextBridge, ipcRenderer } = require('electron')
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping'),
// we can also expose variables, not just functions
})
renderer.js
const information = document.getElementById('info')
const btn = document.getElementById('btn')
information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`
btn.addEventListener('click', () => {
console.log('habad!!!!!')
func()
})
const func = async () => {
const response = await window.versions.ping()
information.innerText = response;
console.log(response) // prints out 'pong'
}
Sorry for a little bit of a mess I hope it will help someone maybe finding solutions to some other problems

Does Electron have MDI similar to WinForm MDI

I've been trying to search this up, but I can't find the answer.
In WinForms you can make an MDI to create forms inside of, so that the MDI child windows cannot go outside of the MDI parent window. Does Electron have this feature or something similar?
Nope because a Browser window cant contain another browser window ... unless you using a movable div with an iframe element... You have 2 options one create a modal window with your content like I've done here
or you can open a new remote window sub in your main JS entry file
const electron = require('electron');
const url = require('url');
const path = require('path');
const {app,BrowserWindow,Menu,ipcMain}= electron;
//SET env
//process.env.NODE_ENV = 'production';
let mainWindow;
let addWindow;
//listen for the app to be ready
app.on('ready',function(){
//creat the new mainWindow
mainWindow = new BrowserWindow({
width:1200,
height:1000,
frame:false,
webPreferences: {
plugins: true,
nodeIntegration: true
//contextIsolation: true
}
});
mainWindow.loadURL(url.format({
pathname: path.join(__dirname,'index.html'),
protocol:'file',
slashes: true
}));
//close all winsows on close
mainWindow.on('closed', function(){
app.quit();
});
//build menu from mainMenuTemplate
const mainMenu = Menu.buildFromTemplate(mainMenuTemplate);
Menu.setApplicationMenu(mainMenu);
});
//handle add new sub window
function createAddWindow()
{
//creat the new mainWindow
addWindow = new BrowserWindow({
width:200,
height:200,
title:'add shoping list item'
});
//load html into window
addWindow.loadURL(url.format({
pathname: path.join(__dirname,'addWindow.html'),
protocol:'file',
slashes: true
}));
//garbage collection Handle
addWindow.on('close',function(){
addWindow= null;
});
}
//Catch Item:add from the subwindown and send to the main html
ipcMain.on('item:add' , function(e, item){
console.log(item);
mainWindow.webContents.send('item:add' , item);
addWindow.close();
});
//var remote = require('remote');
// var BrowserWindow = remote.require('mainWindow');
// function init() {
// Mainwindow.getElementById("min-btn").addEventListener("click", function (e) {
// var lMainwindow = Mainwindow.getFocusedWindow();
// lMainwindow.minimize();
// });
//
// Mainwindow.getElementById("max-btn").addEventListener("click", function (e) {
// var lMainwindow = Mainwindow.getFocusedWindow();
// lMainwindow.maximize();
// });
//
// Mainwindow.getElementById("Close-btn").addEventListener("click", function (e) {
// var lMainwindow = Mainwindow.getFocusedWindow();
// lMainwindow.close();
// });
// };
//
//
// init();
//Creat the menu template
const mainMenuTemplate =[
{
label:'File',
submenu:[
{label:'Add Item',
click(){
createAddWindow();
}
},
{
label:'Clear items',
click(){
mainWindow.webContents.send('item:clear');
console.log('clear click');
}
},
{
label:'Quit',
accelerator:process.platform=='darwin'? 'Command+Q' :'Ctrl+Q',
click(){
app.quit();
}
}
]
},
];
//disable Nasty security warnings
delete process.env.ELECTRON_ENABLE_SECURITY_WARNINGS;
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
//if mac add empty object to the menu to get of the title electron
if(process.platform == 'darwin'){
mainMenuTemplate.unshift({});
}
//add dev tools if not in production
if (process.env.NODE_ENV !== 'production')
{
mainMenuTemplate.push({
label:'Developer Tools',
submenu :[
{
label:'toggle Dev Tools',
accelerator:process.platform=='darwin'? 'Command+I' :'Ctrl+I',
click(item , focusedWindow){
focusedWindow.toggleDevTools();
}
},{
role:'reload',
}
]
})
}
here is the electron reference to a new window object Window Object Reference
electron basically gives you a windows app menu with the chrome browser window... Nothing else... everything inside the window you have to code with your web technologies like HTML CSS and JS
If you find something that wont work in a web page it wont work in electron

electron auto-update on click

I'm trying to make my electron app auto-updatable, and after searching on google I found nice guide that works. The only problem is that I want to create a button for when update is downloaded so that the user can decide when he wants to update and restart the app. So far I was able to put this code together
renderer.js
const electron = require('electron')
const ipcRenderer = electron.ipcRenderer
let lastMsgId = 0
window.quitAndInstall = function () {
electron.remote.autoUpdater.quitAndInstall()
}
ipcRenderer.on('console', (event, consoleMsg) => {
console.log(consoleMsg)
})
ipcRenderer.on('message', (event, data) => {
showMessage(data.msg, data.hide, data.replaceAll)
})
function showMessage(message, hide = true, replaceAll = false) {
const messagesContainer = document.querySelector('.messages-container')
const msgId = lastMsgId++ + 1
const msgTemplate = `<div id="${msgId}" class="alert alert-info alert-info-message animated fadeIn">${message}</div>`
if (replaceAll) {
messagesContainer.innerHTML = msgTemplate
} else {
messagesContainer.insertAdjacentHTML('afterbegin', msgTemplate)
}
if (hide) {
setTimeout(() => {
const msgEl = document.getElementById(msgId)
msgEl.classList.remove('fadeIn')
msgEl.classList.add('fadeOut')
}, 4000)
}
}
and this is my index.js where messages are storred
const electron = require('electron');
const {autoUpdater} = require('electron-updater');
const log = require('electron-log');
const appVersion = require('./package.json').version
// configure logging
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
log.info('App starting...');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1020,
height: 800,
});
mainWindow.loadURL('file://' +__dirname + '/public/index.html');
// Open the DevTools.
//mainWindow.webContents.openDevTools();
mainWindow.on('closed', function() {
mainWindow = null;
});
}
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', function() {
app.quit();
});
app.on('activate', function() {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
//-------------------------------------------------------------------
// Auto updates
//-------------------------------------------------------------------
const sendStatusToWindow = (text) => {
log.info(text);
if (mainWindow) {
mainWindow.webContents.send('console', `App version: ${appVersion}`)
mainWindow.webContents.send('message', { msg: `App version: ${appVersion}` })
}
};
autoUpdater.on('error', (ev, err) => {
mainWindow.webContents.send('message', { msg: `Error: ${err}` })
})
autoUpdater.once('checking-for-update', (ev, err) => {
mainWindow.webContents.send('message', { msg: 'Checking for updates' })
})
autoUpdater.once('update-available', (ev, err) => {
mainWindow.webContents.send('message', { msg: 'Update available. Downloading ⌛️', hide: false })
})
autoUpdater.once('update-not-available', (ev, err) => {
mainWindow.webContents.send('message', { msg: 'Update not available' })
})
autoUpdater.once('update-downloaded', (ev, err) => {
const msg = 'Update downloaded - <button onclick="quitAndInstall()">Restart</button>'
mainWindow.webContents.send('message', { msg, hide: false, replaceAll: true })
})
autoUpdater.checkForUpdates()
As you can see I added a button to call for function but it doesnt work. When I press the button nothing happens. If I remove button and just say auto.updater.quitAndInstall() it works. It auto close window and install new version. What am I missing?
I think it's that electron.remote.autoUpdater.quitAndInstall() doesn't work when run in the renderer.
In the docs it doesn't say anything against running it in the renderer process but I think sending a message back to the main process to run the quitAndInstall function would work.
Inside of the quitAndInstall function put this instead:
ipcRenderer.send('asynchronous-message', 'quitAndInstall');
Then in the main process put:
electron.ipcMain.on('asynchronous-message', function (evt, message) {
if (message == 'quitAndInstall') {
autoUpdater.quitAndInstall();
}
});

Resources