Is it possible to externalize Electron menu template code? - javascript

I have an Electron app with 3 windows and each window has a different menu. The menu template code for each menu is quite long and I would like to externalize it. So far nothing I have tried works.
I've tried different ways to "modularize" it but got lots of errors. The approach below works to set up the menu, but none of the functions referenced in the menu work (e.g. quitApplication).
Is what I am trying to do not possible or am I just "doing it wrong"?
var test = require("./app/js/menuTest.js");
var tm = new test();
var menuTemplate = tm.getMenu();
myWindow = Menu.buildFromTemplate(menuTemplate);
menuTest.js
function testMenu() {
this.getMenu = function () {
var menuTemplate = [
{
label: global.productData.appName,
submenu: [
{ label: 'About ' + global.productData.appName, click: () => { showAboutWindow() } },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ label: 'Quit', click: () => { quitApplication() }, accelerator: 'CmdOrCtrl+q' }
]
// code deleted for clarity
return menuTemplate;
}
}
module.exports = testMenu;

From how I understand your question, you want to move the template code out of your main process script, but keep the functions in there.
This can be achieved by moving the menu structure object into a separate module. The module exports a function that takes an object with references to the functions you want to call in the menu.
I believe this does not add significant complexity and "externalizes" just the menu template code.
menu1.js:
module.exports = function(actions) {
return [
{
label: "Foo",
submenu: [
{ label: "Bar", click: actions.bar },
{ label: "About", click: actions.about }
]
}
];
}
main.js:
const {app,BrowserWindow,Menu} = require("electron");
const actions = {
bar: function () {
console.log("bar");
},
about: function () {
console.log("about");
}
};
const menu1_template = require("./menu1.js")(actions);
const menu1 = Menu.buildFromTemplate(menu1_template);
Menu.setApplicationMenu(menu1);
let mainWindow;
app.on("ready", function() {
mainWindow = new BrowserWindow();
});

Related

How to call a function in another script from main.js in electron

My main.js file in my electron program has a small context menu that is opened when right-clicking the tray icon, like so:
let menuTarea = [
{
label: "Open window",
click: function(){ win.show(); }
},
{
label: "**omitted**",
click: function(){ shell.openExternal("**omitted**"); }
},
{
label: "Close completely",
click: function(){ app.quit(); }
}
]
I would like one of the menu buttons to call a function that is in another script.js file, which is running in the background as it's referenced by the index.html in the main window. How can I do this?
You just have to require the script you want to use in index.html, then call it from main.js either by
executeJavaScript on the page
or using ipc communication
A full example could be:
main.js
const { app, Menu, Tray, BrowserWindow } = require('electron')
const path = require('path')
let tray = null
let win = null
app.on('ready', () => {
win = new BrowserWindow({
show: false
})
win.loadURL(path.join(__dirname, 'index.html'))
tray = new Tray('test.png')
const contextMenu = Menu.buildFromTemplate([
{label: "Open window", click: () => { win.show() }},
{label: "Close completely", click: () => { app.quit() }},
// call required function
{
label: "Call function",
click: () => {
const text = 'asdasdasd'
// #1
win.webContents.send('call-foo', text)
// #2
win.webContents.executeJavaScript(`
foo('${text}')
`)
}
}
])
tray.setContextMenu(contextMenu)
})
index.html
<html>
<body>
<script>
const { foo } = require('./script.js')
const { ipcRenderer } = require('electron')
// For #1
ipcRenderer.on('call-foo', (event, arg) => {
foo(arg)
})
</script>
</body>
</html>
script.js
module.exports = {
foo: (text) => { console.log('foo says', text) }
}

TypeError: x is not a function in Node.js

I'm developing an Electron application and I aim to 'split up' index.js (main process) file. Currently I have put my menu bar-related and Touch Bar-related code into two separate files, menu.js and touchBar.js. Both of these files rely on a function named redir, which is in index.js. Whenever I attempt to activate the click event in my Menu Bar - which relies on redir - I get an error:
TypeError: redir is not a function. This also applies to my Touch Bar code.
Here are my (truncated) files:
index.js
const { app, BrowserWindow } = require('electron'); // eslint-disable-line
const initTB = require('./touchBar.js');
const initMenu = require('./menu.js');
...
let mainWindow; // eslint-disable-line
// Routing + IPC
const redir = (route) => {
if (mainWindow.webContents) {
mainWindow.webContents.send('redir', route);
}
};
module.exports.redir = redir;
function createWindow() {
mainWindow = new BrowserWindow({
height: 600,
width: 800,
title: 'Braindead',
titleBarStyle: 'hiddenInset',
show: false,
resizable: false,
maximizable: false,
});
mainWindow.loadURL(winURL);
initMenu();
mainWindow.setTouchBar(initTB);
...
}
app.on('ready', createWindow);
...
menu.js
const redir = require('./index');
const { app, Menu, shell } = require('electron'); // eslint-disable-line
// Generate template
function getMenuTemplate() {
const template = [
...
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Learn more about x',
click: () => {
shell.openExternal('x'); // these DO work.
},
},
...
],
},
];
if (process.platform === 'darwin') {
template.unshift({
label: 'Braindead',
submenu: [
...
{
label: 'Preferences...',
accelerator: 'Cmd+,',
click: () => {
redir('/preferences'); // this does NOT work
},
}
...
],
});
...
};
return template;
}
// Set the menu
module.exports = function initMenu() {
const menu = Menu.buildFromTemplate(getMenuTemplate());
Menu.setApplicationMenu(menu);
};
My file structure is simple - all three files are in the same directory.
Any code criticisms are also welcome; I've spent hours banging my head trying to figure all this out.
redir it is not a function, because you're exporting an object, containing a redir property, which is a function.
So you should either use:
const { redir } = require('./index.js');
Or export it this way
module.exports = redir
When you do: module.exports.redir = redir;
You're exporting: { redir: [Function] }
You are exporting
module.exports.redir = redir;
That means that your import
const redir = require('./index');
is the exported object. redir happens to be one of its keys. To use the function, use
const redir = require('./index').redir;
or destructure directly into redir
const { redir } = require('./index');

How do I open a markdown file in a BrowserWindow (Electron)?

I am trying to extend an Electron-project.
I have a new BrowserWindow (basicly a new tab in a browser) which should contain the Documention. The Documentation is written in markdown so I´s like to know how to open my markdown file in this BrowserWindow...
Basicly I just need a way to convert a markdownfile on runtime to a webpage.
You'll need the node module fs to open the file and there's a js library called marked - look for that in npm. It renders markdown.
Update - here's a minimal electron app example, tested on electron 0.37.8.
//start - package.json:
{
"name": "mini-md-example",
"version": "0.1.0",
"description": "A small Electron application to open/display a markdown file",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron-prebuilt": "^0.37.7",
"marked": "^0.3.5"
}
}
//:end - package.json
//start - main.js:
const electron = require('electron')
// Module to control application life.
const app = electron.app
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow
const fs = require('fs');
var dialog = require('dialog')
var path = require('path')
var defaultMenu = require('./def-menu-main')
var Menu = require('menu')
const {ipcMain} = electron;
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 999, height: 800})
// and load the index.html of the app.
mainWindow.loadURL(`file://${__dirname}/index.html`)
// Open the DevTools.
mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// 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.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
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()
}
})
var OpenFile = function() {
dialog.showOpenDialog(mainWindow, {
filters: [{name: 'Markdown', extensions: ['md', 'markdown']}],
properties: ['openFile']
}, function(paths) {
if (!paths) return false;
var fPath = paths[0];
var fName = path.basename(fPath);
var fData = fs.readFileSync(fPath, 'utf8');
mainWindow.webContents.send('file-open', fPath, fName, fData);
})
}
var SendEvent = function(name) {
return function() {mainWindow.webContents.send(name);};
};
// Get template for default menu
var menu = defaultMenu()
// Add my very own custom FILE menu
menu.splice(0, 0, {
label: 'File',
submenu: [
{
label: 'Open',
accelerator: "CmdOCtrl+O",
click: OpenFile
},
]
})
// Set top-level application menu, using modified template
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
//:end - main.js
//start - index.html:
<!DOCTYPE html>
<html>
<body>
<div id="content"></div>
<script>
var marked = require('marked')
var ipc = require('electron').ipcRenderer
ipc.on('file-open', function(event, fPath, filename, filedata)
{
document.getElementById('content').innerHTML = marked(filedata) ;
})
</script>
</body>
</html>
//:end - index.html
//start - def-menu-main.js:
var electron = require('electron') // this should work if you're in the electron environment
//var app = electron.remote.app
// original app var calls remote as if this is used in a renderer, but for me menus are a main app thing
var app = electron.app
var shell = electron.shell
module.exports = function() {
var template = [
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.reload();
}
},
{
label: 'Toggle Full Screen',
accelerator: (function() {
if (process.platform === 'darwin')
return 'Ctrl+Command+F';
else
return 'F11';
})(),
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
{
label: 'Toggle Developer Tools',
accelerator: (function() {
if (process.platform === 'darwin')
return 'Alt+Command+I';
else
return 'Ctrl+Shift+I';
})(),
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.toggleDevTools();
}
},
]
},
{
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
},
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Learn More',
click: function() { shell.openExternal('http://electron.atom.io') }
},
]
},
];
if (process.platform === 'darwin') {
var name = app.getName();
template.unshift({
label: name,
submenu: [
{
label: 'About ' + name,
role: 'about'
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
label: 'Hide ' + name,
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
role: 'hideothers'
},
{
label: 'Show All',
role: 'unhide'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: function() { app.quit(); }
},
]
});
var windowMenu = template.find(function(m) { return m.role === 'window' })
if (windowMenu) {
windowMenu.submenu.push(
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
);
}
}
return template;
}
//:end - def-menu-main.js

Mithril: render component on event/dynamically

Trying to get the hang of Mithril, can't really understand one thing. Can I render components on events?
Let's assume I have one parent component:
var MyApp = {
view: function() {
return m("div", [
m.component(MyApp.header, {}),
m("div", {id: "menu-container"})
])
}
};
m.mount(document.body, megogo.main);
It renders the header component (and a placeholder for the menu (do I even need it?)):
MyApp.header = {
view: function() {
return m("div", {
id: 'app-header'
}, [
m('a', {
href: '#',
id: 'menu-button',
onclick: function(){
// this part is just for reference
m.component(MyApp.menu, {})
}
}, 'Menu')
])
}
}
When the user clicks on the menu link I want to load the menu items from my API and only then render the menu.
MyApp.menu = {
controller: function() {
var categories = m.request({method: "GET", url: "https://api.site.com/?params"});
return {categories: categories};
},
view: function(ctrl) {
return m("div", ctrl.categories().data.items.map(function(item) {
return m("a", {
href: "#",
class: 'link-button',
onkeydown: MyApp.menu.keydown
}, item.title)
}));
},
keydown: function(e){
e.preventDefault();
var code = e.keyCode || e.which;
switch(code){
// ...
}
}
};
This part will obviously not work
onclick: function(){
// this part is just for reference
m.component(MyApp.menu, {})
}
So, the question is what is the correct way render components on event?
Try This:
http://jsbin.com/nilesi/3/edit?js,output
You can even toggle the menu.
And remember that you get a promise wrapped in an m.prop from the call to m.request. You'll need to check that it has returned before the menu button can be clicked.
// I'd stick this in a view-model
var showMenu = m.prop(false)
var MyApp = {
view: function(ctrl) {
return m("div", [
m.component(MyApp.header, {}),
showMenu() ? m.component(MyApp.menu) : ''
])
}
};
MyApp.header = {
view: function() {
return m("div", {
id: 'app-header'
}, [
m('a', {
href: '#',
id: 'menu-button',
onclick: function(){
showMenu(!showMenu())
}
}, 'Menu')
])
}
}
MyApp.menu = {
controller: function() {
//var categories = m.request({method: "GET", url: "https://api.site.com/?params"});
var categories = m.prop([{title: 'good'}, {title: 'bad'}, {title: 'ugly'}])
return {categories: categories};
},
view: function(ctrl) {
return m("div.menu", ctrl.categories().map(function(item) {
return m("a", {
href: "#",
class: 'link-button',
onkeydown: MyApp.menu.keydown
}, item.title)
}));
},
keydown: function(e){
e.preventDefault();
var code = e.keyCode || e.which;
switch(code){
// ...
}
}
};
m.mount(document.body, MyApp);
First of all, you'll want to use the return value of m.component, either by returning it from view, or (more likely what you want) put it as a child of another node; use a prop to track whether it's currently open, and set the prop when you wish to open it.
To answer the actual question: by default Mithril will trigger a redraw itself when events like onclick and onkeydown occur, but to trigger a redraw on your own, you'll want to use either m.redraw or m.startComputation / m.endComputation.
The difference between them is that m.redraw will trigger a redraw as soon as it's called, while m.startComputation and m.endComputation will only trigger a redraw once m.endComputation is called the same amount of times that m.startComputation has been called, so that the view isn't redrawn more than once if multiple functions need to trigger a redraw once they've finished.

Add Custom JS and CSS Files to Custom Yeoman Generator

