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:
parent
bcf060fab6
commit
41b2945ced
4 changed files with 53 additions and 21 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
2
typings/internal-electron.d.ts
vendored
2
typings/internal-electron.d.ts
vendored
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue