From 41b2945ced7f93568fb3771ab6989d490214ab91 Mon Sep 17 00:00:00 2001 From: t57ser Date: Wed, 23 Feb 2022 08:59:50 +0100 Subject: [PATCH] feat: add ability to configure if window should close when opener closes (#31314) * feat: Added ability to configure if window should close when opener closes * fix: check if embedder is destroyed * fix: correctly take over closeWithOpener property * chore: Added documentation * Update docs/api/window-open.md Co-authored-by: John Kleinschmidt * chore: refactor Co-authored-by: Jeremy Rose * chore: changed property name from `closeWithOpener` to `outlivesOpener` * dummy change to kick lint * undo above Co-authored-by: John Kleinschmidt Co-authored-by: Jeremy Rose --- docs/api/window-open.md | 5 ++++ lib/browser/api/web-contents.ts | 45 ++++++++++++++++++++--------- lib/browser/guest-window-manager.ts | 22 +++++++++----- typings/internal-electron.d.ts | 2 +- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/docs/api/window-open.md b/docs/api/window-open.md index a2cb76a2a569e..1925a5d1e7e5d 100644 --- a/docs/api/window-open.md +++ b/docs/api/window-open.md @@ -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 diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts index 5705cec695a07..4e2cd4acbda74 100644 --- a/lib/browser/api/web-contents.ts +++ b/lib/browser/api/web-contents.ts @@ -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; } }; @@ -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, @@ -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, @@ -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 @@ -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')) { @@ -730,7 +748,8 @@ WebContents.prototype._init = function () { url, frameName, features: rawFeatures - } + }, + outlivesOpener }); }); } diff --git a/lib/browser/guest-window-manager.ts b/lib/browser/guest-window-manager.ts index 9815865f72499..baa446ab91dae 100644 --- a/lib/browser/guest-window-manager.ts +++ b/lib/browser/guest-window-manager.ts @@ -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, @@ -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({ @@ -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 }); @@ -90,10 +91,11 @@ 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); @@ -101,9 +103,14 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName }: { }; 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) { @@ -163,7 +170,8 @@ function emitDeprecatedNewWindowEvent ({ event, embedder, guest, windowOpenArgs, handleWindowLifecycleEvents({ embedder: event.sender, guest: newGuest, - frameName + frameName, + outlivesOpener: false }); } return true; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 666775c16a257..a0d645440652f 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -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 & Pick): void; _send(internal: boolean, channel: string, args: any): boolean; _sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean;