├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── playwright.yml ├── .gitignore ├── README.md ├── app ├── index.js └── renderer │ ├── loading.html │ ├── main.html │ ├── main.js │ ├── preload.js │ ├── window.html │ └── window.js ├── index.d.ts ├── index.js ├── lib ├── electron-windows.js └── helper.js ├── package.json ├── playwright.config.js ├── sceenshot.png ├── start.js └── test ├── e2e └── electron-windows.e2e.test.js ├── electron-windows.test.js └── mocha.opts /.eslintignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/node_modules 3 | **/test 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'eslint-config-egg', 5 | // babel-eslint (deprecated) is now @babel/eslint-parser 6 | parser: '@babel/eslint-parser', 7 | parserOptions: { 8 | ecmaVersion: 2018, 9 | requireConfigFile: false, 10 | }, 11 | rules: { 12 | 'valid-jsdoc': 0, 13 | 'no-script-url': 0, 14 | 'no-multi-spaces': 0, 15 | 'default-case': 0, 16 | 'no-case-declarations': 0, 17 | 'one-var-declaration-per-line': 0, 18 | 'no-restricted-syntax': 0, 19 | 'jsdoc/require-param': 0, 20 | 'jsdoc/check-param-names': 0, 21 | 'jsdoc/require-param-description': 0, 22 | 'arrow-parens': 0, 23 | 'prefer-promise-reject-errors': 0, 24 | 'no-control-regex': 0, 25 | 'no-use-before-define': 0, 26 | 'array-callback-return': 0, 27 | 'no-bitwise': 0, 28 | 'no-self-compare': 0, 29 | 'one-var': 0 30 | }, 31 | globals: { 32 | window: true, 33 | document: true, 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | Runner: 12 | timeout-minutes: 10 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ ubuntu-latest ] 18 | node-version: [ 16 ] 19 | steps: 20 | - name: Checkout Git Source 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install dependencies 29 | run: | 30 | npm i npm@6 -g 31 | npm i 32 | 33 | - name: Continuous integration 34 | run: | 35 | npm run lint 36 | npm run test 37 | 38 | - name: Code coverage 39 | uses: codecov/codecov-action@v3.0.0 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 10 10 | strategy: 11 | matrix: 12 | os: [ macos-latest, windows-latest ] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | 21 | - name: Install dependencies 22 | run: npm i 23 | 24 | - name: Install Playwright Browser 25 | run: npx playwright install --with-deps chromium 26 | 27 | - name: Run Playwright tests 28 | run: npm run test:e2e 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .nyc_output 4 | coverage 5 | file.txt 6 | *.gz 7 | *.sw* 8 | *.un~ 9 | 10 | # Playwright 11 | /test-results/ 12 | /playwright-report/ 13 | /blob-report/ 14 | /playwright/.cache/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-windows 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI][CI-image]][CI-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![node version][node-image]][node-url] 7 | [![npm download][download-image]][download-url] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/electron-windows.svg 10 | [npm-url]: https://npmjs.org/package/electron-windows 11 | [CI-image]: https://github.com/electron-modules/electron-windows/actions/workflows/ci.yml/badge.svg 12 | [CI-url]: https://github.com/electron-modules/electron-windows/actions/workflows/ci.yml 13 | [codecov-image]: https://img.shields.io/codecov/c/github/electron-modules/electron-windows.svg?logo=codecov 14 | [codecov-url]: https://codecov.io/gh/electron-modules/electron-windows 15 | [node-image]: https://img.shields.io/badge/node.js-%3E=_8-green.svg 16 | [node-url]: http://nodejs.org/download/ 17 | [download-image]: https://img.shields.io/npm/dm/electron-windows.svg 18 | [download-url]: https://npmjs.org/package/electron-windows 19 | 20 | > Manage multiple windows of Electron gracefully and provides powerful features. 21 | 22 | 23 | 24 | ## Contributors 25 | 26 | |[
xudafeng](https://github.com/xudafeng)
|[
sriting](https://github.com/sriting)
|[
snapre](https://github.com/snapre)
|[
ColaDaddyz](https://github.com/ColaDaddyz)
|[
z0gSh1u](https://github.com/z0gSh1u)
|[
zlyi](https://github.com/zlyi)
| 27 | | :---: | :---: | :---: | :---: | :---: | :---: | 28 | [
moshangqi](https://github.com/moshangqi)
29 | 30 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Fri Apr 25 2025 11:40:35 GMT+0800`. 31 | 32 | 33 | 34 | ## Installment 35 | 36 | ```bash 37 | $ npm i electron-windows --save 38 | ``` 39 | 40 | ## Demo 41 | 42 | ![](./sceenshot.png) 43 | 44 | ## APIs 45 | 46 | ### init 47 | 48 | ```javascript 49 | const WindowManager = require('electron-windows'); 50 | const windowManager = new WindowManager(); 51 | ``` 52 | 53 | ### create 54 | 55 | ```javascript 56 | const { app } = require('electron'); 57 | const winRef = windowManager.create({ 58 | name: 'window1', 59 | loadingView: { 60 | url: '', 61 | }, 62 | browserWindow: { 63 | width: 800, 64 | height: 600, 65 | titleBarStyle: 'hidden', 66 | title: 'demo', 67 | show: false, 68 | webPreferences: { 69 | nodeIntegration: app.isDev, 70 | webSecurity: true, 71 | webviewTag: true, 72 | }, 73 | }, 74 | openDevTools: true, 75 | storageKey: 'storage-filename', // optional. The name of file. Support storage of window state 76 | storagePath: app.getPath('userData'), // optional. The path of file, only used when storageKey is not empty 77 | }); 78 | ``` 79 | 80 | ## TODO 81 | 82 | - [ ] support storage of window configuration 83 | - [ ] clone pointed window 84 | 85 | ## License 86 | 87 | The MIT License (MIT) 88 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = require('url'); 4 | const path = require('path'); 5 | const _ = require('lodash'); 6 | const WindowManager = require('..'); 7 | const { ipcMain, screen, BrowserWindow } = require('electron'); 8 | const storage = require('electron-json-storage-alt'); 9 | 10 | const loadingUrl = url.format({ 11 | pathname: path.join(__dirname, 'renderer', 'loading.html'), 12 | protocol: 'file:', 13 | }); 14 | 15 | const mainUrl = url.format({ 16 | pathname: path.join(__dirname, 'renderer', 'main.html'), 17 | protocol: 'file:', 18 | }); 19 | 20 | const windowUrl = url.format({ 21 | pathname: path.join(__dirname, 'renderer', 'window.html'), 22 | protocol: 'file:', 23 | }); 24 | 25 | const getRandomPostion = () => { 26 | const { workAreaSize } = screen.getPrimaryDisplay(); 27 | const { width: screenWidth, height: screenHeight } = workAreaSize; 28 | const x = parseInt(_.random(screenWidth / 16, screenWidth / 2), 10); 29 | const y = parseInt(_.random(screenHeight / 16, screenHeight / 2), 10); 30 | return { 31 | x, 32 | y, 33 | }; 34 | }; 35 | 36 | class App { 37 | windowManager = new WindowManager({ 38 | onStorageSave: (key, data) => { 39 | storage.set(key, data); 40 | }, 41 | onStorageReadSync(key) { 42 | return storage.getSync(key); 43 | }, 44 | }); 45 | 46 | init() { 47 | const mainWindow = this.windowManager.create({ 48 | name: 'main', 49 | storageKey: 'main-window-storage', 50 | loadingView: { 51 | url: loadingUrl, 52 | }, 53 | browserWindow: { 54 | webPreferences: { 55 | enableRemoteModule: false, 56 | // `preload` uses Node's `fs` module, so `nodeIntegration` should be enabled. 57 | nodeIntegration: true, 58 | webSecurity: true, 59 | webviewTag: true, 60 | preload: path.join(__dirname, 'renderer', 'preload.js'), 61 | }, 62 | }, 63 | openDevTools: false, 64 | }); 65 | mainWindow.loadURL(mainUrl); 66 | this.mainWindow = mainWindow; 67 | this.bindIPC(); 68 | } 69 | 70 | bindIPC() { 71 | ipcMain.on('new-window', () => { 72 | const postion = getRandomPostion(); 73 | const window = this.windowManager.create({ 74 | name: Date.now(), 75 | loadingView: { 76 | url: loadingUrl, 77 | }, 78 | browserWindow: { 79 | x: postion.x, 80 | y: postion.y, 81 | webPreferences: { 82 | nodeIntegration: true, 83 | webSecurity: true, 84 | webviewTag: true, 85 | preload: path.join(__dirname, 'renderer', 'preload.js'), 86 | }, 87 | }, 88 | }); 89 | 90 | window.loadURL(windowUrl); 91 | console.log(this.windowManager); 92 | }); 93 | 94 | ipcMain.on('new-online-window', () => { 95 | const postion = getRandomPostion(); 96 | const window = this.windowManager.create({ 97 | name: Date.now(), 98 | loadingView: { 99 | url: loadingUrl, 100 | }, 101 | browserWindow: { 102 | x: postion.x, 103 | y: postion.y, 104 | webPreferences: { 105 | nodeIntegration: true, 106 | webSecurity: true, 107 | webviewTag: true, 108 | preload: path.join(__dirname, 'renderer', 'preload.js'), 109 | }, 110 | }, 111 | }); 112 | 113 | window.loadURL('https://www.github.com'); 114 | console.log(this.windowManager); 115 | }); 116 | 117 | ipcMain.on('close-window', (_) => { 118 | const window = BrowserWindow.fromWebContents(_.sender); 119 | window.close(); 120 | console.log(this.windowManager); 121 | }); 122 | 123 | ipcMain.on('blur-window', (_) => { 124 | const window = BrowserWindow.fromWebContents(_.sender); 125 | window.blur(); 126 | console.log(window); 127 | }); 128 | 129 | ipcMain.on('open-devtools', (_) => { 130 | const window = BrowserWindow.fromWebContents(_.sender); 131 | window.openDevTools(); 132 | }); 133 | } 134 | } 135 | 136 | module.exports = App; 137 | -------------------------------------------------------------------------------- /app/renderer/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loading 7 | 28 | 29 | 30 |
31 |
32 | loading... 33 |
34 |
35 | 36 | -------------------------------------------------------------------------------- /app/renderer/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | main 7 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /app/renderer/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.querySelector('#new').addEventListener('click', () => { 4 | window._electron_bridge.send('new-window'); 5 | }, false); 6 | 7 | document.querySelector('#new-online').addEventListener('click', () => { 8 | window._electron_bridge.send('new-online-window'); 9 | }, false); 10 | 11 | document.querySelector('#play-game').addEventListener('click', () => { 12 | const className = 'custom-iframe'; 13 | const elem = document.querySelector(`.${className}`); 14 | if (elem) return; 15 | const iframe = document.createElement('iframe'); 16 | iframe.src = 'https://xudafeng.github.io'; 17 | iframe.className = className; 18 | document.body.appendChild(iframe); 19 | }, false); 20 | 21 | document.querySelector('#debug').addEventListener('click', () => { 22 | window._electron_bridge.send('open-devtools'); 23 | }, false); 24 | -------------------------------------------------------------------------------- /app/renderer/preload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { writeFile, readFileSync } = require('fs'); 4 | const { ipcRenderer, desktopCapturer, contextBridge } = require('electron'); 5 | 6 | contextBridge.exposeInMainWorld( 7 | '_electron_bridge', 8 | { 9 | send: (channel, args) => { 10 | ipcRenderer.send(channel, args); 11 | }, 12 | writeFile, 13 | readFileSync, 14 | desktopCapturer, 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /app/renderer/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /app/renderer/window.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.querySelector('#close').addEventListener('click', () => { 4 | window._electron_bridge.send('close-window'); 5 | }, false); 6 | 7 | document.querySelector('#blur').addEventListener('click', () => { 8 | window._electron_bridge.send('blur-window'); 9 | }, false); 10 | 11 | document.querySelector('#write').addEventListener('click', () => { 12 | const data = 'write local file successfully'; 13 | const fileName = 'file.txt'; 14 | window._electron_bridge.writeFile(fileName, data, (err) => { 15 | if (err) { 16 | console.log(err); 17 | } else { 18 | console.log(window._electron_bridge.readFileSync(fileName, 'utf8')); 19 | } 20 | }); 21 | }, false); 22 | 23 | document.querySelector('#debug').addEventListener('click', () => { 24 | window._electron_bridge.send('open-devtools'); 25 | }, false); 26 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type Definitions for electron-windows 3 | * 4 | * https://github.com/electron-modules/electron-windows 5 | */ 6 | import type { BrowserWindow, BrowserWindowConstructorOptions } from 'electron' 7 | 8 | /** 9 | * The options used when creating a new window. 10 | */ 11 | interface ICreateOptions { 12 | /** 13 | * The name of the window. Default is `'anonymous'`. 14 | */ 15 | name?: string 16 | /** 17 | * The loading view of the window. 18 | */ 19 | loadingView?: { url?: string; [key: string]: any } 20 | /** 21 | * The options for the BrowserWindow. Same as Electron's `BrowserWindow` constructor options. 22 | */ 23 | browserWindow?: BrowserWindowConstructorOptions 24 | /** 25 | * Whether to open DevTools. Default is false. 26 | */ 27 | openDevTools?: boolean 28 | /** 29 | * Whether to prevent the origin close event. Default is false. 30 | */ 31 | preventOriginClose?: boolean 32 | /** 33 | * Whether to prevent the origin navigate event. Default is false. 34 | */ 35 | preventOriginNavigate?: boolean 36 | /** 37 | * The storage key for the window state. State management is based on `electron-window-state`. 38 | */ 39 | storageKey?: string 40 | /** 41 | * The storage path for the window state. State management is based on `electron-window-state`. 42 | */ 43 | storagePath?: string 44 | /** 45 | * The global user agent string used for window's `loadURL`. 46 | */ 47 | globalUserAgent?: string 48 | 49 | // Allow other properties 50 | [key: string]: any 51 | } 52 | 53 | /** 54 | * Window enriched with state management features. 55 | */ 56 | interface StatefulWindow extends BrowserWindow { 57 | /** 58 | * The storage key for the window state. State management is based on `electron-window-state`. 59 | */ 60 | storageKey?: string 61 | /** 62 | * The state from storage. State management is based on `electron-window-state`. 63 | */ 64 | stateFromStorage?: { 65 | manage: (win: BrowserWindow) => void 66 | x?: number 67 | y?: number 68 | width?: number 69 | height?: number 70 | [key: string]: any 71 | } 72 | /** 73 | * (PRESERVED) Keeps the name of the window. 74 | */ 75 | _name?: string 76 | /** 77 | * (PRESERVED) Keeps the very original `loadURL` method without UA modification. 78 | */ 79 | _loadURL?: (url: string, options?: Electron.LoadURLOptions) => void // Added based on usage in _setGlobalUserAgent 80 | } 81 | 82 | /** 83 | * A union type of `StatefulWindow` and common `BrowserWindow`. 84 | */ 85 | type Window = StatefulWindow | BrowserWindow 86 | 87 | /** 88 | * Manage multiple windows of Electron gracefully and provides powerful features. 89 | */ 90 | declare class WindowsManager { 91 | constructor(options?: Record) 92 | 93 | /** 94 | * The options used when creating a new window. 95 | */ 96 | _createOptions: ICreateOptions 97 | 98 | /** 99 | * The options used when `WindowsManager` is constructed. 100 | */ 101 | options: Record 102 | 103 | /** 104 | * The windows managed by the `WindowsManager`. 105 | */ 106 | windows: { [id: number]: Window } 107 | 108 | /** 109 | * Creates a new window managed by the `WindowsManager`. 110 | * @param options Configuration options for the new window. 111 | * @returns The created `Window` instance (with added state properties if `storageKey` is provided). 112 | */ 113 | create(options: ICreateOptions): Window 114 | 115 | /** 116 | * Retrieves a managed window by its assigned name. 117 | * @param name The name assigned to the window during creation. 118 | * @returns The found `Window` instance or undefined. 119 | */ 120 | get(name: string): Window | undefined 121 | 122 | /** 123 | * Retrieves a managed window by its ID. 124 | * @param id The ID of the window. 125 | * @returns The found `Window` instance or undefined. 126 | */ 127 | getById(id: number): Window | undefined 128 | 129 | /** 130 | * Retrieves all windows currently managed by this instance. 131 | * @returns An object where keys are window IDs and values are `Window` instances. 132 | */ 133 | getAll(): { [id: number]: Window } 134 | 135 | /** 136 | * Clone a window. 137 | * @todo This is NOT IMPLEMENTED yet. Now it just returns the same window. 138 | * @param window The `Window` instance to clone. 139 | * @returns The cloned `Window` instance. 140 | */ 141 | clone(window: Window): Window 142 | 143 | /** 144 | * The global user agent string. 145 | */ 146 | static GLOBAL_USER_AGENT?: string 147 | 148 | /** 149 | * Sets a global user agent string to be used for all subsequent `loadURL` calls within managed windows. 150 | * @param ua The user agent string. 151 | */ 152 | static setGlobalUserAgent(ua: string): void 153 | } 154 | 155 | export = WindowsManager 156 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/electron-windows'); 4 | -------------------------------------------------------------------------------- /lib/electron-windows.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { BrowserWindow, BrowserView } = require('electron'); 5 | const windowStateKeeper = require('electron-window-state'); 6 | 7 | class WindowsManager { 8 | constructor(options = {}) { 9 | this.options = options; 10 | this.windows = {}; 11 | } 12 | 13 | create(options) { 14 | this._createOptions = options; 15 | let stateFromStorage = {}; 16 | const { 17 | name = 'anonymous', 18 | loadingView = {}, 19 | browserWindow: browserWindowOptions = {}, 20 | openDevTools = false, 21 | preventOriginClose = false, 22 | storageKey = undefined, 23 | storagePath = undefined, 24 | } = options; 25 | if (storageKey) { 26 | const initWindowState = { 27 | defaultWidth: browserWindowOptions.width, 28 | defaultHeight: browserWindowOptions.height, 29 | file: storageKey + '.json', 30 | }; 31 | if (storagePath) { 32 | initWindowState.path = storagePath; 33 | } 34 | stateFromStorage = windowStateKeeper(initWindowState); 35 | } 36 | const window = new BrowserWindow(Object.assign({ 37 | acceptFirstMouse: true, 38 | }, browserWindowOptions, _.pick(stateFromStorage, [ 39 | 'x', 40 | 'y', 41 | 'width', 42 | 'height', 43 | ]))); 44 | this._setGlobalUserAgent(window, options); 45 | window._name = name; 46 | if (loadingView.url) { 47 | this._setLoadingView(window); 48 | } 49 | window.on('close', (event) => { 50 | if (preventOriginClose) { 51 | event.preventDefault(); 52 | return; 53 | } 54 | delete this.windows[window.id]; 55 | }); 56 | window.webContents.on('dom-ready', () => { 57 | if (openDevTools) { 58 | window.openDevTools(); 59 | } 60 | }); 61 | this.windows[window.id] = window; 62 | if (storageKey) { 63 | window.stateFromStorage = stateFromStorage; 64 | window.storageKey = storageKey; 65 | // register listener on BrowserWindow for window resize events, store state after window close 66 | stateFromStorage.manage(window); 67 | } 68 | return window; 69 | } 70 | 71 | _setGlobalUserAgent(window, options) { 72 | const { 73 | globalUserAgent, 74 | } = options; 75 | const ua = globalUserAgent || WindowsManager.GLOBAL_USER_AGENT; 76 | if (!ua) return; 77 | window._loadURL = window.loadURL; 78 | window.loadURL = (url, options = {}) => { 79 | options.userAgent = ua; 80 | window._loadURL(url, options); 81 | }; 82 | } 83 | 84 | _setLoadingView(window) { 85 | const { 86 | loadingView: loadingViewOptions, 87 | preventOriginNavigate = false, 88 | } = this._createOptions; 89 | let _loadingView = new BrowserView(); 90 | 91 | if (window.isDestroyed()) { 92 | return; 93 | } 94 | 95 | const loadLoadingView = () => { 96 | const [ viewWidth, viewHeight ] = window.getSize(); 97 | window.setBrowserView(_loadingView); 98 | _loadingView.setBounds({ 99 | x: 0, 100 | y: 0, 101 | width: viewWidth, 102 | height: viewHeight, 103 | }); 104 | _loadingView.webContents.loadURL(loadingViewOptions.url); 105 | }; 106 | 107 | const onFailure = () => { 108 | if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) { 109 | _loadingView.webContents.destroy(); 110 | } 111 | if (window.isDestroyed()) { 112 | return; 113 | } 114 | 115 | if (window) { 116 | window.removeBrowserView(_loadingView); 117 | } 118 | }; 119 | 120 | loadLoadingView(); 121 | 122 | window.on('resize', _.debounce(() => { 123 | if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) { 124 | if (window.isDestroyed()) { 125 | return; 126 | } 127 | const [ viewWidth, viewHeight ] = window.getSize(); 128 | _loadingView.setBounds({ x: 0, y: 0, width: viewWidth, height: viewHeight }); 129 | } 130 | }, 500)); 131 | 132 | window.webContents.on('will-navigate', (e) => { 133 | if (preventOriginNavigate) { 134 | e.preventDefault(); 135 | return; 136 | } 137 | if (window.isDestroyed()) { 138 | return; 139 | } 140 | if (_loadingView.webContents && !_loadingView.webContents.isDestroyed()) { 141 | window.setBrowserView(_loadingView); 142 | } else { // if loadingView has been destroyed 143 | _loadingView = new BrowserView(); 144 | loadLoadingView(); 145 | } 146 | }); 147 | window.webContents.on('dom-ready', onFailure); 148 | window.webContents.on('crashed', onFailure); 149 | window.webContents.on('unresponsive', onFailure); 150 | window.webContents.on('did-fail-load', onFailure); 151 | } 152 | 153 | get(name) { 154 | return Object.values(this.windows) 155 | .find(item => item._name === name); 156 | } 157 | 158 | getById(id) { 159 | return Object.values(this.windows) 160 | .find(item => item.id === id); 161 | } 162 | 163 | getAll() { 164 | return this.windows; 165 | } 166 | 167 | clone(window) { 168 | return window; 169 | } 170 | } 171 | 172 | WindowsManager.setGlobalUserAgent = (ua) => { 173 | WindowsManager.GLOBAL_USER_AGENT = ua; 174 | }; 175 | 176 | module.exports = WindowsManager; 177 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-windows", 3 | "version": "34.0.0", 4 | "description": "Manage multiple windows of Electron gracefully and provides powerful features.", 5 | "keywords": [ 6 | "electron", 7 | "window", 8 | "windows" 9 | ], 10 | "files": [ 11 | "lib", 12 | "index.d.ts" 13 | ], 14 | "main": "index.js", 15 | "types": "./index.d.ts", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/electron-modules/electron-windows.git" 19 | }, 20 | "dependencies": { 21 | "electron": "34", 22 | "electron-window-state": "^5.0.3", 23 | "lodash": "4" 24 | }, 25 | "devDependencies": { 26 | "@babel/eslint-parser": "^7.27.1", 27 | "@playwright/test": "^1.52.0", 28 | "@types/node": "^22.15.17", 29 | "electron-json-storage-alt": "18", 30 | "eslint": "7", 31 | "eslint-config-egg": "12", 32 | "eslint-plugin-mocha": "~10.0.0", 33 | "git-contributor": "*", 34 | "husky": "*", 35 | "mocha": "*", 36 | "nyc": "*" 37 | }, 38 | "scripts": { 39 | "dev": "electron ./start.js", 40 | "test": "nyc --reporter=lcov --reporter=text mocha", 41 | "test:e2e": "npx playwright test", 42 | "lint": "eslint . --fix", 43 | "contributor": "git-contributor" 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "npm run lint" 48 | } 49 | }, 50 | "license": "MIT" 51 | } -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { defineConfig, devices } = require('@playwright/test'); 3 | 4 | const isCI = !!process.env.CI; 5 | 6 | module.exports = defineConfig({ 7 | testDir: './test/e2e', 8 | fullyParallel: false, 9 | forbidOnly: isCI, 10 | retries: 0, 11 | workers: 1, 12 | reporter: 'null', 13 | use: { 14 | trace: 'off', 15 | }, 16 | projects: [ 17 | { 18 | name: 'chromium', 19 | use: { ...devices['Desktop Chrome'] }, 20 | }, 21 | ], 22 | }); 23 | -------------------------------------------------------------------------------- /sceenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-modules/electron-windows/a4229c5afb32a8ed3c124fd9592b0f7852063258/sceenshot.png -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app: electronApp } = require('electron'); 4 | 5 | const App = require('./app'); 6 | 7 | electronApp.on('ready', async () => { 8 | const app = new App(); 9 | app.init(); 10 | }); 11 | -------------------------------------------------------------------------------- /test/e2e/electron-windows.e2e.test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect, _electron as electron } from '@playwright/test'; 3 | import { last as _last } from 'lodash'; 4 | import * as path from 'path'; 5 | 6 | const wait = (ms = 2000) => new Promise((resolve) => setTimeout(resolve, ms)); 7 | 8 | /** 9 | * Gets the last element of array. 10 | * @template T 11 | * @param {T[] | null | undefined} arr 12 | * @returns {T} 13 | */ 14 | const last = (arr) => { 15 | const lastElement = _last(arr); 16 | expect(lastElement).not.toBeUndefined(); 17 | // @ts-ignore 18 | return lastElement; 19 | }; 20 | 21 | test.describe('test/e2e/electron-windows.e2e.test.js', () => { 22 | /** @type { import('@playwright/test').ElectronApplication } */ 23 | let electronApp; 24 | 25 | test.beforeEach(async () => { 26 | electronApp = await electron.launch({ args: ['start.js'], cwd: path.join(__dirname, '../..') }); 27 | }); 28 | 29 | test('loadingView option is working', async () => { 30 | const window = await electronApp.firstWindow(); 31 | 32 | expect(await window.title()).toBe('Loading'); 33 | expect(window.url()).toMatch(/renderer\/loading\.html$/); 34 | }); 35 | 36 | test('new window can be correctly created', async () => { 37 | await wait(); 38 | const window = await electronApp.firstWindow(); 39 | await window.click('button#new'); 40 | await wait(); 41 | const windows = electronApp.windows(); 42 | 43 | expect(windows.length).toBeGreaterThan(1); 44 | expect(last(windows).url()).toMatch(/renderer\/window\.html$/); 45 | }); 46 | 47 | test('new online window can be correctly created', async () => { 48 | await wait(); 49 | const window = await electronApp.firstWindow(); 50 | await window.click('button#new-online'); 51 | await wait(); 52 | const windows = electronApp.windows(); 53 | 54 | expect(windows.length).toBeGreaterThan(1); 55 | expect(last(windows).url()).toMatch(/https(.+)?github\.com/); 56 | }); 57 | 58 | test('window can be correctly closed', async () => { 59 | await wait(); 60 | const window = await electronApp.firstWindow(); 61 | await window.click('button#new'); 62 | await wait(); 63 | const newWindow = last(electronApp.windows()); 64 | await newWindow.click('button#close'); 65 | await wait(); 66 | const windows = electronApp.windows(); 67 | 68 | expect(windows).toHaveLength(1); 69 | expect(await windows[0].title()).toBe('main'); 70 | }); 71 | 72 | test.afterEach(async () => { 73 | await electronApp?.close(); 74 | // @ts-ignore 75 | electronApp = null; 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/electron-windows.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const WindowManager = require('..'); 6 | 7 | describe('./test/electron-windows.test.js', () => { 8 | it('should be ok', () => { 9 | assert(WindowManager); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --------------------------------------------------------------------------------