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 <jkleinsc@github.com>

* chore: refactor

Co-authored-by: Jeremy Rose <nornagon@nornagon.net>

* chore: changed property name from `closeWithOpener` to `outlivesOpener`

* dummy change to kick lint

* undo above

Co-authored-by: John Kleinschmidt <jkleinsc@github.com>
Co-authored-by: Jeremy Rose <nornagon@nornagon.net>
This commit is contained in:
t57ser 2022-02-23 08:59:50 +01:00 committed by GitHub
parent bcf060fab6
commit 41b2945ced
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 21 deletions

View file

@ -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 through the feature string, as the renderer has more limited privileges in
deciding security preferences than the main process. 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 ### Native `Window` example
```javascript ```javascript

View file

@ -492,41 +492,51 @@ WebContents.prototype.loadURL = function (url, options) {
return p; 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; 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) { if (!this._windowOpenHandler) {
return null; return defaultResponse;
} }
const response = this._windowOpenHandler(details); const response = this._windowOpenHandler(details);
if (typeof response !== 'object') { if (typeof response !== 'object') {
event.preventDefault(); event.preventDefault();
console.error(`The window open handler response must be an object, but was instead of type '${typeof response}'.`); 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) { if (response === null) {
event.preventDefault(); event.preventDefault();
console.error('The window open handler response must be an object, but was instead null.'); console.error('The window open handler response must be an object, but was instead null.');
return null; return defaultResponse;
} }
if (response.action === 'deny') { if (response.action === 'deny') {
event.preventDefault(); event.preventDefault();
return null; return defaultResponse;
} else if (response.action === 'allow') { } else if (response.action === 'allow') {
if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) { if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) {
return response.overrideBrowserWindowOptions; return {
browserWindowConstructorOptions: response.overrideBrowserWindowOptions,
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
};
} else { } else {
return {}; return {
browserWindowConstructorOptions: {},
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false
};
} }
} else { } else {
event.preventDefault(); event.preventDefault();
console.error('The window open handler response must be an object with an \'action\' property of \'allow\' or \'deny\'.'); 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, postBody,
disposition disposition
}; };
const options = this._callWindowOpenHandler(event, details); const result = this._callWindowOpenHandler(event, details);
const options = result.browserWindowConstructorOptions;
if (!event.defaultPrevented) { if (!event.defaultPrevented) {
openGuestWindow({ openGuestWindow({
event, event,
@ -660,12 +671,14 @@ WebContents.prototype._init = function () {
referrer, referrer,
postData, postData,
overrideBrowserWindowOptions: options || {}, overrideBrowserWindowOptions: options || {},
windowOpenArgs: details windowOpenArgs: details,
outlivesOpener: result.outlivesOpener
}); });
} }
}); });
let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null; 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) => { 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 ? { const postBody = postData ? {
data: postData, data: postData,
@ -679,7 +692,9 @@ WebContents.prototype._init = function () {
referrer, referrer,
postBody postBody
}; };
windowOpenOverriddenOptions = this._callWindowOpenHandler(event, details); const result = this._callWindowOpenHandler(event, details);
windowOpenOutlivesOpenerOption = result.outlivesOpener;
windowOpenOverriddenOptions = result.browserWindowConstructorOptions;
if (!event.defaultPrevented) { if (!event.defaultPrevented) {
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? { const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
// Allow setting of backgroundColor as a webPreference even though // 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, _userGesture: boolean, _left: number, _top: number, _width: number, _height: number, url: string, frameName: string,
referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => { referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
const overriddenOptions = windowOpenOverriddenOptions || undefined; const overriddenOptions = windowOpenOverriddenOptions || undefined;
const outlivesOpener = windowOpenOutlivesOpenerOption;
windowOpenOverriddenOptions = null; windowOpenOverriddenOptions = null;
// false is the default
windowOpenOutlivesOpenerOption = false;
if ((disposition !== 'foreground-tab' && disposition !== 'new-window' && if ((disposition !== 'foreground-tab' && disposition !== 'new-window' &&
disposition !== 'background-tab')) { disposition !== 'background-tab')) {
@ -730,7 +748,8 @@ WebContents.prototype._init = function () {
url, url,
frameName, frameName,
features: rawFeatures features: rawFeatures
} },
outlivesOpener
}); });
}); });
} }

View file

@ -29,7 +29,7 @@ const getGuestWindowByFrameName = (name: string) => frameNamesToWindow.get(name)
* user to preventDefault() on the passed event (which ends up calling * user to preventDefault() on the passed event (which ends up calling
* DestroyWebContents). * 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 }, event: { sender: WebContents, defaultPrevented: boolean },
embedder: WebContents, embedder: WebContents,
guest?: WebContents, guest?: WebContents,
@ -38,6 +38,7 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
postData?: PostData, postData?: PostData,
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions, overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
windowOpenArgs: WindowOpenArgs, windowOpenArgs: WindowOpenArgs,
outlivesOpener: boolean,
}): BrowserWindow | undefined { }): BrowserWindow | undefined {
const { url, frameName, features } = windowOpenArgs; const { url, frameName, features } = windowOpenArgs;
const { options: browserWindowOptions } = makeBrowserWindowOptions({ const { options: browserWindowOptions } = makeBrowserWindowOptions({
@ -77,7 +78,7 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
...browserWindowOptions ...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 }); 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 * too is the guest destroyed; this is Electron convention and isn't based in
* browser behavior. * browser behavior.
*/ */
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName }: { const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
embedder: WebContents, embedder: WebContents,
guest: BrowserWindow, guest: BrowserWindow,
frameName: string frameName: string,
outlivesOpener: boolean
}) { }) {
const closedByEmbedder = function () { const closedByEmbedder = function () {
guest.removeListener('closed', closedByUser); guest.removeListener('closed', closedByUser);
@ -101,9 +103,14 @@ const handleWindowLifecycleEvents = function ({ embedder, guest, frameName }: {
}; };
const closedByUser = function () { 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); guest.once('closed', closedByUser);
if (frameName) { if (frameName) {
@ -163,7 +170,8 @@ function emitDeprecatedNewWindowEvent ({ event, embedder, guest, windowOpenArgs,
handleWindowLifecycleEvents({ handleWindowLifecycleEvents({
embedder: event.sender, embedder: event.sender,
guest: newGuest, guest: newGuest,
frameName frameName,
outlivesOpener: false
}); });
} }
return true; return true;

View file

@ -63,7 +63,7 @@ declare namespace Electron {
equal(other: WebContents): boolean; equal(other: WebContents): boolean;
browserWindowOptions: BrowserWindowConstructorOptions; browserWindowOptions: BrowserWindowConstructorOptions;
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null; _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; _setNextChildWebPreferences(prefs: Partial<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): void;
_send(internal: boolean, channel: string, args: any): boolean; _send(internal: boolean, channel: string, args: any): boolean;
_sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean; _sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean;