├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist └── index.test.js ├── example ├── Main.ts ├── app │ ├── App.ts │ ├── AppComponent.tsx │ ├── AppFactory.ts │ ├── Index.tsx │ ├── components │ │ ├── MonitorComponent.tsx │ │ ├── MonitorsComponent.tsx │ │ ├── layout │ │ │ └── LayoutComponent.tsx │ │ ├── picture │ │ │ └── PictureComponent.tsx │ │ └── pictures-list │ │ │ ├── PicturesListComponent.tsx │ │ │ └── PicturesListPictureComponent.tsx │ ├── index.html │ ├── stores │ │ └── PicturesStore.ts │ ├── style.css │ ├── tsconfig.json │ ├── types │ │ └── window │ │ │ └── index.d.ts │ └── webpack.config.js └── tsconfig.json ├── misc ├── demo-10-monitor.gif └── demo-2-monitor.gif ├── package-lock.json ├── package.json ├── src ├── MultiMonitor.ts ├── MultiMonitorFactory.ts ├── browser │ ├── ElectronMultiMonitor.ts │ └── OtherMonitor.ts ├── index.test.ts ├── index.ts ├── monitor │ ├── MainMonitor.ts │ ├── Monitor.ts │ └── MonitorFactory.ts ├── preload │ ├── Main.ts │ ├── MainWithContextIsolation.ts │ └── Other.ts └── types │ └── multi-monitor │ └── index.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | build 4 | dist 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.0.0] - 2020-03-27 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Pieter-Jan Van Robays 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron Multi Monitor 2 | 3 | 4 | ## Introduction 5 | This package provides web developers with the ability to create applications which cover multiple browser windows via [Electron](https://electronjs.org/). 6 | It removes the restriction for web developers to only have a single window to develop their product in. 7 | 8 | The library will create linked JavaScript `window` objects for you. Meaning you only need to worry about one window object. No need for special communication between the different windows; just pure JavaScript! 9 | 10 | To see the library in action, clone the repository and run the example: 11 | 12 | $ git clone https://github.com/pvrobays/electron-multi-monitor.git 13 | $ cd electron-multi-monitor 14 | $ npm i 15 | $ npm run example 16 | 17 | ## Demo 18 | ### 2-monitor example 19 | 20 | 21 | ### 10-monitor example 🤷‍ 22 | Overkill. But possible. 23 | 24 | 25 | 26 | ## Getting Started 27 | You can always check out the code from the demo, found in the `example` folder. 28 | 29 | ### 1. Installation & Import 30 | Easiest way to install it is via [npm](https://www.npmjs.com/get-npm): 31 | ```bash 32 | npm install electron-multi-monitor 33 | ``` 34 | Next you'll be able to import the MultiMonitor object inside your [Electron](https://electronjs.org/) app: 35 | ```ts 36 | import { MultiMonitor } from "electron-multi-monitor"; 37 | ``` 38 | P.s. If you're new to electron, [Electron Forge](https://www.electronforge.io/) is a great starting point. They even have a [guide](https://www.electronforge.io/guides/framework-integration/react-with-typescript) on how to enable TypeScript & React. 39 | 40 | ### 2. Create a MultiMonitor instance 41 | There are 2 ways of creating MultiMonitor instance: 42 | 43 | 1. Use the default instance 44 | ```ts 45 | const multiMonitor = MultiMonitor.instance; 46 | ``` 47 | 2. or, Create your own via the MultiMonitorFactory 48 | ```ts 49 | const multiMonitor = new MultiMonitorFactory().create(); 50 | ``` 51 | The `multiMonitor` object can be used to adapt, move, interact with the opened windows within your Main process: 52 | ```ts 53 | interface IMultiMonitor { 54 | readonly monitors: BrowserWindow[]; 55 | openUrl(url: string, numberOfMonitors: number): Promise; 56 | destroyAllMonitors(): void; 57 | } 58 | ``` 59 | 60 | ### 3. Launch multiple monitors 61 | Now you can open your multi-monitor page via the MultiMonitor instance: 62 | ```ts 63 | multiMonitor.openUrl(url, numberOfWindowsToOpen) 64 | .then(() => { 65 | console.log("Monitor windows are opened have your URL loaded!"); 66 | }); 67 | ``` 68 | 69 | This will open your url inside the number of windows you've defined. 70 | Now you'll have the object `window.electronMultiMonitor` available in your render process: 71 | 72 | ```ts 73 | interface IElectronMultiMonitor { 74 | readonly mainWindow: Window; 75 | readonly otherMonitors: IOtherMonitor[]; 76 | readonly numberOfMonitors: number; 77 | } 78 | 79 | interface IOtherMonitor { 80 | readonly htmlRoot: HTMLElement; 81 | readonly window: Window; 82 | } 83 | ``` 84 | 85 | It's now your responsibility to render the different UI elements on the different htmlRoots. 86 | If you don't want this responsibility, you can check out [electron-multi-monitor-react](https://github.com/pvrobays/electron-multi-monitor-react) which does all this for you. 87 | 88 | ## Contribute 89 | Yes please! I'm looking for motivated contributors to help me. If you're interested don't hesitate to contact me. 90 | 91 | ## Thanks 92 | * [Picsum Photos](https://picsum.photos/) - Hosting the images for the example app -------------------------------------------------------------------------------- /dist/index.test.js: -------------------------------------------------------------------------------- 1 | describe("Index", () => { 2 | test("You should always have at least one test, right?", () => { }); 3 | }); 4 | -------------------------------------------------------------------------------- /example/Main.ts: -------------------------------------------------------------------------------- 1 | import { app } from "electron"; 2 | // @ts-ignore 3 | import { IMultiMonitor, MultiMonitor } from "../../dist"; //import { IMultiMonitor, MultiMonitor } from "electron-multi-monitor"; //when using via npm 4 | 5 | const numberOfWindowsToOpen = 2; 6 | 7 | let multiMonitor: IMultiMonitor | null = null; 8 | 9 | function onReady() { 10 | console.log("app ready"); 11 | 12 | multiMonitor = MultiMonitor.instance; 13 | 14 | // const url = "https://google.com"; 15 | // const url = "about:blank"; 16 | const url = `file://${__dirname}/../app/index.html`; 17 | 18 | multiMonitor.openUrl(url, numberOfWindowsToOpen).then(() => { 19 | console.log("Monitor windows are opened & loaded!"); 20 | }); 21 | } 22 | 23 | app.on('ready', onReady); 24 | 25 | app.on('window-all-closed', function() { 26 | if (process.platform !== 'darwin') { 27 | app.quit(); 28 | } 29 | 30 | multiMonitor = null; 31 | }); 32 | 33 | app.on('activate', function() { 34 | if (multiMonitor === null) { 35 | onReady(); 36 | } 37 | }); -------------------------------------------------------------------------------- /example/app/App.ts: -------------------------------------------------------------------------------- 1 | import { inject } from "mobx-react"; 2 | import { IStoresToProps } from "mobx-react/dist/types/IStoresToProps"; 3 | import { IPicturesStore } from "./stores/PicturesStore"; 4 | 5 | export interface IApp { 6 | stores: IAppStores 7 | } 8 | 9 | export class App implements IApp { 10 | 11 | constructor( 12 | public readonly stores: IAppStores 13 | ) { 14 | 15 | } 16 | 17 | 18 | } 19 | 20 | export interface IAppStores { 21 | picturesStore: IPicturesStore; 22 | } 23 | 24 | export function injectFromApp(fn: IStoresToProps>) { 25 | return inject(fn); 26 | } -------------------------------------------------------------------------------- /example/app/AppComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IApp } from "./App"; 3 | import { Provider } from "mobx-react" 4 | import { MonitorsComponent } from "./components/MonitorsComponent"; 5 | 6 | export interface IAppComponentProps { 7 | app: IApp; 8 | } 9 | 10 | export class AppComponent extends React.Component { 11 | 12 | render() { 13 | const { app } = this.props; 14 | //If you want, here you can write code that should only happen on the main window. 15 | 16 | //Once you're done, call the which will render the UI for all the monitors 17 | return 18 | 19 | ; 20 | } 21 | } -------------------------------------------------------------------------------- /example/app/AppFactory.ts: -------------------------------------------------------------------------------- 1 | import { App, IApp, IAppStores } from "./App"; 2 | import { PicturesStore } from "./stores/PicturesStore"; 3 | 4 | export interface IAppFactory { 5 | create(): IApp; 6 | } 7 | 8 | export class AppFactory implements IAppFactory { 9 | 10 | create(): IApp { 11 | 12 | const picturesStores = new PicturesStore(); 13 | 14 | const appStores: IAppStores = { 15 | picturesStore: picturesStores 16 | }; 17 | 18 | return new App(appStores); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /example/app/Index.tsx: -------------------------------------------------------------------------------- 1 | import { configure } from "mobx"; 2 | import * as React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { AppComponent } from "./AppComponent"; 5 | import { AppFactory } from "./AppFactory"; 6 | 7 | (() => { 8 | if (!window.electronMultiMonitor) { 9 | console.warn("We're not the Main window, so we won't execute any code to avoid errors."); 10 | return; // Only the main window should execute this code 11 | } 12 | 13 | //mobx configuration - mobx = simple state management library. If you don't know it yet, look it up; it's awesome! https://github.com/mobxjs/mobx 14 | configure({ 15 | enforceActions: "observed", 16 | useProxies: "always", 17 | }); 18 | 19 | const appFactory = new AppFactory(); 20 | const app = appFactory.create(); 21 | 22 | window["app"] = app; 23 | 24 | ReactDOM.render(, document.body); 25 | })(); -------------------------------------------------------------------------------- /example/app/components/MonitorComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LayoutComponent } from "./layout/LayoutComponent"; 3 | import { PictureComponent } from "./picture/PictureComponent"; 4 | import { PicturesListComponent } from "./pictures-list/PicturesListComponent"; 5 | 6 | export interface IMonitorComponentProps { 7 | rank: number; 8 | numberOfMonitors: number; 9 | currentWindow: Window; 10 | } 11 | 12 | export class MonitorComponent extends React.Component { 13 | 14 | render() { 15 | const { rank, numberOfMonitors } = this.props; 16 | 17 | let monitorComponent: JSX.Element; 18 | switch (rank) { 19 | // Choose which monitor should show which component... 20 | case 1: 21 | monitorComponent = ; 22 | break; 23 | default: 24 | monitorComponent = ; 25 | } 26 | 27 | return 28 | { monitorComponent } 29 | ; 30 | } 31 | } -------------------------------------------------------------------------------- /example/app/components/MonitorsComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MonitorComponent } from "./MonitorComponent"; 4 | 5 | export class MonitorsComponent extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | 10 | const { otherMonitors } = window.electronMultiMonitor; 11 | 12 | //Remove all child elements from the htmlRoot; React Portal doesn't remove those by default 13 | for (const otherMonitor of otherMonitors) { 14 | const { htmlRoot } = otherMonitor; 15 | while (htmlRoot.firstChild) { 16 | htmlRoot.removeChild(htmlRoot.firstChild); 17 | } 18 | } 19 | } 20 | 21 | render() { 22 | const { otherMonitors, mainWindow, numberOfMonitors } = window.electronMultiMonitor; 23 | 24 | return <> 25 | 26 | { 27 | otherMonitors.map((value, index) => 28 | ReactDOM.createPortal( 29 | , 30 | value.htmlRoot 31 | ) 32 | ) 33 | } 34 | ; 35 | } 36 | } -------------------------------------------------------------------------------- /example/app/components/layout/LayoutComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface ILayoutComponentProps { 4 | rank: number; 5 | numberOfMonitors: number; 6 | } 7 | 8 | /** 9 | * General Layout component which will be shown on each monitor 10 | */ 11 | export class LayoutComponent extends React.Component { 12 | 13 | render() { 14 | const { children, rank, numberOfMonitors } = this.props; 15 | 16 | return
17 |
18 |
19 | Photo Gallery 20 | 21 | 24 | 25 |
26 | Monitor number #{ rank } / { numberOfMonitors } 27 |
28 |
29 |
30 |
31 | { children } 32 |
33 |
34 | View the source of this multi monitor web application on github.com/pvrobays/electron-multi-monitor. Made with ♥ in Belgium ⚫🟡🔴 35 |
36 |
; 37 | } 38 | } -------------------------------------------------------------------------------- /example/app/components/picture/PictureComponent.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import React from "react"; 3 | import { injectFromApp } from "../../App"; 4 | import { IPicturesStore } from "../../stores/PicturesStore"; 5 | 6 | export interface IPictureComponentProps { 7 | type: number; 8 | picturesStore?: IPicturesStore; 9 | } 10 | 11 | @injectFromApp(stores => ({ 12 | picturesStore: stores.picturesStore 13 | })) 14 | @observer 15 | export class PictureComponent extends React.Component { 16 | 17 | private closePicture = () => { 18 | this.props.picturesStore!.setSelectedPictureId(null); 19 | }; 20 | 21 | render() { 22 | const { type, picturesStore } = this.props; 23 | const { selectedPictureId } = picturesStore!; 24 | 25 | if (!selectedPictureId) 26 | return

Select a picture

; 27 | 28 | 29 | let typeExtra: string; 30 | switch (type) { 31 | case 2: 32 | typeExtra = "?grayscale"; 33 | break; 34 | case 3: 35 | typeExtra = "?blur"; 36 | break; 37 | case 4: 38 | typeExtra = "?grayscale&blur=2"; 39 | break; 40 | case 5: 41 | typeExtra = "?grayscale&blur=5"; 42 | break; 43 | case 1: 44 | default: 45 | typeExtra = ""; 46 | break; 47 | } 48 | 49 | return
50 |

Picture: { selectedPictureId }

51 |

X

52 | { 53 |

Picurues are powered by Picsum Photos

54 |
; 55 | } 56 | } -------------------------------------------------------------------------------- /example/app/components/pictures-list/PicturesListComponent.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import React from "react"; 3 | import { injectFromApp } from "../../App"; 4 | import { IPicturesStore } from "../../stores/PicturesStore"; 5 | import { PicturesListPictureComponent } from "./PicturesListPictureComponent"; 6 | 7 | export interface IPicturesListComponentProps { 8 | picturesStore?: IPicturesStore 9 | } 10 | 11 | @injectFromApp(stores => ({ 12 | picturesStore: stores.picturesStore 13 | })) 14 | @observer 15 | export class PicturesListComponent extends React.Component { 16 | 17 | render() { 18 | const { pictureIds } = this.props.picturesStore!; 19 | 20 | return
21 | { 22 | pictureIds.map(pictureId => ) 23 | } 24 |
; 25 | } 26 | } -------------------------------------------------------------------------------- /example/app/components/pictures-list/PicturesListPictureComponent.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import React from "react"; 3 | import { injectFromApp } from "../../App"; 4 | import { IPicturesStore } from "../../stores/PicturesStore"; 5 | 6 | export interface IPicturesListPictureComponentProps { 7 | pictureId: string; 8 | picturesStore?: IPicturesStore; 9 | } 10 | 11 | @injectFromApp(stores => ({ 12 | picturesStore: stores.picturesStore 13 | })) 14 | @observer 15 | export class PicturesListPictureComponent extends React.Component { 16 | 17 | private select = (pictureId: string) => { 18 | console.log(`🎆 clicked on pictureId ${ pictureId }`); 19 | this.props.picturesStore!.setSelectedPictureId(pictureId); 20 | }; 21 | 22 | render() { 23 | const { pictureId, picturesStore } = this.props; 24 | const { selectedPictureId } = picturesStore!; 25 | 26 | return
this.select(pictureId) }> 29 |

{ pictureId }

30 |
31 | { 32 |
33 |
34 | } 35 | } -------------------------------------------------------------------------------- /example/app/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Multi Monitor Application 6 | 7 | 8 | 9 | 10 |

Loading...

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/app/stores/PicturesStore.ts: -------------------------------------------------------------------------------- 1 | import { action, autorun, makeObservable, observable, runInAction } from "mobx"; 2 | 3 | const wordSeeds = "rush rich opposition dough responsible available strap cherry jet temptation suspect official halt faint robot ambition scholar frequency corner overwhelm terms omission loot appeal hell laundry nest presence delete praise machinery convention punish dividend attract slant party tower patch preference pill entitlement sweat romantic TRUE beam gap econobox radical impulse intermediate proof director sex white bullet outlook particle incident personality safe question ride relate archive quality weed betray lane harmony miscarriage premium talented track cable field kinship expression quarrel friend load draw witness incongruous resignation grimace west bike pneumonia fish night brother family file privilege plaintiff lot deny computing firm"; 4 | 5 | export interface IPicturesStore { 6 | readonly pictureIds: string[]; 7 | selectedPictureId: string | null; 8 | 9 | setSelectedPictureId(pictureId: string | null); 10 | } 11 | 12 | export class PicturesStore implements IPicturesStore { 13 | 14 | @observable 15 | public readonly pictureIds: string[]; 16 | 17 | @observable 18 | public selectedPictureId: string | null; 19 | 20 | constructor() { 21 | this.pictureIds = wordSeeds.split(" "); 22 | this.selectedPictureId = this.pictureIds[0] ?? null; 23 | 24 | makeObservable(this); 25 | } 26 | 27 | @action 28 | setSelectedPictureId = (pictureId: string | null) => { 29 | this.selectedPictureId = pictureId; 30 | } 31 | } -------------------------------------------------------------------------------- /example/app/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | 4 | .header { 5 | background: #f1f1f1; 6 | color: #999; 7 | } 8 | 9 | .header .info { 10 | float: right; 11 | color: #AAA; 12 | font-size: 0.8rem; 13 | line-height: 34px; 14 | padding-right: 5px; 15 | } 16 | 17 | .footer { 18 | position: fixed; 19 | bottom: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 40px; 23 | box-sizing: border-box; 24 | background: #111; 25 | color: #666; 26 | text-align: center; 27 | padding: 1em; 28 | font-size: 80%; 29 | } 30 | 31 | .footer a, .footer a:visited { 32 | color: #666; 33 | } 34 | 35 | .picture-container { 36 | border: 5px solid transparent; 37 | box-sizing: border-box; 38 | transition: all 0.2s ease-out; 39 | cursor: pointer; 40 | } 41 | 42 | .picture-container.selected { 43 | border: 0px solid transparent; 44 | } 45 | 46 | .picture-title { 47 | text-transform: capitalize; 48 | } 49 | 50 | .picture-placeholder { 51 | position: relative; 52 | width: 300px; 53 | min-height: 50px; 54 | } 55 | 56 | .picture-placeholder::after { 57 | content: ""; 58 | position: absolute; 59 | left: 50%; 60 | top: 50%; 61 | width: 30px; 62 | height: 30px; 63 | transform: translate(-50%, -50%); 64 | box-sizing: border-box; 65 | border-top: 2px solid black; 66 | border-right: 2px solid transparent; 67 | border-radius: 50%; 68 | -webkit-animation: 1s spin linear infinite; 69 | animation: 1s spin linear infinite; 70 | z-index: -1; 71 | } 72 | 73 | .picture-viewport { 74 | position: relative; 75 | width: 100%; 76 | height: Calc(100vh - 74px); 77 | } 78 | 79 | .picture-viewport .picture-title { 80 | position: fixed; 81 | top: 40px; 82 | left: 10px; 83 | background: rgba(0,0,0,0.1); 84 | padding: 3px; 85 | margin: 0; 86 | color: white; 87 | } 88 | 89 | .picture-viewport .picture-close { 90 | position: fixed; 91 | top: 40px; 92 | right: 10px; 93 | background: rgba(0,0,0,0.1); 94 | padding: 3px; 95 | margin: 0; 96 | color: white; 97 | line-height: 42px; 98 | width: 42px; 99 | text-align: center; 100 | cursor: pointer; 101 | } 102 | 103 | .powered-by { 104 | position: fixed; 105 | bottom: 40px; 106 | right: 10px; 107 | color: white; 108 | background: rgba(0,0,0,0.5); 109 | font-size: 0.8rem; 110 | } 111 | 112 | .powered-by a, .powered-by a:visited { 113 | color: white; 114 | } 115 | 116 | @-webkit-keyframes spin { 117 | from { 118 | -webkit-transform: rotate(0deg); 119 | transform: rotate(0deg); 120 | } 121 | to { 122 | -webkit-transform: rotate(360deg); 123 | transform: rotate(360deg); 124 | } 125 | } 126 | 127 | @keyframes spin { 128 | from { 129 | -webkit-transform: rotate(0deg); 130 | transform: rotate(0deg); 131 | } 132 | to { 133 | -webkit-transform: rotate(360deg); 134 | transform: rotate(360deg); 135 | } 136 | } -------------------------------------------------------------------------------- /example/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "esModuleInterop": true, 5 | "experimentalDecorators": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noImplicitAny": false, 11 | "outDir": "dist", 12 | "removeComments": false, 13 | "skipLibCheck": true, 14 | "sourceMap": false, 15 | "strictNullChecks": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "target": "ESNext", 18 | "typeRoots": [ 19 | "../../node_modules/@types" 20 | ] 21 | }, 22 | "include": [ 23 | "./" 24 | ] 25 | } -------------------------------------------------------------------------------- /example/app/types/window/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | electronMultiMonitor: IElectronMultiMonitor 3 | } 4 | 5 | interface IElectronMultiMonitor { 6 | readonly mainWindow: Window; 7 | readonly otherMonitors: IOtherMonitor[]; 8 | readonly numberOfMonitors: number; 9 | 10 | // registerOtherMonitor(window: Window): Window; 11 | } 12 | 13 | interface IOtherMonitor { 14 | readonly htmlRoot: HTMLElement; 15 | readonly window: Window; 16 | } -------------------------------------------------------------------------------- /example/app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.tsx', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/, 11 | }, 12 | ], 13 | }, 14 | resolve: { 15 | extensions: ['.tsx', '.ts', '.js'], 16 | }, 17 | output: { 18 | filename: 'bundle.js', 19 | path: path.resolve(__dirname, 'dist'), 20 | }, 21 | }; -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "esModuleInterop": true, 5 | "experimentalDecorators": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noImplicitAny": false, 11 | "outDir": "dist", 12 | "removeComments": false, 13 | "skipLibCheck": true, 14 | "sourceMap": false, 15 | "strictNullChecks": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "target": "ESNext", 18 | "typeRoots": [ 19 | "../node_modules/@types", 20 | "../src/types", 21 | "./types" 22 | ] 23 | }, 24 | "exclude": [ 25 | "app" 26 | ], 27 | "include": [ 28 | "./" 29 | ] 30 | } -------------------------------------------------------------------------------- /misc/demo-10-monitor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvrobays/electron-multi-monitor/3bdf65005ec9654ca95ef45885435c0bc4f214a0/misc/demo-10-monitor.gif -------------------------------------------------------------------------------- /misc/demo-2-monitor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pvrobays/electron-multi-monitor/3bdf65005ec9654ca95ef45885435c0bc4f214a0/misc/demo-2-monitor.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-multi-monitor", 3 | "version": "1.0.0", 4 | "description": "Create multi monitor applications using web development", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "rimraf ./dist && tsc", 8 | "build-example": "cd example && rimraf ./dist && tsc", 9 | "build-example-app": "cd example/app && webpack-cli --config=webpack.config.js --mode=development", 10 | "example": "npm run build && npm run build-example && npm run build-example-app && electron example/dist/Main.js", 11 | "prepublishOnly": "npm run test && npm run build && npm run build-example && npm run build-example-app", 12 | "test": "jest", 13 | "ts": "tsc --pretty --noEmit" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/pvrobays/electron-multi-monitor.git" 18 | }, 19 | "keywords": [ 20 | "multi", 21 | "monitor", 22 | "electron", 23 | "multiple", 24 | "screen", 25 | "web", 26 | "development" 27 | ], 28 | "author": "Pieter-Jan Van Robays", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/pvrobays/electron-multi-monitor/issues" 32 | }, 33 | "homepage": "https://github.com/pvrobays/electron-multi-monitor#readme", 34 | "files": [ 35 | "dist/*" 36 | ], 37 | "devDependencies": { 38 | "@types/jest": "^26.0.21", 39 | "@types/node": "^14.14.35", 40 | "@types/react": "^17.0.3", 41 | "@types/react-dom": "^17.0.2", 42 | "electron": "^12.0.1", 43 | "jest": "^26.6.3", 44 | "jest-junit": "^12.0.0", 45 | "mobx": "^6.1.8", 46 | "mobx-react": "^7.1.0", 47 | "purecss": "^2.0.5", 48 | "react": "^17.0.1", 49 | "react-dom": "^17.0.1", 50 | "rimraf": "^3.0.2", 51 | "ts-jest": "^26.5.3", 52 | "ts-loader": "^8.0.18", 53 | "typescript": "^4.2.3", 54 | "webpack": "^5.26.3", 55 | "webpack-cli": "^4.5.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/MultiMonitor.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, BrowserWindowConstructorOptions, Event, Referrer } from "electron"; 2 | import { MainMonitor } from "./monitor/MainMonitor"; 3 | import { IMonitorFactory } from "./monitor/MonitorFactory"; 4 | import { MultiMonitorFactory } from "./MultiMonitorFactory"; 5 | 6 | 7 | export interface IMultiMonitor { 8 | readonly monitors: BrowserWindow[]; 9 | 10 | openUrl(url: string, numberOfMonitors: number): Promise; 11 | destroyAllMonitors(): void; 12 | } 13 | 14 | export class MultiMonitor implements IMultiMonitor { 15 | public static instance: IMultiMonitor = new MultiMonitorFactory().create(); 16 | 17 | private _mainMonitor: MainMonitor | null; 18 | private _otherMonitors: BrowserWindow[]; 19 | 20 | constructor( 21 | private readonly monitorFactory: IMonitorFactory 22 | ) { 23 | this._otherMonitors = []; 24 | this._mainMonitor = null; 25 | } 26 | 27 | get monitors(): BrowserWindow[] { 28 | if (!this._mainMonitor) return this._otherMonitors; 29 | return [this._mainMonitor, ...this._otherMonitors]; 30 | } 31 | 32 | async openUrl(url: string, numberOfMonitors: number): Promise { 33 | if (numberOfMonitors < 1) 34 | throw new Error("numberOfMonitors should be at least 1"); 35 | 36 | this.destroyAllMonitors(); 37 | 38 | 39 | const { monitorFactory } = this; 40 | 41 | this._mainMonitor = monitorFactory.createMain(numberOfMonitors - 1); 42 | this.monitors.push(this._mainMonitor); 43 | 44 | // this._mainMonitor.webContents.openDevTools(); 45 | 46 | //TODO: refactor to use webContents.setWindowOpenHandler 47 | this._mainMonitor.webContents.on('new-window', this.onNewWindow); 48 | 49 | await this._mainMonitor.loadURL(url); 50 | } 51 | 52 | destroyAllMonitors() { 53 | const { monitors } = this; 54 | 55 | for (const monitor of monitors) { 56 | monitor.destroy(); 57 | } 58 | 59 | this._mainMonitor = null; 60 | this._otherMonitors = []; 61 | } 62 | 63 | private onNewWindow = (event: Event, url: string, frameName: string, disposition: string, options: BrowserWindowConstructorOptions, additionalFeatures: string[], referrer: Referrer) => { 64 | console.debug(`on mainMonitor 'new-window' - url: ${ url } - frameName: ${ frameName }`); 65 | 66 | if (frameName.substr(0, 8) !== "MM-other") { 67 | console.debug(`we're not interested in frame ${ frameName } since it doesn't start with 'MM-other'`); 68 | return; 69 | } 70 | 71 | const { monitorFactory, _mainMonitor } = this; 72 | 73 | monitorFactory.updateOptions(options); 74 | 75 | // @ts-ignore TODO: check to solve this typing issue 76 | options.webContents.once('dom-ready', (event: Event) => { 77 | 78 | // @ts-ignore TODO: check to solve this typing issue 79 | const browserWindow = BrowserWindow.fromWebContents(event.sender); 80 | 81 | if (!browserWindow) { 82 | console.warn(`Couldn't create a BrowserWindow from the event.sender`); 83 | return; 84 | } 85 | 86 | this.addOtherMonitor(browserWindow); 87 | 88 | // browserWindow.webContents.openDevTools(); 89 | 90 | //TODO set bounds etc. 91 | browserWindow.setSize(1280, 1024); 92 | browserWindow.setPosition(1280, 0); 93 | }); 94 | 95 | 96 | let domLoaded = false; 97 | // @ts-ignore TODO: check to solve this typing issue 98 | options.webContents.on('dom-ready', (event: Event) => { 99 | domLoaded = true; 100 | }); 101 | 102 | // @ts-ignore TODO: check to solve this typing issue 103 | options.webContents.on('will-navigate', (e, url) => { 104 | if (!_mainMonitor) return; 105 | if (_mainMonitor.webContents.getURL() === url && !domLoaded) 106 | return; //only pass the same url to the mainMonitor when the dom was already loaded 107 | console.info('Other window received url navigation which it will send to the main window:', url); 108 | e.preventDefault(); 109 | domLoaded = false; 110 | _mainMonitor.loadURL(url); 111 | }); 112 | } 113 | 114 | private addOtherMonitor(monitor: BrowserWindow) { 115 | this._otherMonitors.push(monitor); 116 | } 117 | } -------------------------------------------------------------------------------- /src/MultiMonitorFactory.ts: -------------------------------------------------------------------------------- 1 | import { MonitorFactory } from "./monitor/MonitorFactory"; 2 | import { IMultiMonitor, MultiMonitor } from "./MultiMonitor"; 3 | 4 | export class MultiMonitorFactory { 5 | 6 | create(): IMultiMonitor { 7 | 8 | const monitorFactory = new MonitorFactory(); 9 | 10 | const multiMonitor = new MultiMonitor(monitorFactory); 11 | 12 | return multiMonitor; 13 | } 14 | } -------------------------------------------------------------------------------- /src/browser/ElectronMultiMonitor.ts: -------------------------------------------------------------------------------- 1 | import { IOtherMonitor, OtherMonitor } from "./OtherMonitor"; 2 | 3 | export interface IElectronMultiMonitor { 4 | readonly mainWindow: Window; 5 | readonly otherMonitors: IOtherMonitor[]; 6 | readonly numberOfMonitors: number; 7 | 8 | registerOtherMonitor(window: Window): Window; 9 | } 10 | 11 | export class ElectronMultiMonitor implements IElectronMultiMonitor { 12 | readonly otherMonitors: IOtherMonitor[]; 13 | 14 | public onReady?: (electronMultiMonitor: IElectronMultiMonitor) => void; 15 | 16 | constructor( 17 | public readonly mainWindow: Window, 18 | public readonly numberOfMonitors: number 19 | ) { 20 | this.otherMonitors = []; 21 | 22 | if (numberOfMonitors === 1) 23 | this.triggerOnReady(); 24 | } 25 | 26 | registerOtherMonitor(window: Window): Window { 27 | 28 | const otherMonitor = new OtherMonitor(window); 29 | 30 | this.otherMonitors.push(otherMonitor); 31 | 32 | if (this.otherMonitors.length === this.numberOfMonitors) { 33 | this.triggerOnReady(); 34 | } 35 | 36 | return this.mainWindow; 37 | } 38 | 39 | private triggerOnReady() { 40 | setTimeout(() => { 41 | if (this.onReady) 42 | this.onReady(this); 43 | }, 10); 44 | } 45 | } -------------------------------------------------------------------------------- /src/browser/OtherMonitor.ts: -------------------------------------------------------------------------------- 1 |  2 | export interface IOtherMonitor { 3 | readonly htmlRoot: HTMLElement; 4 | readonly window: Window; 5 | } 6 | 7 | export class OtherMonitor implements IOtherMonitor { 8 | 9 | constructor( 10 | public readonly window: Window 11 | ) { 12 | 13 | } 14 | 15 | get htmlRoot(): HTMLElement { 16 | return this.window.document.body; 17 | } 18 | } -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | describe("Index", () => { 2 | test("You should always have at least one test, right?", () => {}); 3 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { IMultiMonitor, MultiMonitor } from "./MultiMonitor"; 2 | export { MultiMonitorFactory } from "./MultiMonitorFactory"; -------------------------------------------------------------------------------- /src/monitor/MainMonitor.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindowConstructorOptions } from "electron"; 2 | import { Monitor } from "./Monitor"; 3 | 4 | export class MainMonitor extends Monitor { 5 | public isMain: boolean; 6 | public numberOfOthersToOpen: number; 7 | 8 | constructor(numberOfOthersToOpen: number, options: BrowserWindowConstructorOptions) { 9 | super(options); 10 | 11 | this.isMain = true; 12 | this.numberOfOthersToOpen = numberOfOthersToOpen; 13 | } 14 | } -------------------------------------------------------------------------------- /src/monitor/Monitor.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; 2 | 3 | export class Monitor extends BrowserWindow { 4 | public isMain: boolean; 5 | 6 | constructor(options: BrowserWindowConstructorOptions) { 7 | super(options); 8 | 9 | this.isMain = false; 10 | } 11 | } -------------------------------------------------------------------------------- /src/monitor/MonitorFactory.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindowConstructorOptions } from "electron"; 2 | import path from "path"; 3 | import { MainMonitor } from "./MainMonitor"; 4 | 5 | export interface IMonitorFactory { 6 | createMain(numberOfOthersToOpen: number): MainMonitor; 7 | 8 | updateOptions(options: BrowserWindowConstructorOptions): void; 9 | } 10 | 11 | export class MonitorFactory implements IMonitorFactory { 12 | 13 | createMain(numberOfOthersToOpen: number): MainMonitor { 14 | const rank = 0; 15 | const browserWindowOptions: BrowserWindowConstructorOptions = { 16 | // show: false, 17 | x: 0, 18 | y: 0, //TODO: make the webapp itself change the bounds of the window 19 | width: 1280, 20 | height: 1024, 21 | minWidth: 600, 22 | minHeight: 600, 23 | resizable: true, 24 | movable: true, 25 | titleBarStyle: "default", 26 | backgroundColor: "#FFFFFF", 27 | title: `Multi Monitor #${ rank }`, 28 | webPreferences: { 29 | contextIsolation: false, //TODO: find fix for window.opener? Create our own Html file? 30 | nodeIntegration: false, 31 | preload: path.join(__dirname, "../preload/Main.js"), 32 | nativeWindowOpen: true, 33 | affinity: "MULTI-MONITOR", 34 | enableRemoteModule: true //TODO: try to remove this by using IPC calls 35 | } 36 | }; 37 | 38 | const monitor = new MainMonitor(numberOfOthersToOpen, browserWindowOptions); 39 | 40 | return monitor; 41 | } 42 | 43 | updateOptions(options: BrowserWindowConstructorOptions) { 44 | if (!options.webPreferences) 45 | throw new Error("webPreferences of the new pop-up window are undefined"); 46 | 47 | options.webPreferences.preload = path.join(__dirname, './preload/Other.js'); 48 | options.webPreferences.nativeWindowOpen = true; 49 | options.webPreferences.affinity = 'MULTI-MONITOR'; 50 | options.webPreferences.nodeIntegration = false; 51 | options.webPreferences.enableRemoteModule = true; //TODO: try to remove this by using IPC calls 52 | } 53 | } -------------------------------------------------------------------------------- /src/preload/Main.ts: -------------------------------------------------------------------------------- 1 | import { remote } from "electron"; 2 | import { ElectronMultiMonitor } from "../browser/ElectronMultiMonitor"; 3 | import { MainMonitor } from "../monitor/MainMonitor"; 4 | // import { contextBridge } from "electron"; 5 | 6 | console.warn("Loading preload/Main.js..."); 7 | 8 | const currentMonitor: MainMonitor = remote.getCurrentWindow() as MainMonitor; 9 | // contextBridge.exposeInMainWorld("currentMonitor", currentMonitor); 10 | window.currentMonitor = currentMonitor; 11 | 12 | const { numberOfOthersToOpen } = currentMonitor; 13 | 14 | if (typeof(numberOfOthersToOpen) !== "undefined") { 15 | console.log("🔳 Main monitor"); 16 | 17 | const numberOfMonitors = numberOfOthersToOpen + 1; 18 | const electronMultiMonitor = new ElectronMultiMonitor(window, numberOfMonitors); 19 | // contextBridge.exposeInMainWorld("electronMultiMonitor", electronMultiMonitor); 20 | window.electronMultiMonitor = electronMultiMonitor; 21 | 22 | const { href } = window.location; 23 | for (let i = 0; i < numberOfOthersToOpen; i++) { 24 | window.open(href, `MM-other-${i}`); 25 | } 26 | } 27 | else { 28 | console.log("🔳 Other monitor"); 29 | 30 | if (!window.opener) 31 | throw new Error("This should be an 'other monitor' but there's no window.opener defined! Can't connect the multiple monitors!"); 32 | 33 | const opener = window.opener as Window; 34 | console.warn("window.opener", opener); 35 | // contextBridge.exposeInMainWorld("customOpener", opener); 36 | const { electronMultiMonitor } = opener; 37 | if (!electronMultiMonitor) 38 | throw new Error("This should be an 'other monitor' but the window.opener has no 'electronMultiMonitor' object! Can't connect the multiple monitors!"); 39 | 40 | const mainWindow = electronMultiMonitor.registerOtherMonitor(window); 41 | // contextBridge.exposeInMainWorld("mainWindow", mainWindow); 42 | window.mainWindow = mainWindow; 43 | } 44 | 45 | console.warn("Loaded preload/Main.js..."); -------------------------------------------------------------------------------- /src/preload/MainWithContextIsolation.ts: -------------------------------------------------------------------------------- 1 | import { remote, contextBridge } from "electron"; 2 | import { ElectronMultiMonitor } from "../browser/ElectronMultiMonitor"; 3 | import { MainMonitor } from "../monitor/MainMonitor"; 4 | 5 | //TODO Delete this file 6 | 7 | console.warn("Loading preload/Main.js..."); 8 | 9 | const currentMonitor: MainMonitor = remote.getCurrentWindow() as MainMonitor; 10 | contextBridge.exposeInMainWorld("currentMonitor", currentMonitor); 11 | console.log("currentMonitor: ", currentMonitor); 12 | 13 | const { numberOfOthersToOpen } = currentMonitor; 14 | 15 | if (typeof(numberOfOthersToOpen) !== "undefined") { 16 | console.log("🔳 Main monitor"); 17 | 18 | const numberOfMonitors = numberOfOthersToOpen + 1; 19 | const electronMultiMonitor = new ElectronMultiMonitor(window, numberOfMonitors); 20 | contextBridge.exposeInMainWorld("electronMultiMonitor", electronMultiMonitor); 21 | 22 | for (let i = 0; i < numberOfOthersToOpen; i++) { 23 | window.open("about:blank", `MM-other-${i}`); 24 | } 25 | } 26 | else { 27 | console.log("🔳 Other monitor"); 28 | 29 | if (!window.opener) 30 | throw new Error("This should be an 'other monitor' but there's no window.opener defined! Can't connect the multiple monitors!"); 31 | 32 | console.warn("window.opener", window.opener); 33 | 34 | const opener = window.opener as Window; 35 | const { electronMultiMonitor } = opener; 36 | if (!electronMultiMonitor) 37 | throw new Error("This should be an 'other monitor' but the window.opener has no 'electronMultiMonitor' object! Can't connect the multiple monitors!"); 38 | 39 | const mainWindow = electronMultiMonitor.registerOtherMonitor(window); 40 | contextBridge.exposeInMainWorld("mainWindow", mainWindow); 41 | } 42 | 43 | console.warn("Loaded preload/Main.js..."); -------------------------------------------------------------------------------- /src/preload/Other.ts: -------------------------------------------------------------------------------- 1 | console.warn("Loading preload/Other.js..."); 2 | 3 | //TODO Delete this file 4 | 5 | console.warn("Loaded preload/Other.js..."); -------------------------------------------------------------------------------- /src/types/multi-monitor/index.d.ts: -------------------------------------------------------------------------------- 1 |  2 | interface Window { 3 | currentMonitor?: any; //TODO: create better declaration; or remove? 4 | electronMultiMonitor?: IElectronMultiMonitor; 5 | mainWindow?: Window; 6 | } 7 | 8 | interface IElectronMultiMonitor { 9 | readonly mainWindow: Window; 10 | readonly otherMonitors: IOtherMonitor[]; 11 | readonly numberOfMonitors: number; 12 | 13 | registerOtherMonitor(window: Window): Window; 14 | } 15 | 16 | interface IOtherMonitor { 17 | readonly htmlRoot: HTMLElement; 18 | readonly window: Window; 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noImplicitAny": false, 11 | "outDir": "dist", 12 | "removeComments": false, 13 | "skipLibCheck": true, 14 | "sourceMap": false, 15 | "strictNullChecks": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "target": "ESNext", 18 | "typeRoots": [ 19 | "./node_modules/@types", 20 | "./src/types" 21 | ] 22 | }, 23 | "include": [ 24 | "./", 25 | "./src/preload" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "out", 30 | "obj", 31 | "bin", 32 | "example" 33 | ] 34 | } --------------------------------------------------------------------------------