Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added ability to configure if window should close when opener closes #31314

Merged
merged 9 commits into from Feb 23, 2022
5 changes: 5 additions & 0 deletions docs/api/window-open.md
Expand Up @@ -73,6 +73,11 @@ creating the window. Note that this is more powerful than passing options
through the feature string, as the renderer has more limited privileges in
deciding security preferences than the main process.

In addition to passing in `action` and `overrideBrowserWindowOptions`,
`outlivesOpener` can be passed like: `{ action: 'allow', outlivesOpener: true,
overrideBrowserWindowOptions: { ... } }`. If set to `true`, the newly created
window will not close when the opener window closes. The default value is `false`.

### Native `Window` example

```javascript
Expand Down
45 changes: 32 additions & 13 deletions lib/browser/api/web-contents.ts
Expand Up @@ -492,41 +492,51 @@ WebContents.prototype.loadURL = function (url, options) {
return p;
};

WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => ({action: 'allow'} | {action: 'deny', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions})) {
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => ({action: 'deny'} | {action: 'allow', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions, outlivesOpener?: boolean})) {
this._windowOpenHandler = handler;
};

WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event, details: Electron.HandlerDetails): BrowserWindowConstructorOptions | null {
WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event, details: Electron.HandlerDetails): {browserWindowConstructorOptions: BrowserWindowConstructorOptions | null, outlivesOpener: boolean} {
const defaultResponse = {
browserWindowConstructorOptions: null,
outlivesOpener: false
};
if (!this._windowOpenHandler) {
return null;
return defaultResponse;
}
const response = this._windowOpenHandler(details);

if (typeof response !== 'object') {
event.preventDefault();
console.error(`The window open handler response must be an object, but was instead of type '${typeof response}'.`);
return null;
return defaultResponse;
}

if (response === null) {
event.preventDefault();
console.error('The window open handler response must be an object, but was instead null.');
return null;
return defaultResponse;
}

if (response.action === 'deny') {
event.preventDefault();
return null;
return defaultResponse;
} else if (response.action === 'allow') {
if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) {
return response.overrideBrowserWindowOptions;
return {
browserWindowConstructorOptions: response.overrideBrowserWindowOptions,
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
};
} else {
return {};
return {
browserWindowConstructorOptions: {},
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
};
}
} else {
event.preventDefault();
console.error('The window open handler response must be an object with an \'action\' property of \'allow\' or \'deny\'.');
return null;
return defaultResponse;
}
};

Expand Down Expand Up @@ -651,7 +661,8 @@ WebContents.prototype._init = function () {
postBody,
disposition
};
const options = this._callWindowOpenHandler(event, details);
const result = this._callWindowOpenHandler(event, details);
const options = result.browserWindowConstructorOptions;
if (!event.defaultPrevented) {
openGuestWindow({
event,
Expand All @@ -660,12 +671,14 @@ WebContents.prototype._init = function () {
referrer,
postData,
overrideBrowserWindowOptions: options || {},
windowOpenArgs: details
windowOpenArgs: details,
outlivesOpener: result.outlivesOpener
});
}
});

let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
let windowOpenOutlivesOpenerOption: boolean = false;
this.on('-will-add-new-contents' as any, (event: ElectronInternal.Event, url: string, frameName: string, rawFeatures: string, disposition: Electron.HandlerDetails['disposition'], referrer: Electron.Referrer, postData: PostData) => {
const postBody = postData ? {
data: postData,
Expand All @@ -679,7 +692,9 @@ WebContents.prototype._init = function () {
referrer,
postBody
};
windowOpenOverriddenOptions = this._callWindowOpenHandler(event, details);
const result = this._callWindowOpenHandler(event, details);
windowOpenOutlivesOpenerOption = result.outlivesOpener;
windowOpenOverriddenOptions = result.browserWindowConstructorOptions;
if (!event.defaultPrevented) {
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
// Allow setting of backgroundColor as a webPreference even though
Expand Down Expand Up @@ -710,7 +725,10 @@ WebContents.prototype._init = function () {
_userGesture: boolean, _left: number, _top: number, _width: number, _height: number, url: string, frameName: string,
referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
const overriddenOptions = windowOpenOverriddenOptions || undefined;
const outlivesOpener = windowOpenOutlivesOpenerOption;
windowOpenOverriddenOptions = null;
// false is the default
windowOpenOutlivesOpenerOption = false;

if ((disposition !== 'foreground-tab' && disposition !== 'new-window' &&
disposition !== 'background-tab')) {
Expand All @@ -730,7 +748,8 @@ WebContents.prototype._init = function () {
url,
frameName,
features: rawFeatures
}
},
outlivesOpener
});
});
}
Expand Down
22 changes: 15 additions & 7 deletions lib/browser/guest-window-manager.ts
Expand Up @@ -29,7 +29,7 @@ const getGuestWindowByFrameName = (name: string) => frameNamesToWindow.get(name)
* user to preventDefault() on the passed event (which ends up calling
* DestroyWebContents).
*/
export function openGuestWindow ({ event, embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs }: {
export function openGuestWindow ({ event, embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
event: { sender: WebContents, defaultPrevented: boolean },
embedder: WebContents,
guest?: WebContents,
Expand All @@ -38,6 +38,7 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
postData?: PostData,
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
windowOpenArgs: WindowOpenArgs,
outlivesOpener: boolean,
}): BrowserWindow | undefined {
const { url, frameName, features } = windowOpenArgs;
const { options: browserWindowOptions } = makeBrowserWindowOptions({
Expand Down Expand Up @@ -77,7 +78,7 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
...browserWindowOptions
});

handleWindowLifecycleEvents({ embedder, frameName, guest: window });
handleWindowLifecycleEvents({ embedder, frameName, guest: window, outlivesOpener });

embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });

Expand All @@ -90,20 +91,26 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
* too is the guest destroyed; this is Electron convention and isn't based in
* browser behavior.
*/
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName }: {
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
embedder: WebContents,
guest: BrowserWindow,
frameName: string
frameName: string,
outlivesOpener: boolean
}) {
const closedByEmbedder = function () {
guest.removeListener('closed', closedByUser);
guest.destroy();
};

const closedByUser = function () {
embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
// Embedder might have been closed
if (!embedder.isDestroyed() && !outlivesOpener) {
embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
}
};
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
if (!outlivesOpener) {
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
}
guest.once('closed', closedByUser);

if (frameName) {
Expand Down Expand Up @@ -163,7 +170,8 @@ function emitDeprecatedNewWindowEvent ({ event, embedder, guest, windowOpenArgs,
handleWindowLifecycleEvents({
embedder: event.sender,
guest: newGuest,
frameName
frameName,
outlivesOpener: false
});
}
return true;
Expand Down
2 changes: 1 addition & 1 deletion typings/internal-electron.d.ts
Expand Up @@ -63,7 +63,7 @@ declare namespace Electron {
equal(other: WebContents): boolean;
browserWindowOptions: BrowserWindowConstructorOptions;
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null;
_callWindowOpenHandler(event: any, details: Electron.HandlerDetails): Electron.BrowserWindowConstructorOptions | null;
_callWindowOpenHandler(event: any, details: Electron.HandlerDetails): {browserWindowConstructorOptions: Electron.BrowserWindowConstructorOptions | null, outlivesOpener: boolean};
_setNextChildWebPreferences(prefs: Partial<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): void;
_send(internal: boolean, channel: string, args: any): boolean;
_sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean;
Expand Down