I'm trying to build a custom yeoman generator and I'm basing if off of the webapp generator from yeoman. I'm need to know how to add certain js and css files if bootstrap is not selected. These files are currently being stored at the same level as my index.html file along with main.css.
I've tested the generator and everything works just fine, I just can't figure out how to add certain files or exclude them if bootstrap is present. Any help is greatly appreciated.
Please ask a question before a down vote. This is the first time I've messed with a custom generator.
Below is my index.js file:
'use strict';
var join = require('path').join;
var yeoman = require('yeoman-generator');
var chalk = require('chalk');
module.exports = yeoman.generators.Base.extend({
constructor: function () {
yeoman.generators.Base.apply(this, arguments);
// setup the test-framework property, Gruntfile template will need this
this.option('test-framework', {
desc: 'Test framework to be invoked',
type: String,
defaults: 'mocha'
});
this.testFramework = this.options['test-framework'];
this.option('coffee', {
desc: 'Use CoffeeScript',
type: Boolean,
defaults: false
});
this.coffee = this.options.coffee;
this.pkg = require('../package.json');
},
askFor: function () {
var done = this.async();
// welcome message
if (!this.options['skip-welcome-message']) {
this.log(require('yosay')());
this.log(chalk.magenta(
'Out of the box I include HTML5 Boilerplate, jQuery, and a ' +
'Gruntfile.js to build your app.'
));
}
var prompts = [{
type: 'checkbox',
name: 'features',
message: 'What more would you like?',
choices: [{
name: 'Bootstrap',
value: 'includeBootstrap',
checked: true
},{
name: 'Sass',
value: 'includeSass',
checked: false
},{
name: 'Modernizr',
value: 'includeModernizr',
checked: false
}]
}, {
when: function (answers) {
return answers && answers.feature &&
answers.features.indexOf('includeSass') !== -1;
},
type: 'confirm',
name: 'libsass',
value: 'includeLibSass',
message: 'Would you like to use libsass? Read up more at \n' +
chalk.green('https://github.com/andrew/node-sass#node-sass'),
default: false
}];
this.prompt(prompts, function (answers) {
var features = answers.features;
function hasFeature(feat) {
return features && features.indexOf(feat) !== -1;
}
this.includeSass = hasFeature('includeSass');
this.includeBootstrap = hasFeature('includeBootstrap');
this.includeModernizr = hasFeature('includeModernizr');
this.includeLibSass = answers.libsass;
this.includeRubySass = !answers.libsass;
done();
}.bind(this));
},
gruntfile: function () {
this.template('Gruntfile.js');
},
packageJSON: function () {
this.template('_package.json', 'package.json');
},
git: function () {
this.template('gitignore', '.gitignore');
this.copy('gitattributes', '.gitattributes');
},
bower: function () {
var bower = {
name: this._.slugify(this.appname),
private: true,
dependencies: {}
};
if (this.includeBootstrap) {
var bs = 'bootstrap' + (this.includeSass ? '-sass-official' : '');
bower.dependencies[bs] = "~3.2.0";
} else {
bower.dependencies.jquery = "~1.11.1";
}
if (this.includeModernizr) {
bower.dependencies.modernizr = "~2.8.2";
}
this.write('bower.json', JSON.stringify(bower, null, 2));
},
jshint: function () {
this.copy('jshintrc', '.jshintrc');
},
editorConfig: function () {
this.copy('editorconfig', '.editorconfig');
},
mainStylesheet: function () {
var css = 'main.' + (this.includeSass ? 's' : '') + 'css';
this.template(css, 'app/styles/' + css);
// Add imod-grid.css
var imod = 'imod-grid.' + 'css';
this.template(imod, 'app/styles/' + imod);
},
writeIndex: function () {
this.indexFile = this.engine(
this.readFileAsString(join(this.sourceRoot(), 'index.html')),
this
);
// wire Bootstrap plugins
if (this.includeBootstrap && !this.includeSass) {
var bs = 'bower_components/bootstrap/js/';
this.indexFile = this.appendFiles({
html: this.indexFile,
fileType: 'js',
optimizedPath: 'scripts/plugins.js',
sourceFileList: [
bs + 'affix.js',
bs + 'alert.js',
bs + 'dropdown.js',
bs + 'tooltip.js',
bs + 'modal.js',
bs + 'transition.js',
bs + 'button.js',
bs + 'popover.js',
bs + 'carousel.js',
bs + 'scrollspy.js',
bs + 'collapse.js',
bs + 'tab.js'
],
searchPath: '.'
});
}
this.indexFile = this.appendFiles({
html: this.indexFile,
fileType: 'js',
optimizedPath: 'scripts/main.js',
sourceFileList: ['scripts/main.js'],
searchPath: ['app', '.tmp']
});
},
app: function () {
this.directory('app');
this.mkdir('app/scripts');
this.mkdir('app/styles');
this.mkdir('app/images');
this.write('app/index.html', this.indexFile);
if (this.coffee) {
this.write(
'app/scripts/main.coffee',
'console.log "\'Allo from CoffeeScript!"'
);
}
else {
this.write('app/scripts/main.js', 'console.log(\'\\\'Allo \\\'Allo!\');');
}
},
install: function () {
this.on('end', function () {
this.invoke(this.options['test-framework'], {
options: {
'skip-message': this.options['skip-install-message'],
'skip-install': this.options['skip-install'],
'coffee': this.options.coffee
}
});
if (!this.options['skip-install']) {
this.installDependencies({
skipMessage: this.options['skip-install-message'],
skipInstall: this.options['skip-install']
});
}
});
}
});

Categories