add support for window.open (#59)

This commit is contained in:
Lynn
2017-04-18 09:02:25 -07:00
committed by GitHub
parent 4c7a547f26
commit b2fcf1afc2
8 changed files with 268 additions and 212 deletions

View File

@@ -46,6 +46,12 @@
<button id='snippet'>get snippet</button>
<p>snippet output:</p>
<image id='snippet-img'/>
<hr>
<p>Window activate:</p>
<button id='open-win'>open window</button>
<button id='bring-to-front'>bring open window to front</button>
</body>
<script>
var notfEl = document.getElementById('notf');
@@ -128,5 +134,17 @@
}
});
var win;
var openWinButton = document.getElementById('open-win');
openWinButton.addEventListener('click', function() {
console.log('win=', win)
win = window.open('win.html', 'test-window', 'height=100,width=100');
});
var front = document.getElementById('bring-to-front');
front.addEventListener('click', function() {
window.SYM_API.activate(win.name);
});
</script>
</html>

5
demo/win.html Normal file
View File

@@ -0,0 +1,5 @@
<html>
<head>
</head>
Test Window has been opened
</html>

View File

@@ -4,10 +4,10 @@ var keyMirror = require('keymirror');
const cmds = keyMirror({
isOnline: null,
open: null,
registerLogger: null,
setBadgeCount: null,
badgeDataUrl: null
badgeDataUrl: null,
activate: null
});
module.exports = {

View File

@@ -31,15 +31,14 @@ function isValidWindow(event) {
// validate that event sender is from window we created
const browserWin = electron.BrowserWindow.fromWebContents(event.sender);
const winKey = event.sender.browserWindowOptions &&
event.sender.browserWindowOptions.webPreferences &&
event.sender.browserWindowOptions.webPreferences.winKey;
event.sender.browserWindowOptions.winKey;
result = windowMgr.hasWindow(browserWin, winKey);
}
if (!result) {
/* eslint-disable no-console */
console.log('invalid window try to perform action, ignoring action.');
console.log('invalid window try to perform action, ignoring action');
/* eslint-enable no-console */
}
@@ -75,17 +74,14 @@ electron.ipcMain.on(apiName, (event, arg) => {
return;
}
if (arg.cmd === apiCmds.registerLogger) {
// renderer window that has a registered logger from JS.
log.setLogWindow(event.sender);
if (arg.cmd === apiCmds.activate && typeof arg.windowName === 'string') {
windowMgr.activate(arg.windowName);
return;
}
if (arg.cmd === apiCmds.open && typeof arg.url === 'string') {
let title = arg.title || 'Symphony';
let width = arg.width || 1024;
let height = arg.height || 768;
windowMgr.createChildWindow(arg.url, title, width, height);
if (arg.cmd === apiCmds.registerLogger) {
// renderer window that has a registered logger from JS.
log.setLogWindow(event.sender);
}
});

View File

@@ -1,25 +0,0 @@
'use strict';
// script run before others and still has access to node integration, even
// when turned off - allows us to leak only what want into window object.
// see: http://electron.atom.io/docs/api/browser-window/
//
// to leak some node module into:
// https://medium.com/@leonli/securing-embedded-external-content-in-electron-node-js-8b6ef665cd8e#.fex4e68p7
// https://slack.engineering/building-hybrid-applications-with-electron-dc67686de5fb#.tp6zz1nrk
//
// also to bring pieces of node.js:
// https://github.com/electron/electron/issues/2984
//
//
// API exposed to renderer child window process (aka pop-outs).
//
window.SYM_API = {
// api version
version: '1.0.0'
// currently no funcs are exposed to child windows
};
Object.freeze(window.SYM_API);

View File

@@ -11,7 +11,6 @@
// also to bring pieces of node.js:
// https://github.com/electron/electron/issues/2984
//
const { ipcRenderer, remote } = require('electron');
const throttle = require('../utils/throttle.js');
@@ -21,12 +20,9 @@ const apiName = apiEnums.apiName;
// hold ref so doesn't get GC'ed
const local = {
ipcRenderer: ipcRenderer,
ipcRenderer: ipcRenderer
};
var notify = remote.require('./notify/notifyImpl.js');
var ScreenSnippet = remote.require('./screenSnippet/ScreenSnippet.js');
// throttle calls to this func to at most once per sec, called on leading edge.
const throttledSetBadgeCount = throttle(1000, function(count) {
local.ipcRenderer.send(apiName, {
@@ -35,131 +31,151 @@ const throttledSetBadgeCount = throttle(1000, function(count) {
});
});
//
// API exposed to renderer main window process.
//
window.SYM_API = {
// api version
version: '1.0.0',
createAPI();
openWindow: function(url) {
local.ipcRenderer.send(apiName, {
cmd: apiCmds.open,
url: url
});
},
// creates API exposed from electron.
// wrapped in a function so we can abort early in function coming from an iframe
function createAPI() {
// iframes (and any other non-top level frames) get no api access
// http://stackoverflow.com/questions/326069/how-to-identify-if-a-webpage-is-being-loaded-inside-an-iframe-or-directly-into-t/326076
if (window.self !== window.top) {
return;
}
/**
* sets the count on the tray icon to the given number.
* @param {number} count count to be displayed
* note: count of 0 will remove the displayed count.
* note: for mac the number displayed will be 1 to infinity
* note: for windws the number displayed will be 1 to 99 and 99+
*/
setBadgeCount: function(count) {
throttledSetBadgeCount(count);
},
// note: window.open from main window (if in the same domain) will get
// api access. window.open in another domain will be opened in the default
// browser (see: handler for event 'new-window' in windowMgr.js)
/**
* provides api similar to html5 Notification, see details
* in notify/notifyImpl.js
*/
Notification: notify,
//
// API exposed to renderer process.
//
window.SYM_API = {
// api version
version: '1.0.0',
/**
* provides api to allow user to capture portion of screen, see api
* details in screenSnipper/ScreenSnippet.js
*/
ScreenSnippet: ScreenSnippet,
/**
* sets the count on the tray icon to the given number.
* @param {number} count count to be displayed
* note: count of 0 will remove the displayed count.
* note: for mac the number displayed will be 1 to infinity
* note: for windws the number displayed will be 1 to 99 and 99+
*/
setBadgeCount: function(count) {
throttledSetBadgeCount(count);
},
/**
* allows JS to register a logger that can be used by electron main process.
* @param {Object} logger function that can be called accepting
* object: {
* logLevel: 'ERROR'|'CONFLICT'|'WARN'|'ACTION'|'INFO'|'DEBUG',
* logDetails: String
* }
*
* note: only main window is allowed to register a logger, others are
* ignored.
*/
registerLogger: function(logger) {
if (typeof logger === 'function') {
local.logger = logger;
/**
* provides api similar to html5 Notification, see details
* in notify/notifyImpl.js
*/
Notification: remote.require('./notify/notifyImpl.js'),
// only main window can register
/**
* provides api to allow user to capture portion of screen, see api
* details in screenSnipper/ScreenSnippet.js
*/
ScreenSnippet: remote.require('./screenSnippet/ScreenSnippet.js'),
/**
* Brings window forward and gives focus.
* @param {String} windowName Name of window. Note: main window name is 'main'
*/
activate: function(windowName) {
local.ipcRenderer.send(apiName, {
cmd: apiCmds.registerLogger
cmd: apiCmds.activate,
windowName: windowName
});
},
/**
* allows JS to register a logger that can be used by electron main process.
* @param {Object} logger function that can be called accepting
* object: {
* logLevel: 'ERROR'|'CONFLICT'|'WARN'|'ACTION'|'INFO'|'DEBUG',
* logDetails: String
* }
*
* note: only main window is allowed to register a logger, others are
* ignored.
*/
registerLogger: function(logger) {
if (typeof logger === 'function') {
local.logger = logger;
// only main window can register
local.ipcRenderer.send(apiName, {
cmd: apiCmds.registerLogger
});
}
}
}
};
};
Object.freeze(window.SYM_API);
Object.freeze(window.SYM_API);
// listen for log message from main process
local.ipcRenderer.on('log', (event, arg) => {
if (local.logger && arg && arg.level && arg.details) {
local.logger(arg.level, arg.details);
}
});
/**
* Use render process to create badge count img and send back to main process.
* If number is greater than 99 then 99+ img is returned.
* note: with sandboxing turned on only get arg and no event passed in, so
* need to use ipcRenderer to callback to main process.
* @type {object} arg.count - number: count to be displayed
*/
local.ipcRenderer.on('createBadgeDataUrl', (event, arg) => {
const count = arg && arg.count || 0;
// create 32 x 32 img
let radius = 16;
let canvas = document.createElement('canvas');
canvas.height = radius * 2;
canvas.width = radius * 2;
let ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(radius, radius, radius, 0, 2 * Math.PI, false);
ctx.fill();
ctx.textAlign = 'center';
ctx.fillStyle = 'white';
let text = count > 99 ? '99+' : count.toString();
if (text.length > 2) {
ctx.font = 'bold 18px sans-serif';
ctx.fillText(text, radius, 22);
} else if (text.length > 1) {
ctx.font = 'bold 24px sans-serif';
ctx.fillText(text, radius, 24);
} else {
ctx.font = 'bold 26px sans-serif';
ctx.fillText(text, radius, 26);
}
let dataUrl = canvas.toDataURL('image/png', 1.0);
local.ipcRenderer.send(apiName, {
cmd: apiCmds.badgeDataUrl,
dataUrl: dataUrl,
count: count
// listen for log message from main process
local.ipcRenderer.on('log', (event, arg) => {
if (local.logger && arg && arg.level && arg.details) {
local.logger(arg.level, arg.details);
}
});
});
function updateOnlineStatus() {
local.ipcRenderer.send(apiName, {
cmd: apiCmds.isOnline,
isOnline: window.navigator.onLine
/**
* Use render process to create badge count img and send back to main process.
* If number is greater than 99 then 99+ img is returned.
* note: with sandboxing turned on only get arg and no event passed in, so
* need to use ipcRenderer to callback to main process.
* @type {object} arg.count - number: count to be displayed
*/
local.ipcRenderer.on('createBadgeDataUrl', (event, arg) => {
const count = arg && arg.count || 0;
// create 32 x 32 img
let radius = 16;
let canvas = document.createElement('canvas');
canvas.height = radius * 2;
canvas.width = radius * 2;
let ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(radius, radius, radius, 0, 2 * Math.PI, false);
ctx.fill();
ctx.textAlign = 'center';
ctx.fillStyle = 'white';
let text = count > 99 ? '99+' : count.toString();
if (text.length > 2) {
ctx.font = 'bold 18px sans-serif';
ctx.fillText(text, radius, 22);
} else if (text.length > 1) {
ctx.font = 'bold 24px sans-serif';
ctx.fillText(text, radius, 24);
} else {
ctx.font = 'bold 26px sans-serif';
ctx.fillText(text, radius, 26);
}
let dataUrl = canvas.toDataURL('image/png', 1.0);
local.ipcRenderer.send(apiName, {
cmd: apiCmds.badgeDataUrl,
dataUrl: dataUrl,
count: count
});
});
function updateOnlineStatus() {
local.ipcRenderer.send(apiName, {
cmd: apiCmds.isOnline,
isOnline: window.navigator.onLine
});
}
window.addEventListener('offline', updateOnlineStatus, false);
window.addEventListener('online', updateOnlineStatus, false);
updateOnlineStatus();
}
window.addEventListener('offline', updateOnlineStatus, false);
window.addEventListener('online', updateOnlineStatus, false);
updateOnlineStatus();

View File

@@ -3,6 +3,7 @@
const electron = require('electron');
const app = electron.app;
const path = require('path');
const nodeURL = require('url');
const menuTemplate = require('./menus/menuTemplate.js');
const loadErrors = require('./dialogs/showLoadError.js');
@@ -25,10 +26,8 @@ let windows = {};
let willQuitApp = false;
let isOnline = true;
// different preload script for main window and child windows.
// note: these files are generated by browserify prebuild process.
// note: this file is built using browserify in prebuild step.
const preloadMainScript = path.join(__dirname, 'preload/_preloadMain.js');
const preloadChildScript = path.join(__dirname, 'preload/_preloadChild.js');
function addWindowKey(key, browserWin) {
windows[ key ] = browserWin;
@@ -38,10 +37,17 @@ function removeWindowKey(key) {
delete windows[ key ];
}
function createMainWindow(url) {
function getHostFromUrl(url) {
let parsedUrl = nodeURL.parse(url);
let UrlHost = parsedUrl.host;
return UrlHost
}
function createMainWindow(initialUrl) {
let url = initialUrl;
let key = getGuid();
mainWindow = new electron.BrowserWindow({
let newWinOpts = {
title: 'Symphony',
width: 1024, height: 768,
show: true,
@@ -49,9 +55,15 @@ function createMainWindow(url) {
sandbox: true,
nodeIntegration: false,
preload: preloadMainScript,
winKey: key
}
});
};
// note: augmenting with some custom values
newWinOpts.winKey = key;
mainWindow = new electron.BrowserWindow(newWinOpts);
mainWindow.winName = 'main';
function retry() {
if (!isOnline) {
@@ -67,12 +79,14 @@ function createMainWindow(url) {
// content can be cached and will still finish load but
// we might not have netowrk connectivity, so warn the user.
mainWindow.webContents.on('did-finish-load', function() {
url = mainWindow.webContents.getURL();
if (!isOnline) {
loadErrors.showNetworkConnectivityError(mainWindow, url, retry);
} else {
// removes all existing notifications when main window reloads
notify.reset();
log.send(logLevels.INFO, 'main window loaded');
log.send(logLevels.INFO, 'main window loaded url: ' + url);
}
});
@@ -89,7 +103,7 @@ function createMainWindow(url) {
mainWindow.on('close', function(e) {
if (willQuitApp) {
destroyMainWindow();
destroyAllWindows();
return;
}
// mac should hide window when hitting x close
@@ -99,24 +113,75 @@ function createMainWindow(url) {
}
});
function destroyMainWindow() {
removeWindowKey(key);
if (mainWindow) {
mainWindow.removeAllListeners();
if (!mainWindow.isDestroyed() && mainWindow.webContents) {
mainWindow.webContents.removeAllListeners();
}
mainWindow = null;
function destroyAllWindows() {
var keys = Object.keys(windows);
for(var i = 0, len = keys.length; i < len; i++) {
let winKey = keys[i];
removeWindowKey(winKey);
}
mainWindow = null;
}
mainWindow.on('closed', destroyMainWindow);
mainWindow.on('closed', destroyAllWindows);
// open external links in default browser - window.open
mainWindow.webContents.on('new-window', function(event, newWinUrl) {
event.preventDefault();
electron.shell.openExternal(newWinUrl);
// open external links in default browser - a tag, window.open
mainWindow.webContents.on('new-window', function(event, newWinUrl,
frameName, disposition, newWinOptions) {
let newWinHost = getHostFromUrl(newWinUrl);
let mainWinHost = getHostFromUrl(url);
// if host url doesn't match then open in external browser
if (newWinHost !== mainWinHost) {
event.preventDefault();
electron.shell.openExternal(newWinUrl);
} else if (disposition === 'foreground-tab' ||
disposition === 'new-window') {
// handle: window.open
if (!frameName) {
// abort - no frame name provided.
return;
}
// reposition new window
var mainWinPos = mainWindow.getPosition();
if (mainWinPos && mainWinPos.length === 2) {
let newWinKey = getGuid();
/* eslint-disable no-param-reassign */
newWinOptions.x = mainWinPos[0] + 50;
newWinOptions.y = mainWinPos[1] + 50;
newWinOptions.winKey = newWinKey;
/* eslint-enable no-param-reassign */
// note: will use code below later for saved layout impl.
var webContents = newWinOptions.webContents;
webContents.once('did-finish-load', function() {
var browserWin = electron.BrowserWindow.fromWebContents(webContents);
browserWin.winName = frameName;
addWindowKey(newWinKey, browserWin);
browserWin.once('close', function() {
removeWindowKey(newWinKey);
});
// note: will use later for save-layout feature
// browserWin.on('move', function() {
// var newPos = browserWin.getPosition();
// console.log('new pos=', newPos)
// });
// browserWin.on('resize', function() {
// var newSize = browserWin.getSize();
// console.log('new size=', newSize)
// });
});
}
}
});
contextMenu(mainWindow);
}
@@ -145,46 +210,27 @@ function hasWindow(win, winKey) {
return false;
}
function createChildWindow(url, title, width, height) {
let winKey = getGuid();
let childWindow = new electron.BrowserWindow({
title: title,
width: width,
height: height,
webPreferences: {
sandbox: true,
nodeIntegration: false,
preload: preloadChildScript,
winKey: winKey
}
});
addWindowKey(winKey, childWindow);
childWindow.loadURL(url);
childWindow.on('closed', function() {
removeWindowKey(winKey);
if (childWindow) {
childWindow.removeAllListeners();
if (!childWindow.isDestroyed() && childWindow.webContents) {
childWindow.webContents.removeAllListeners();
}
}
});
contextMenu(childWindow);
}
function setIsOnline(status) {
isOnline = status;
}
function activate(windowName) {
var keys = Object.keys(windows);
for(var i = 0, len = keys.length; i < len; i++) {
var window = windows[keys[i]];
if (window.winName === windowName) {
window.show();
return;
}
}
}
module.exports = {
createMainWindow: createMainWindow,
getMainWindow: getMainWindow,
showMainWindow: showMainWindow,
isMainWindow: isMainWindow,
hasWindow: hasWindow,
createChildWindow: createChildWindow,
setIsOnline: setIsOnline
setIsOnline: setIsOnline,
activate: activate
};

View File

@@ -16,7 +16,7 @@
"prebuild": "npm run lint && npm run test && npm run browserify-preload",
"lint": "eslint --ext .js js/",
"test": "jest --testPathPattern test",
"browserify-preload": "browserify -o js/preload/_preloadMain.js -x electron js/preload/preloadMain.js && browserify -o js/preload/_preloadChild.js -x electron js/preload/preloadChild.js"
"browserify-preload": "browserify -o js/preload/_preloadMain.js -x electron --insert-global-vars=__filename,__dirname js/preload/preloadMain.js"
},
"jest": {
"collectCoverage": true,
@@ -69,7 +69,7 @@
"babel-preset-es2015": "^6.24.0",
"browserify": "^14.1.0",
"cross-env": "^3.2.4",
"electron": "1.6.5",
"electron": "1.6.6",
"electron-builder": "^13.9.0",
"electron-builder-squirrel-windows": "^12.3.0",
"electron-packager": "^8.5.2",