├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── communication.gif ├── custom_tslint.json ├── deploy.sh ├── deploy_key.enc ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── projects └── ngx-multi-window │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── ng-package.prod.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── multi-window.module.ts │ │ ├── providers │ │ │ ├── config.provider.ts │ │ │ ├── multi-window.service.ts │ │ │ ├── storage.service.ts │ │ │ └── window.provider.ts │ │ └── types │ │ │ ├── message.type.ts │ │ │ ├── multi-window.config.ts │ │ │ ├── window-save-strategy.enum.ts │ │ │ └── window.type.ts │ └── test.ts │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── src ├── .browserslistrc ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ └── providers │ │ ├── name-generator.service.spec.ts │ │ └── name-generator.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json └── tsconfig.spec.json └── tsconfig.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | eslint: 12 | enabled: true 13 | markdownlint: 14 | enabled: true 15 | fixme: 16 | enabled: true 17 | ratings: 18 | paths: 19 | - "**.inc" 20 | - "**.js" 21 | - "**.jsx" 22 | - "**.module" 23 | - "**.php" 24 | - "**.py" 25 | - "**.rb" 26 | exclude_paths: [] 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json" 14 | ], 15 | "createDefaultProgram": true 16 | }, 17 | "extends": [ 18 | "plugin:@angular-eslint/recommended", 19 | "plugin:@angular-eslint/template/process-inline-templates" 20 | ], 21 | "rules": { 22 | "no-console": "warn", 23 | "no-debugger": "error", 24 | "@angular-eslint/directive-selector": [ 25 | "error", 26 | { 27 | "type": "attribute", 28 | "prefix": "app", 29 | "style": "camelCase" 30 | } 31 | ], 32 | "@angular-eslint/component-selector": [ 33 | "error", 34 | { 35 | "type": "element", 36 | "prefix": "app", 37 | "style": "kebab-case" 38 | } 39 | ] 40 | } 41 | }, 42 | { 43 | "files": [ 44 | "*.html" 45 | ], 46 | "extends": [ 47 | "plugin:@angular-eslint/template/recommended" 48 | ], 49 | "rules": {} 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | # Project specific 43 | deploy_key 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | 5 | env: 6 | global: 7 | - ENCRYPTION_LABEL: "7cec43065660" 8 | - COMMIT_AUTHOR_EMAIL: "sebastian.fuss@googlemail.com" 9 | 10 | node_js: 11 | - "11" 12 | 13 | before_install: 14 | 15 | install: 16 | # Install the library deps 17 | - npm install 18 | # Install commitlint cli for travis 19 | - npm install @commitlint/travis-cli 20 | # Lint the demo app 21 | - npm run lint 22 | 23 | script: 24 | # Lint the commit messages 25 | - commitlint-travis 26 | # Execute the tests 27 | - npm run test:ci 28 | # Build the library 29 | - npm run build:lib 30 | # Build the demo app 31 | - npm run build:app 32 | 33 | after_success: 34 | - npm run deploy:app 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Sebastian Fuss 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 | # ngx-multi-window [![npm version](https://img.shields.io/npm/v/ngx-multi-window.svg?style=flat)](https://www.npmjs.com/package/ngx-multi-window) [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) 2 | 3 | Pull-based cross-window communication for multi-window angular applications 4 | 5 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b175dcd8585a42bdbdb9c1ee2a313b3b)](https://www.codacy.com/app/sebastian-fuss/ngx-multi-window?utm_source=github.com&utm_medium=referral&utm_content=Nolanus/ngx-multi-window&utm_campaign=Badge_Grade) 7 | 8 | ## Features 9 | 10 | - Send messages between different tabs/windows that are running the angular app 11 | - Message receive notification for sending tab/window 12 | - Automatic detection/registration of new tabs/windows 13 | 14 | ## Setup 15 | 16 | First you need to install the npm module: 17 | ```sh 18 | npm install ngx-multi-window --save 19 | ``` 20 | 21 | For older angular versions you may install previous versions of this library: 22 | 23 | | ngx-multi-window version | compatible angular version | 24 | |--------------------------|----------------------------| 25 | | `0.6.1` | `16` | 26 | | `0.6` | `15` | 27 | | `0.5` | `14` | 28 | | `0.4.1` | `8 - 13` | 29 | | `0.3.2` | `7` | 30 | | `0.2.4` | `6` | 31 | 32 | Then add the `MultiWindowModule` to the imports array of your application module: 33 | 34 | ```typescript 35 | import {MultiWindowModule} from 'ngx-multi-window'; 36 | 37 | @NgModule({ 38 | imports: [ 39 | /* Other imports here */ 40 | MultiWindowModule 41 | ] 42 | }) 43 | export class AppModule { 44 | } 45 | ``` 46 | 47 | Finally, you need to specify how your application should load the ngx-multi-window library: 48 | 49 | ## Usage 50 | 51 | Inject the `MultiWindowService` into your component or service. 52 | 53 | ```typescript 54 | import {MultiWindowService} from 'ngx-multi-window'; 55 | 56 | export class AppComponent { 57 | constructor(private multiWindowService: MultiWindowService) { 58 | // use the service 59 | } 60 | } 61 | ``` 62 | 63 | ### Configuration 64 | 65 | You may inject a custom `MultiWindowConfig` object when importing the `MultiWindowModule` into your application. 66 | 67 | ```typescript 68 | @NgModule({ 69 | imports: [ 70 | ... 71 | MultiWindowModule.forRoot({ heartbeat: 542 }) 72 | ], 73 | }) 74 | ``` 75 | 76 | Check the description of the [MultiWindowConfig interface](https://github.com/Nolanus/ngx-multi-window/blob/master/projects/ngx-multi-window/src/lib/types/multi-window.config.ts) properties for options. 77 | The [default options](https://github.com/Nolanus/ngx-multi-window/blob/master/projects/ngx-multi-window/src/lib/providers/config.provider.ts#L6) are 78 | ```typescript 79 | { 80 | keyPrefix: 'ngxmw_', 81 | heartbeat: 1000, 82 | newWindowScan: 5000, 83 | messageTimeout: 10000, 84 | windowTimeout: 15000, 85 | windowSaveStrategy: WindowSaveStrategy.NONE, 86 | } 87 | ``` 88 | 89 | ### Window ID and name 90 | 91 | Every window has a unique, unchangeable id which can be accessed via `multiWindowService.id`. 92 | In addition to that every window as a changeable name which can be get/set 93 | via `multiWindowService.name`. 94 | 95 | ### Receive messages 96 | 97 | Receive messages addressed to the current window by subscribing to the observable returned from 98 | `multiWindowService.onMessages()`: 99 | 100 | ```typescript 101 | import { MultiWindowService, Message } from 'ngx-multi-window'; 102 | 103 | class App { 104 | constructor(private multiWindowService: MultiWindowService) { 105 | multiWindowService.onMessage().subscribe((value: Message) => { 106 | console.log('Received a message from ' + value.senderId + ': ' + value.data); 107 | }); 108 | } 109 | } 110 | ``` 111 | 112 | ### Send messages 113 | 114 | Send a message by calling `multiWindowService.sendMessage()`: 115 | 116 | ```typescript 117 | import { MultiWindowService, WindowData, Message } from 'ngx-multi-window'; 118 | 119 | class App { 120 | constructor(private multiWindowService: MultiWindowService) { 121 | const recipientId: string; // TODO 122 | const message: string; // TODO 123 | multiWindowService.sendMessage(recipientId, 'customEvent', message).subscribe( 124 | (messageId: string) => { 125 | console.log('Message send, ID is ' + messageId); 126 | }, 127 | (error) => { 128 | console.log('Message sending failed, error: ' + error); 129 | }, 130 | () => { 131 | console.log('Message successfully delivered'); 132 | }); 133 | } 134 | } 135 | ``` 136 | The message returns an observable which will resolve with a message id once the message has been send (= written to local storage). 137 | The receiving window will retrieve the message and respond with a `MessageType.MESSAGE_RECEIVED` typed message. 138 | The sending window/app will be informed by finishing the observable. 139 | 140 | In case no `MessageType.MESSAGE_RECEIVED` message has been received by the sending window 141 | within a certain time limit (`MultiWindowConfig.messageTimeout`, default is 10s) 142 | the message submission will be canceled. The observable will be rejected and the 143 | initial message will be removed from the current windows postbox. 144 | 145 | ### Other windows 146 | 147 | To get the names and ids of other window/app instances the `MultiWindowService` offers two methods: 148 | 149 | `multiWindowService.onWindows()` returns an observable to subscribe to in case you require periodic updates of the 150 | fellow windows. The observable will emit a new value every time the local storage has been scanned for the windows. 151 | This by default happens every 5 seconds (`MultiWindowConfig.newWindowScan`). 152 | 153 | Use `multiWindowService.getKnownWindows` to return an array of `WindowData`. 154 | 155 | ### New windows 156 | 157 | No special handling is necessary to open new windows. Every new window/app will register itself 158 | by writing to its key in the local storage. Existing windows will identify new windows 159 | after `MultiWindowConfig.newWindowScan` at the latest. 160 | 161 | The `MultiWindowService` offers a convenience method `newWindow()` which provides details for the 162 | new window's start url. If used the returned observable can be utilized to get notified 163 | once the new window is ready to consume/receive message. 164 | 165 | ### Save window name 166 | 167 | The library comes with a mechanism to save the window id using the browser's `window.name` property. This 168 | property is persisted on page reloads, resulting in the same tab/window running your angular application to keep 169 | the ngx-multi-window id even when reloading the page. 170 | Note: Only the window id is persisted, the customizable window name and messages are kept in the local storage, 171 | but are automatically rediscovered by the new window once it starts consuming messages. 172 | 173 | To save the window id, set the respective config property `nameSafeStrategy` to the desired value. Additionally 174 | one needs to call `saveWindow()` function e.g. during window unloading by attaching a `HostListener` in your 175 | main AppComponent. 176 | 177 | ```typescript 178 | @HostListener('window:unload') 179 | unloadHandler() { 180 | this.multiWindowService.saveWindow(); 181 | } 182 | ``` 183 | 184 | ## Communication strategy 185 | 186 | This library is based on "pull-based" communication. Every window periodically checks the local storage for messages addressed to itself. 187 | For that reason every window has its own key in the local storage, whose contents/value looks like: 188 | 189 | ```json 190 | {"heartbeat":1508936270103,"id":"oufi90mui5u5n","name":"AppWindow oufi90mui5u5n","messages":[]} 191 | ``` 192 | 193 | The heartbeat is updated every time the window performed a reading check on the other window's local storage keys. 194 | 195 | Sending message from sender A to recipient B involves the following steps: 196 | - The sender A writes the initial message (including the text and recipient id of B) into the "messages" array located at its own local storage key 197 | - The recipient window B reads the messages array of the other windows and picks up a message addressed to itself 198 | - B places a "MESSAGE_RECEIVED" message addressed to A in its own messages array 199 | - A picks up the "MESSAGE_RECEIVED" message in B's message array and removes the initial message from its own messages array 200 | - B identifies that the initial message has been removed from A's messages array and removes the receipt message from its own messages array 201 | 202 | ![Communication Strategy showcase](communication.gif) 203 | 204 | ## Example App 205 | 206 | This project contains a demo application that has been adapted to showcase the functionality of ngx-multi-window. 207 | Run the demo app by checking out that repository and execute the following command in the project root directory: 208 | 209 | ``` 210 | npm install 211 | ng serve 212 | ``` 213 | 214 | ## TODO 215 | 216 | - Tests and cross browser testing 217 | 218 | ## License 219 | 220 | [MIT](LICENSE) 221 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-multi-window-demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/ngx-multi-window-demo", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [], 29 | "aot": false, 30 | "vendorChunk": true, 31 | "extractLicenses": false, 32 | "buildOptimizer": false, 33 | "sourceMap": true, 34 | "optimization": false, 35 | "namedChunks": true 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "aot": true, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true 53 | } 54 | }, 55 | "defaultConfiguration": "" 56 | }, 57 | "serve": { 58 | "builder": "@angular-devkit/build-angular:dev-server", 59 | "options": { 60 | "browserTarget": "ngx-multi-window-demo:build" 61 | }, 62 | "configurations": { 63 | "production": { 64 | "browserTarget": "ngx-multi-window-demo:build:production" 65 | } 66 | } 67 | }, 68 | "extract-i18n": { 69 | "builder": "@angular-devkit/build-angular:extract-i18n", 70 | "options": { 71 | "browserTarget": "ngx-multi-window-demo:build" 72 | } 73 | }, 74 | "test": { 75 | "builder": "@angular-devkit/build-angular:karma", 76 | "options": { 77 | "main": "src/test.ts", 78 | "polyfills": "src/polyfills.ts", 79 | "tsConfig": "src/tsconfig.spec.json", 80 | "karmaConfig": "src/karma.conf.js", 81 | "styles": [ 82 | "src/styles.css" 83 | ], 84 | "scripts": [], 85 | "assets": [ 86 | "src/favicon.ico", 87 | "src/assets" 88 | ] 89 | } 90 | }, 91 | "lint": { 92 | "builder": "@angular-eslint/builder:lint", 93 | "options": { 94 | "lintFilePatterns": [ 95 | "src/**/*.ts", 96 | "src/**/*.html" 97 | ] 98 | } 99 | } 100 | } 101 | }, 102 | "ngx-multi-window-demo-e2e": { 103 | "root": "e2e/", 104 | "projectType": "application", 105 | "architect": { 106 | "e2e": { 107 | "builder": "@angular-devkit/build-angular:protractor", 108 | "options": { 109 | "protractorConfig": "e2e/protractor.conf.js", 110 | "devServerTarget": "ngx-multi-window-demo:serve" 111 | } 112 | } 113 | } 114 | }, 115 | "ngx-multi-window": { 116 | "root": "projects/ngx-multi-window", 117 | "sourceRoot": "projects/ngx-multi-window/src", 118 | "projectType": "library", 119 | "prefix": "lib", 120 | "architect": { 121 | "build": { 122 | "builder": "@angular-devkit/build-angular:ng-packagr", 123 | "options": { 124 | "tsConfig": "projects/ngx-multi-window/tsconfig.lib.json", 125 | "project": "projects/ngx-multi-window/ng-package.json" 126 | }, 127 | "configurations": { 128 | "production": { 129 | "project": "projects/ngx-multi-window/ng-package.prod.json" 130 | } 131 | } 132 | }, 133 | "test": { 134 | "builder": "@angular-devkit/build-angular:karma", 135 | "options": { 136 | "main": "projects/ngx-multi-window/src/test.ts", 137 | "tsConfig": "projects/ngx-multi-window/tsconfig.spec.json", 138 | "karmaConfig": "projects/ngx-multi-window/karma.conf.js" 139 | } 140 | }, 141 | "lint": { 142 | "builder": "@angular-eslint/builder:lint", 143 | "options": { 144 | "lintFilePatterns": [ 145 | "src/**/*.ts", 146 | "src/**/*.html" 147 | ] 148 | } 149 | } 150 | } 151 | } 152 | }, 153 | "schematics": { 154 | "@angular-eslint/schematics:application": { 155 | "setParserOptionsProject": true 156 | }, 157 | "@angular-eslint/schematics:library": { 158 | "setParserOptionsProject": true 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /communication.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nolanus/ngx-multi-window/4974c6321eb6d053cd36df801ddc3e226f8beb45/communication.gif -------------------------------------------------------------------------------- /custom_tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "cyclomatic-complexity": true, 4 | "newline-before-return": true, 5 | "no-consecutive-blank-lines": true, 6 | "no-irregular-whitespace": true, 7 | "object-literal-key-quotes": [ 8 | true, 9 | "as-needed" 10 | ], 11 | "only-arrow-functions": true, 12 | "prefer-method-signature": true, 13 | "trailing-comma": [ 14 | true, 15 | { 16 | "multiline": { 17 | "objects": "always", 18 | "arrays": "always", 19 | "functions": "never", 20 | "typeLiterals": "ignore" 21 | }, 22 | "singleline": "never", 23 | "esSpecCompliant": true 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # Exit with nonzero exit code if anything fails 3 | 4 | SOURCE_BRANCH="master" 5 | TARGET_BRANCH="gh-pages" 6 | 7 | # Pull requests and commits to other branches shouldn't try to deploy, just build to verify 8 | if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_BRANCH" != "$SOURCE_BRANCH" ]; then 9 | echo "Skipping deploy; Not correct branch or just a PR" 10 | exit 0 11 | fi 12 | 13 | # Save some useful information 14 | REPO=$(git config remote.origin.url) 15 | SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} 16 | SHA=$(git rev-parse --verify HEAD) 17 | 18 | # Clone the existing gh-pages for this repo into ghpages/ 19 | # Create a new empty branch if gh-pages doesn't exist yet (should only happen on first deploy) 20 | git clone $REPO ghpages 21 | cd ghpages 22 | git checkout $TARGET_BRANCH || git checkout --orphan $TARGET_BRANCH 23 | # Clean out existing contents (except for the .git dir) 24 | ls -A1 | grep -v .git | xargs rm -rf || exit 0 25 | 26 | cd .. 27 | 28 | # Copy over the build/compiled files from the demo app 29 | echo "Moving over the demo dist from $1" 30 | cp -a $1/. ghpages/ 31 | 32 | # Now let's go have some fun with the cloned repo 33 | echo "Go into ghpages folder" 34 | cd ghpages 35 | 36 | # Configure git 37 | echo "Configure git" 38 | git config user.name "Travis CI" 39 | git config user.email "$COMMIT_AUTHOR_EMAIL" 40 | 41 | echo "Check git diff status" 42 | # If there are no changes to the compiled out (e.g. this is a README update) then just bail. 43 | if [ $(git status --porcelain | wc -l) -lt 1 ]; then 44 | echo "No changes to the output on this push; exiting." 45 | exit 0 46 | fi 47 | 48 | # Commit the "changes", i.e. the new version. 49 | # The delta will show diffs between new and old versions. 50 | echo "Committing the changes" 51 | git add -A . 52 | git commit -m "Deploy to GitHub Pages: ${SHA}" 53 | 54 | echo "Doing ssl key stuff" 55 | # Get the deploy key by using Travis's stored variables to decrypt deploy_key.enc 56 | ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" 57 | ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" 58 | ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} 59 | ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} 60 | openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../deploy_key.enc -out deploy_key -d 61 | chmod 600 deploy_key 62 | eval $(ssh-agent -s) 63 | ssh-add deploy_key 64 | 65 | # Now that we're all set up, we can push. 66 | echo "Pushing to Github" 67 | git push $SSH_REPO $TARGET_BRANCH 68 | -------------------------------------------------------------------------------- /deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nolanus/ngx-multi-window/4974c6321eb6d053cd36df801ddc3e226f8beb45/deploy_key.enc -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | /* eslint-disable global-require */ 4 | 5 | const {SpecReporter} = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './src/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function () { 22 | } 23 | }, 24 | onPrepare() { 25 | require('ts-node').register({ 26 | project: require('path').join(__dirname, './tsconfig.e2e.json') 27 | }); 28 | jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}})); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-multi-window-demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "build:app": "ng build --configuration production --aot --baseHref=/ngx-multi-window/", 6 | "build:lib": "ng build ngx-multi-window --configuration production && npm run copy:readme", 7 | "copy:readme": "cpx README.md dist/ngx-multi-window", 8 | "commit": "git-cz", 9 | "deploy:app": "./deploy.sh dist/$npm_package_name/", 10 | "e2e": "ng e2e", 11 | "lint": "ng lint", 12 | "ng": "ng", 13 | "release:lib": "npm run lint && npm run build:lib && cd dist/ngx-multi-window && npm publish", 14 | "start": "ng serve", 15 | "test": "ng test ngx-multi-window", 16 | "test:ci": "ng test ngx-multi-window --watch=false" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "^16.1.5", 21 | "@angular/common": "^16.1.5", 22 | "@angular/compiler": "^16.1.5", 23 | "@angular/core": "^16.1.5", 24 | "@angular/forms": "^16.1.5", 25 | "@angular/platform-browser": "^16.1.5", 26 | "@angular/platform-browser-dynamic": "^16.1.5", 27 | "@angular/router": "^16.1.5", 28 | "rxjs": "~7.8.1", 29 | "tslib": "^2.6.0", 30 | "zone.js": "~0.13.1" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "^16.1.4", 34 | "@angular-eslint/builder": "16.1.0", 35 | "@angular-eslint/eslint-plugin": "16.1.0", 36 | "@angular-eslint/eslint-plugin-template": "16.1.0", 37 | "@angular-eslint/schematics": "16.1.0", 38 | "@angular-eslint/template-parser": "16.1.0", 39 | "@angular/cli": "^16.1.4", 40 | "@angular/compiler-cli": "^16.1.5", 41 | "@angular/language-service": "^16.1.5", 42 | "@commitlint/cli": "^17.6.6", 43 | "@commitlint/config-conventional": "^17.6.6", 44 | "@types/jasmine": "~4.3.5", 45 | "@types/jasminewd2": "^2.0.10", 46 | "@typescript-eslint/eslint-plugin": "^5.59.2", 47 | "@typescript-eslint/parser": "^5.59.2", 48 | "commitizen": "^4.3.0", 49 | "cpx": "^1.5.0", 50 | "cz-conventional-changelog": "^3.3.0", 51 | "eslint": "^8.39.0", 52 | "jasmine-core": "~5.0.1", 53 | "jasmine-spec-reporter": "~7.0.0", 54 | "karma": "~6.4.2", 55 | "karma-chrome-launcher": "~3.2.0", 56 | "karma-coverage": "~2.2.1", 57 | "karma-jasmine": "~5.1.0", 58 | "ng-packagr": "^16.1.0", 59 | "protractor": "~7.0.0", 60 | "ts-node": "^10.9.1", 61 | "typescript": "~5.1.6" 62 | }, 63 | "config": { 64 | "commitizen": { 65 | "path": "./node_modules/cz-conventional-changelog" 66 | } 67 | }, 68 | "commitlint": { 69 | "extends": [ 70 | "@commitlint/config-conventional" 71 | ] 72 | }, 73 | "husky": { 74 | "hooks": { 75 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.1 (2023-07-18) 2 | 3 | - Requires at least angular v15 and allows newer versions (tested with Angular 16.1.5) 4 | - Removed dev dependency "tsickle" as it is no longer compatible 5 | 6 | ## 0.6.0 (2022-12-27) 7 | 8 | - Requires angular v15 9 | 10 | ## 0.5.0 (2022-06-10) 11 | 12 | - Requires angular v14 13 | 14 | ## 0.4.1 (2022-06-10) 15 | 16 | - Build lib with angular v7 again to address pre v14 projects 17 | 18 | ## 0.4.0 (2022-06-08) 19 | 20 | - Fix deep import warning (closes [#81](https://github.com/Nolanus/ngx-multi-window/issues/35), [b6c3418](https://github.com/Nolanus/ngx-multi-window/commit/b6c34188e254a0aac7d3cd5944b8253ed5202383)) 21 | - Add support for angular 7 to 14 (closes [#76](https://github.com/Nolanus/ngx-multi-window/issues/76) and [#87](https://github.com/Nolanus/ngx-multi-window/issues/87), [b6c3418](https://github.com/Nolanus/ngx-multi-window/commit/b6c34188e254a0aac7d3cd5944b8253ed5202383)) 22 | 23 | ## 0.3.2 (2019-02-15) 24 | 25 | - Fix bug causing non `forRoot()` module imports to fail ([c988132](https://github.com/Nolanus/ngx-multi-window/commit/c98813297d3531917de5ddd7cbcccf070b68f3f5)) 26 | 27 | ## 0.3.1 (2018-11-21) 28 | 29 | - Fix problem with package published to npm 30 | 31 | ## 0.3.0 (2018-11-21) 32 | 33 | - Add support for angular 7 (closes [#35](https://github.com/Nolanus/ngx-multi-window/issues/35), [d7c7c5f](https://github.com/Nolanus/ngx-multi-window/commit/d7c7c5fcae64a7e2b3dd586ea87f187f426de27e)) 34 | - New feature of persisting the window id on reload (closes [#26](https://github.com/Nolanus/ngx-multi-window/issues/26), [cd6985b](https://github.com/Nolanus/ngx-multi-window/commit/cd6985b52c4e90e3e573fd7269fc5a02ba2a0331)) 35 | 36 | ## 0.2.4 (2018-11-07) 37 | 38 | - Export Config InjectionToken for tests (closes [#39](https://github.com/Nolanus/ngx-multi-window/issues/39), [cb78fee](https://github.com/Nolanus/ngx-multi-window/commit/cb78fee63ded35171b0c76d6859898cdd460098f)) 39 | 40 | ## 0.2.3 (2018-11-01) 41 | 42 | - Fix problem with config usage and AOT compilation ([9ec1f11](https://github.com/Nolanus/ngx-multi-window/commit/9ec1f11a0ec4d953ca7735a8c544583c717e270f)) 43 | 44 | ## 0.2.2 (2018-10-30) 45 | 46 | - Fix problem with "other" windows not being detected in IE ([#29](https://github.com/Nolanus/ngx-multi-window/issues/29), [#23](https://github.com/Nolanus/ngx-multi-window/issues/23), [be5fdc0](https://github.com/Nolanus/ngx-multi-window/commit/be5fdc04df6e686c5bc33438957a611ddf32ab50)) 47 | 48 | ## 0.2.1 (2018-10-30) 49 | 50 | - Fix problem with `this` reference in MultiWindowService 51 | 52 | ## 0.2.0 (2018-10-29) 53 | 54 | - Change build system to use angular cli library feature (special thanks to [Kay van Bree](https://github.com/kayvanbree)) 55 | - Change configuration to use `forRoot` method 56 | 57 | ## 0.1.3 (2018-06-06) 58 | 59 | ### Breaking Change 60 | 61 | - The library now requires angular version 6 62 | 63 | ## 0.1.2 (2017-11-07) 64 | 65 | ### Other 66 | 67 | - Update dependencies and supported angular version to 5 68 | 69 | ## 0.1.1 (2017-10-25) 70 | 71 | Initial release 72 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jan Kuri 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 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/README.md: -------------------------------------------------------------------------------- 1 | # This readme will be replaced by the other one 2 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | /* eslint-disable global-require */ 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 9 | plugins: [ 10 | require('karma-jasmine'), 11 | require('karma-chrome-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular-devkit/build-angular/plugins/karma') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, '../../coverage'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['progress', 'kjhtml'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: ['Chromium_no_sandbox'], 30 | customLaunchers: { 31 | Chromium_no_sandbox: { 32 | base: 'ChromiumHeadless', 33 | flags: ['--no-sandbox'] 34 | } 35 | }, 36 | singleRun: false, 37 | failOnEmptyTestSuite: false 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-multi-window", 4 | "deleteDestPath": false, 5 | "lib": { 6 | "entryFile": "src/index.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-multi-window", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-multi-window", 3 | "description": "Cross-window communication for multi-window angular applications", 4 | "version": "0.6.1", 5 | "author": "Sebastian Fuss ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/Nolanus/ngx-multi-window.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Nolanus/ngx-multi-window/issues" 13 | }, 14 | "homepage": "https://github.com/Nolanus/ngx-multi-window#readme", 15 | "dependencies": { 16 | "tslib": "^2.0.0" 17 | }, 18 | "peerDependencies": { 19 | "@angular/common": ">= 15.0.4", 20 | "@angular/core": ">= 15.0.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-multi-window 3 | */ 4 | 5 | export * from './lib/multi-window.module'; 6 | 7 | export * from './lib/providers/config.provider'; 8 | export * from './lib/providers/multi-window.service'; 9 | export * from './lib/providers/storage.service'; 10 | export * from './lib/providers/window.provider'; 11 | 12 | export * from './lib/types/message.type'; 13 | export * from './lib/types/multi-window.config'; 14 | export * from './lib/types/window-save-strategy.enum'; 15 | export * from './lib/types/window.type'; 16 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/multi-window.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ModuleWithProviders, NgModule } from '@angular/core'; 3 | 4 | import { MultiWindowService } from './providers/multi-window.service'; 5 | import { StorageService } from './providers/storage.service'; 6 | import { NGXMW_CONFIG } from './providers/config.provider'; 7 | import { MultiWindowConfig } from './types/multi-window.config'; 8 | import { WindowRef } from './providers/window.provider'; 9 | 10 | @NgModule({ 11 | imports: [CommonModule], 12 | providers: [ 13 | StorageService, 14 | MultiWindowService, 15 | WindowRef, 16 | {provide: NGXMW_CONFIG, useValue: {}}, 17 | ], 18 | }) 19 | export class MultiWindowModule { 20 | static forRoot(config?: MultiWindowConfig): ModuleWithProviders { 21 | return { 22 | ngModule: MultiWindowModule, 23 | providers: [MultiWindowService, {provide: NGXMW_CONFIG, useValue: config}], 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/providers/config.provider.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { MultiWindowConfig } from '../types/multi-window.config'; 3 | import { WindowSaveStrategy } from '../types/window-save-strategy.enum'; 4 | 5 | export const NGXMW_CONFIG = new InjectionToken('ngxmw_config'); 6 | 7 | export const defaultMultiWindowConfig: MultiWindowConfig = { 8 | keyPrefix: 'ngxmw_', 9 | heartbeat: 1000, 10 | newWindowScan: 5000, 11 | messageTimeout: 10000, 12 | windowTimeout: 15000, 13 | windowSaveStrategy: WindowSaveStrategy.NONE, 14 | }; 15 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/providers/multi-window.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@angular/core'; 2 | import { Location } from '@angular/common'; 3 | import { BehaviorSubject, Observable, Subject } from 'rxjs'; 4 | import { ignoreElements } from 'rxjs/operators'; 5 | 6 | import { StorageService } from './storage.service'; 7 | import { defaultMultiWindowConfig, NGXMW_CONFIG } from './config.provider'; 8 | import { MultiWindowConfig } from '../types/multi-window.config'; 9 | import { AppWindow, KnownAppWindow, WindowData } from '../types/window.type'; 10 | import { Message, MessageTemplate, MessageType } from '../types/message.type'; 11 | import { WindowRef } from '../providers/window.provider'; 12 | import { WindowSaveStrategy } from '../types/window-save-strategy.enum'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class MultiWindowService { 18 | 19 | private config: MultiWindowConfig; 20 | 21 | private myWindow: WindowData; 22 | 23 | private heartbeatId = null; 24 | private windowScanId = null; 25 | 26 | private knownWindows: KnownAppWindow[] = []; 27 | 28 | /** 29 | * A hash that keeps track of subjects for all send messages 30 | */ 31 | private messageTracker: { [key: string]: Subject } = {}; 32 | 33 | /** 34 | * A copy of the outbox that is regularly written to the local storage 35 | */ 36 | private outboxCache: { [key: string]: Message } = {}; 37 | 38 | /** 39 | * A subject to subscribe to in order to get notified about messages send to this window 40 | */ 41 | private messageSubject: Subject = new Subject(); 42 | 43 | /** 44 | * A subject to subscribe to in order to get notified about all known windows 45 | */ 46 | private windowSubject: Subject = new BehaviorSubject(this.knownWindows); 47 | 48 | private static generateId(): string { 49 | return new Date().getTime().toString(36).substr(-4) + Math.random().toString(36).substr(2, 9); 50 | } 51 | 52 | private tryMatchWindowKey(source: string): string { 53 | const nameRegex = new RegExp( 54 | // Escape any potential regex-specific chars in the keyPrefix which may be changed by the dev 55 | this.config.keyPrefix.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + 'w_([a-z0-9]+)' 56 | ); 57 | const match = source.match(nameRegex); 58 | if (match !== null) { 59 | return match[1]; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | private generatePayloadKey({messageId}: Message): string { 66 | return this.config.keyPrefix + 'payload_' + messageId; 67 | } 68 | 69 | private generateWindowKey(windowId: string): string { 70 | return this.config.keyPrefix + 'w_' + windowId; 71 | } 72 | 73 | private isWindowKey(key: string): boolean { 74 | return key.indexOf(this.config.keyPrefix + 'w_') === 0; 75 | } 76 | 77 | constructor(@Inject(NGXMW_CONFIG) customConfig: MultiWindowConfig, @Optional() private location: Location, 78 | private storageService: StorageService, private windowRef: WindowRef) { 79 | this.config = {...defaultMultiWindowConfig, ...customConfig}; 80 | 81 | let windowId: string; 82 | 83 | if (this.location) { 84 | // Try to extract the new window name from the location path 85 | windowId = this.tryMatchWindowKey(this.location.path(true)); 86 | } 87 | // Only check the name save strategy if no id has been extracted from the path already 88 | if (!windowId && this.config.windowSaveStrategy !== WindowSaveStrategy.NONE) { 89 | // There might be window data stored in the window.name property, try restoring it 90 | const storedWindowData = this.windowRef.nativeWindow.name; 91 | 92 | if (this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_BACKUP) { 93 | // There should be a JSON string in the window.name, try parsing it and set the values 94 | try { 95 | const storedJsonData = JSON.parse(storedWindowData); 96 | windowId = storedJsonData.ngxmw_id; 97 | this.windowRef.nativeWindow.name = storedJsonData.backup; 98 | } catch (ex) { // Swallow JSON parsing exceptions, as we can't handle them anyway 99 | } 100 | } else { 101 | windowId = this.tryMatchWindowKey(storedWindowData); 102 | if (this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_WHEN_EMPTY) { 103 | this.windowRef.nativeWindow.name = ''; 104 | } 105 | } 106 | } 107 | this.init(windowId); 108 | this.start(); 109 | } 110 | 111 | get name(): string { 112 | return this.myWindow.name; 113 | } 114 | 115 | set name(value: string) { 116 | this.myWindow.name = value; 117 | } 118 | 119 | get id(): string { 120 | return this.myWindow.id; 121 | } 122 | 123 | /** 124 | * An observable to subscribe to. It emits all messages the current window receives. 125 | * After a message has been emitted by this observable it is marked as read, so the sending window 126 | * gets informed about successful delivery. 127 | */ 128 | public onMessage(): Observable { 129 | return this.messageSubject.asObservable(); 130 | } 131 | 132 | /** 133 | * An observable to subscribe to. It emits the array of known windows 134 | * whenever it is read again from the localstorage. 135 | * 136 | * This Observable emits the last list of known windows on subscription 137 | * (refer to rxjs BehaviorSubject). 138 | * 139 | * Use {@link getKnownWindows} to get the current list of 140 | * known windows or if you only need a snapshot of that list. 141 | * 142 | * @see {@link MultiWindowConfig#newWindowScan} 143 | * @returns 144 | */ 145 | public onWindows(): Observable { 146 | return this.windowSubject.asObservable(); 147 | } 148 | 149 | /** 150 | * Get the latest list of known windows. 151 | * 152 | * Use {@link onWindows} to get an observable which emits 153 | * whenever is updated. 154 | */ 155 | public getKnownWindows() { 156 | return this.knownWindows; 157 | } 158 | 159 | /** 160 | * Start so that the current instance of the angular app/service/tab participates in the cross window communication. 161 | * It starts the interval-based checking for messages and updating the heartbeat. 162 | * 163 | * Note: There should be no need to call this method in production apps. It will automatically be called internally 164 | * during service construction (see {@link MultiWindowService} constructor) 165 | */ 166 | public start(): void { 167 | if (!this.heartbeatId) { 168 | this.heartbeatId = setInterval(this.heartbeat, this.config.heartbeat); 169 | } 170 | if (!this.windowScanId) { 171 | this.windowScanId = setInterval(this.scanForWindows, this.config.newWindowScan); 172 | } 173 | } 174 | 175 | /** 176 | * Stop the current instance of the angular app/service/tab from participating in the cross window communication. 177 | * It stops the interval-based checking for messages and updating the heartbeat. 178 | * 179 | * Note: There should be no need to call this method in production apps. 180 | */ 181 | public stop(): void { 182 | if (this.heartbeatId) { 183 | clearInterval(this.heartbeatId); 184 | this.heartbeatId = null; 185 | } 186 | if (this.windowScanId) { 187 | clearInterval(this.windowScanId); 188 | this.windowScanId = null; 189 | } 190 | } 191 | 192 | /** 193 | * Remove the current window representation from the localstorage. 194 | * 195 | * Note: Unless you {@link stop}ped the service the window representation will be 196 | * recreated after {@link MultiWindowConfig#heartbeat} milliseconds 197 | */ 198 | public clear(): void { 199 | this.storageService.removeLocalItem(this.generateWindowKey(this.myWindow.id)); 200 | } 201 | 202 | /** 203 | * Send a message to another window. 204 | * 205 | * @param recipientId The ID of the recipient window. 206 | * @param event Custom identifier to distinguish certain events 207 | * @param data Custom data to contain the data for the event 208 | * @param payload Further data to be passed to the recipient window via a separate entry in the localstorage 209 | * @returns An observable that emits the messageId once the message has been put into 210 | * the current windows outbox. It completes on successful delivery and fails if the delivery times out. 211 | */ 212 | public sendMessage(recipientId: string, event: string, data: any, payload?: any): Observable { 213 | const messageId = this.pushToOutbox({ 214 | recipientId, 215 | type: MessageType.MESSAGE, 216 | event, 217 | data, 218 | payload, 219 | }); 220 | this.messageTracker[messageId] = new Subject(); 221 | 222 | return this.messageTracker[messageId].asObservable(); 223 | } 224 | 225 | /** 226 | * Create a new window and get informed once it has been created. 227 | * 228 | * Note: The actual window "creation" of redirection needs to be performed by 229 | * the library user. This method only returns helpful information on how to 230 | * do that 231 | * 232 | * The returned object contains three properties: 233 | * 234 | * windowId: An id generated to be assigned to the newly created window 235 | * 236 | * urlString: This string must be included in the url the new window loads. 237 | * It does not matter whether it is in the path, query or hash part 238 | * 239 | * created: An observable that will complete after new window was opened and was able 240 | * to receive a message. If the window does not start consuming messages within 241 | * {@link MultiWindowConfig#messageTimeout}, this observable will fail, although 242 | * the window might become present after that. The Observable will never emit any elements. 243 | */ 244 | public newWindow(): { windowId: string, urlString: string, created: Observable } { 245 | if (this.location === null) { 246 | // Reading information from the URL is only possible with the Location provider. If 247 | // this window does not have one, another one will have none as well and thus would 248 | // not be able to read its new windowId from the url path 249 | throw new Error('No Location Provider present'); 250 | } 251 | const newWindowId = MultiWindowService.generateId(); 252 | 253 | const messageId = this.pushToOutbox({ 254 | recipientId: newWindowId, 255 | type: MessageType.PING, 256 | event: undefined, 257 | data: undefined, 258 | payload: undefined, 259 | }); 260 | 261 | this.messageTracker[messageId] = new Subject(); 262 | 263 | return { 264 | windowId: newWindowId, 265 | urlString: this.generateWindowKey(newWindowId), 266 | created: this.messageTracker[messageId].pipe(ignoreElements()), 267 | }; 268 | } 269 | 270 | public saveWindow(): void { 271 | if (this.config.windowSaveStrategy !== WindowSaveStrategy.NONE) { 272 | const windowId = this.generateWindowKey(this.id); 273 | if ((this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_WHEN_EMPTY && !this.windowRef.nativeWindow.name) 274 | || this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_FORCE) { 275 | this.windowRef.nativeWindow.name = windowId; 276 | } else if (this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_BACKUP) { 277 | this.windowRef.nativeWindow.name = JSON.stringify({ngxmw_id: windowId, backup: this.windowRef.nativeWindow.name}); 278 | } 279 | } 280 | } 281 | 282 | private init(windowId?: string): void { 283 | const windowKey = windowId 284 | ? this.generateWindowKey(windowId) 285 | : this.storageService.getWindowName(); 286 | let windowData: WindowData | null = null; 287 | if (windowKey && this.isWindowKey(windowKey)) { 288 | windowData = this.storageService.getLocalObject(windowKey); 289 | } 290 | 291 | if (windowData !== null) { 292 | // Restore window information from storage 293 | this.myWindow = { 294 | id: windowData.id, 295 | name: windowData.name, 296 | heartbeat: windowData.heartbeat, 297 | }; 298 | } else { 299 | const myWindowId = windowId || MultiWindowService.generateId(); 300 | this.myWindow = { 301 | id: myWindowId, 302 | name: 'AppWindow ' + myWindowId, 303 | heartbeat: -1, 304 | }; 305 | } 306 | 307 | this.storageService.setWindowName(windowKey); 308 | 309 | // Scan for already existing windows 310 | this.scanForWindows(); 311 | 312 | // Trigger heartbeat for the first time 313 | this.heartbeat(); 314 | } 315 | 316 | private pushToOutbox({recipientId, type, event, data, payload}: MessageTemplate, 317 | messageId: string = MultiWindowService.generateId()): string { 318 | if (recipientId === this.id) { 319 | throw new Error('Cannot send messages to self'); 320 | } 321 | this.outboxCache[messageId] = { 322 | messageId, 323 | recipientId, 324 | senderId: this.myWindow.id, 325 | sendTime: new Date().getTime(), 326 | type, 327 | event, 328 | data, 329 | payload, 330 | send: false, 331 | }; 332 | 333 | return messageId; 334 | } 335 | 336 | private heartbeat = () => { 337 | const now = new Date().getTime(); 338 | // Check whether there are new messages for the current window in the other window's outboxes 339 | 340 | // Store the ids of all messages we receive in this iteration 341 | const receivedMessages: string[] = []; 342 | 343 | this.knownWindows.forEach(windowData => { 344 | // Load the window from the localstorage 345 | const appWindow = this.storageService.getLocalObject(this.generateWindowKey(windowData.id)); 346 | 347 | if (appWindow === null) { 348 | // No window found, it possibly got closed/removed since our last scanForWindow 349 | return; 350 | } 351 | 352 | if (appWindow.id === this.myWindow.id) { 353 | // Ignore messages from myself (not done using Array.filter to reduce array iterations) 354 | // but check for proper last heartbeat time 355 | if (this.myWindow.heartbeat !== -1 && this.myWindow.heartbeat !== appWindow.heartbeat) { 356 | // The heartbeat value in the localstorage is a different one than the one we wrote into localstorage 357 | // during our last heartbeat. There are probably two app windows 358 | // using the same window id => change the current windows id 359 | this.myWindow.id = MultiWindowService.generateId(); 360 | // eslint-disable-next-line no-console 361 | console.warn('Window ' + appWindow.id + ' detected that there is probably another instance with' + 362 | ' this id, changed id to ' + this.myWindow.id); 363 | this.storageService.setWindowName(this.generateWindowKey(this.myWindow.id)); 364 | } 365 | 366 | return; 367 | } 368 | 369 | if (now - appWindow.heartbeat > this.config.windowTimeout) { 370 | // The window seems to be dead, remove the entry from the localstorage 371 | this.storageService.removeLocalItem(this.generateWindowKey(appWindow.id)); 372 | } 373 | 374 | // Update the windows name and heartbeat value in the list of known windows (that's what we iterate over) 375 | windowData.name = appWindow.name; 376 | windowData.heartbeat = appWindow.heartbeat; 377 | 378 | if (appWindow.messages && appWindow.messages.length > 0) { 379 | // This other window has messages, so iterate over the messages the other window has 380 | appWindow.messages.forEach(message => { 381 | if (message.recipientId !== this.myWindow.id) { 382 | // The message is not targeted to the current window 383 | return; 384 | } 385 | 386 | if (message.type === MessageType.MESSAGE_RECEIVED) { 387 | // It's a message to inform the current window that a previously sent message from the 388 | // current window has been processed by the recipient window 389 | 390 | // Trigger the observable to complete and then remove it 391 | if (this.messageTracker[message.messageId]) { 392 | this.messageTracker[message.messageId].complete(); 393 | } 394 | delete this.messageTracker[message.messageId]; 395 | 396 | // Remove the message from the outbox, as the transfer is complete 397 | delete this.outboxCache[message.messageId]; 398 | } else { 399 | receivedMessages.push(message.messageId); 400 | // Check whether we already processed that message. If that's the case we've got a 'message_received' 401 | // confirmation in our own outbox. 402 | 403 | if (!(this.outboxCache[message.messageId] && 404 | this.outboxCache[message.messageId].type === MessageType.MESSAGE_RECEIVED)) { 405 | // We did not process that message 406 | 407 | // Create a new message for the message sender in the current window's 408 | // outbox that the message has been processed (reuse the message id for that) 409 | this.pushToOutbox({ 410 | recipientId: message.senderId, 411 | type: MessageType.MESSAGE_RECEIVED, 412 | event: undefined, 413 | }, message.messageId); 414 | 415 | // Process it locally, unless it's just a PING message 416 | if (message.type !== MessageType.PING) { 417 | if (message.payload === true) { 418 | // The message has a separate payload 419 | const payloadKey = this.generatePayloadKey(message); 420 | message.payload = this.storageService.getLocalObject(payloadKey); 421 | this.storageService.removeLocalItem(payloadKey); 422 | } 423 | this.messageSubject.next(message); 424 | } 425 | } 426 | } 427 | }); 428 | } 429 | }); 430 | 431 | // Iterate over the outbox to clean it up, process timeouts and payloads 432 | Object.keys(this.outboxCache).forEach(messageId => { 433 | const message = this.outboxCache[messageId]; 434 | if (message.type === MessageType.MESSAGE_RECEIVED && !receivedMessages.some(msgId => msgId === messageId)) { 435 | // It's a message confirmation and we did not receive the 'original' method for that confirmation 436 | // => the sender has received our confirmation and removed the message from it's outbox, thus we can 437 | // safely remove the message confirmation as well 438 | delete this.outboxCache[messageId]; 439 | } else if (message.type !== MessageType.MESSAGE_RECEIVED && now - message.sendTime > this.config.messageTimeout) { 440 | // Delivering the message has failed, as the target window did not pick it up in time 441 | // The type of message doesn't matter for that 442 | delete this.outboxCache[messageId]; 443 | if (this.messageTracker[messageId]) { 444 | this.messageTracker[messageId].error('Timeout'); 445 | } 446 | delete this.messageTracker[messageId]; 447 | } else if (message.type === MessageType.MESSAGE && message.send === false) { 448 | if (message.payload !== undefined && message.payload !== true) { 449 | // Message has a payload that has not been moved yet, so move that in a separate localstorage key 450 | this.storageService.setLocalObject(this.generatePayloadKey(message), message.payload); 451 | message.payload = true; 452 | } 453 | this.messageTracker[message.messageId].next(message.messageId); 454 | // Set property to undefined, as we do not need to encode "send:true" in the localstorage json multiple times 455 | message.send = undefined; 456 | } 457 | }); 458 | 459 | this.storageService.setLocalObject(this.generateWindowKey(this.myWindow.id), { 460 | heartbeat: now, 461 | id: this.myWindow.id, 462 | name: this.myWindow.name, 463 | messages: Object.keys(this.outboxCache).map(key => this.outboxCache[key]), 464 | }); 465 | 466 | if (this.myWindow.heartbeat === -1) { 467 | // This was the first heartbeat run for the local window, so rescan for known windows to get 468 | // the current (new) window in the list 469 | this.scanForWindows(); 470 | } 471 | 472 | // Store the new heartbeat value in the local windowData copy 473 | this.myWindow.heartbeat = now; 474 | } 475 | 476 | private scanForWindows = () => { 477 | this.knownWindows = this.storageService.getLocalObjects( 478 | this.storageService.getLocalItemKeys().filter((key) => this.isWindowKey(key))) 479 | .map(({id, name, heartbeat}: WindowData) => { 480 | return { 481 | id, 482 | name, 483 | heartbeat, 484 | stalled: new Date().getTime() - heartbeat > this.config.heartbeat * 2, 485 | self: this.myWindow.id === id, 486 | }; 487 | }); 488 | this.windowSubject.next(this.knownWindows); 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/providers/storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional, SkipSelf } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class StorageService { 7 | 8 | private window: Window; 9 | private localStorage: Storage; 10 | private sessionStorage: Storage; 11 | 12 | constructor() { 13 | this.window = window; 14 | this.localStorage = window.localStorage; 15 | this.sessionStorage = window.sessionStorage; 16 | } 17 | 18 | /* 19 | Write methods 20 | */ 21 | public setLocalObject(key: string, obj: any): boolean { 22 | return this.setObject(this.localStorage, key, obj); 23 | } 24 | 25 | public setLocalItem(key: string, obj: string): void { 26 | this.setItem(this.localStorage, key, obj); 27 | } 28 | 29 | public setSessionObject(key: string, obj: any): boolean { 30 | return this.setObject(this.sessionStorage, key, obj); 31 | } 32 | 33 | public setSessionItem(key: string, obj: string): void { 34 | this.setItem(this.sessionStorage, key, obj); 35 | } 36 | 37 | private setObject(storage: Storage, key: string, obj: any): boolean { 38 | let jsonString: string; 39 | try { 40 | jsonString = JSON.stringify(obj); 41 | } catch (ex) { 42 | return false; 43 | } 44 | this.setItem(storage, key, jsonString); 45 | 46 | return true; 47 | } 48 | 49 | private setItem(storage: Storage, key: string, obj: string): void { 50 | storage.setItem(key, obj); 51 | } 52 | 53 | public setWindowName(value: string): void { 54 | this.window.name = value; 55 | } 56 | 57 | /* 58 | Read methods 59 | */ 60 | 61 | public getLocalObject(key: string): T | null { 62 | return this.getObject(this.localStorage, key); 63 | } 64 | 65 | public getLocalObjects(keys: string[]): (T | null)[] { 66 | return this.getObjects(this.localStorage, keys); 67 | } 68 | 69 | public getLocalItem(key: string): string | null { 70 | return this.getItem(this.localStorage, key); 71 | } 72 | 73 | public getSessionObject(key: string): T | null { 74 | return this.getObject(this.sessionStorage, key); 75 | } 76 | 77 | public getSessionObjects(keys: string[]): (T | null)[] { 78 | return this.getObjects(this.sessionStorage, keys); 79 | } 80 | 81 | public getSessionItem(key: string): string | null { 82 | return this.getItem(this.sessionStorage, key); 83 | } 84 | 85 | public getObjects(storage: Storage, keys: string[]): (T | null)[] { 86 | return keys.map(key => this.getObject(storage, key)); 87 | } 88 | 89 | private getObject(storage: Storage, key: string): T | null { 90 | const jsonString = this.getItem(storage, key); 91 | if (jsonString === null) { 92 | return null; 93 | } 94 | try { 95 | return JSON.parse(jsonString) as T; 96 | } catch (ex) { 97 | return null; 98 | } 99 | } 100 | 101 | private getItem(storage: Storage, key: string): string | null { 102 | return storage.getItem(key) || null; 103 | } 104 | 105 | public getWindowName(): string { 106 | return this.window.name; 107 | } 108 | 109 | /* 110 | Remove methods 111 | */ 112 | 113 | public removeLocalItem(key: string): void { 114 | this.removeItem(this.localStorage, key); 115 | } 116 | 117 | public removeSessionItem(key: string): void { 118 | this.removeItem(this.sessionStorage, key); 119 | } 120 | 121 | private removeItem(storage: Storage, key: string): void { 122 | storage.removeItem(key); 123 | } 124 | 125 | public clearLocalStorage(): void { 126 | this.clearStorage(this.localStorage); 127 | } 128 | 129 | public clearSessionStorage(): void { 130 | this.clearStorage(this.sessionStorage); 131 | } 132 | 133 | private clearStorage(storage: Storage): void { 134 | storage.clear(); 135 | } 136 | 137 | /* 138 | Inspection methods 139 | */ 140 | 141 | public getLocalItemKeys(): string[] { 142 | return this.getStorageItemKeys(this.localStorage); 143 | } 144 | 145 | public getSessionItemKeys(): string[] { 146 | return this.getStorageItemKeys(this.sessionStorage); 147 | } 148 | 149 | private getStorageItemKeys(storage: Storage): string[] { 150 | const keys = []; 151 | for (let i = 0; i < storage.length; i++) { 152 | keys.push(storage.key(i)); 153 | } 154 | 155 | return keys; 156 | } 157 | } 158 | 159 | /* singleton pattern taken from https://github.com/angular/angular/issues/13854 */ 160 | export const StorageServiceProviderFactory = (parentDispatcher: StorageService) => { 161 | return parentDispatcher || new StorageService(); 162 | }; 163 | 164 | export const StorageServiceProvider = { 165 | provide: StorageService, 166 | deps: [[new Optional(), new SkipSelf(), StorageService]], 167 | useFactory: StorageServiceProviderFactory, 168 | }; 169 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/providers/window.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | const _window: () => any = () => { 4 | // return the global native browser window object 5 | return window; 6 | }; 7 | 8 | @Injectable() 9 | export class WindowRef { 10 | get nativeWindow(): any { 11 | return _window(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/types/message.type.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | MESSAGE, 3 | MESSAGE_RECEIVED, 4 | PING, 5 | } 6 | 7 | export interface MessageTemplate { 8 | recipientId: string; 9 | type: MessageType; 10 | event: string; 11 | data?: any; 12 | payload?: any; 13 | } 14 | 15 | export interface Message extends MessageTemplate { 16 | send?: boolean; 17 | messageId: string; 18 | sendTime: number; 19 | senderId: string; 20 | } 21 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/types/multi-window.config.ts: -------------------------------------------------------------------------------- 1 | import { WindowSaveStrategy } from './window-save-strategy.enum'; 2 | /** 3 | * Object representing the configuration options for the MultiWindow Module 4 | */ 5 | export interface MultiWindowConfig { 6 | /** 7 | * String to be used as prefix when storing data in the localstorage 8 | */ 9 | keyPrefix?: string; 10 | 11 | /** 12 | * Time in milliseconds how often a heartbeat should be performed. During a heartbeat a window 13 | * looks for new messages from other windows and processes these messages. 14 | */ 15 | heartbeat?: number; 16 | 17 | /** 18 | * Time in milliseconds how often a scan for new windows should be performed. 19 | */ 20 | newWindowScan?: number; 21 | 22 | /** 23 | * Time in milliseconds after which a message delivery is considered to have failed. 24 | * If no "message_received" confirmation has arrived during the specified duration, 25 | * the message will be removed from the outbox. 26 | */ 27 | messageTimeout?: number; 28 | 29 | /** 30 | * Time in milliseconds after which a window is considered dead. 31 | * 32 | * Should be a multiple of {@link MultiWindowConfig#newWindowScan} 33 | */ 34 | windowTimeout?: number; 35 | 36 | /** 37 | * A strategy to be used when it comes to saving the window name. 38 | * 39 | * Refer to the different enum values for detailed description about the possible values. 40 | */ 41 | windowSaveStrategy?: WindowSaveStrategy; 42 | } 43 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/types/window-save-strategy.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A representation of a strategy on how to save the window id in the native window.name property. 3 | */ 4 | export enum WindowSaveStrategy { 5 | /** 6 | * Default behaviour. Window data will not be saved. 7 | */ 8 | NONE, 9 | /** 10 | * Only save the window data when the storage (native window.name property) is empty/undefined, meaning nothing else is 11 | * probably utilizing it 12 | */ 13 | SAVE_WHEN_EMPTY, 14 | /** 15 | * Save the window data without checking whether the storage might be used by another script 16 | */ 17 | SAVE_FORCE, 18 | /** 19 | * Save window data, but backup probable existing data in the storage and restore the original data after reading the window data. 20 | * Restoring the original data might not happen "on time" for the script using it, which would require delaying it's execution. 21 | */ 22 | SAVE_BACKUP, 23 | } 24 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/lib/types/window.type.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './message.type'; 2 | 3 | export interface WindowData { 4 | heartbeat: number; 5 | id: string; 6 | name: string; 7 | } 8 | 9 | export interface KnownAppWindow extends WindowData { 10 | stalled: boolean; 11 | self: boolean; 12 | } 13 | 14 | /** 15 | * Represents a window with the current application how it is stored 16 | * in the localstorage. It has an id, name, heartbeat represented in a timestamp 17 | * and an array of messages that that window sent out. 18 | * 19 | * It's named "AppWindow" to avoid confusion with the type Window 20 | * of the global variable "window". 21 | */ 22 | export interface AppWindow extends WindowData { 23 | messages: Message[]; 24 | } 25 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), { 16 | teardown: { destroyAfterEach: false } 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "types": [], 14 | "lib": [ 15 | "dom", 16 | "es2015" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "annotateForClosureCompiler": true, 21 | "skipTemplateCodegen": true, 22 | "strictMetadataEmit": true, 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true, 25 | "flatModuleId": "AUTOGENERATED", 26 | "flatModuleOutFile": "AUTOGENERATED", 27 | "compilationMode": "partial" 28 | }, 29 | "exclude": [ 30 | "src/test.ts", 31 | "**/*.spec.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /projects/ngx-multi-window/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nolanus/ngx-multi-window/4974c6321eb6d053cd36df801ddc3e226f8beb45/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 10 |

Angular Multi-Window

11 |
12 | 13 |
14 |

Talking Tabs!

15 |

Cross-Window/Tab communcation in angular for multi-window applications.

16 |
17 | 18 |
19 |
20 |

Communication Log

21 |
22 |
23 | 29 | 32 |
33 |
34 | 35 |
36 |
37 |

Current Window

38 |
39 |
40 |
41 | 42 | 44 |
45 |
46 | 47 | 49 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |

Registered Windows

60 |

The list of known windows will be refreshed on regular basis.

61 |

62 | 63 |

64 | 81 |
82 | 83 |
84 |

Send Message

85 |
86 |
87 | 88 | 89 | Select a window from the list of known windows 90 | 91 |
92 |
93 | 94 | 95 |
96 | 99 |
100 |
101 |
102 | 103 |
104 |

Angular Multi-Window library demo application

105 |
106 | 107 |
108 | 109 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed, waitForAsync} from '@angular/core/testing'; 2 | import {AppComponent} from './app.component'; 3 | import {NGXMW_CONFIG} from 'ngx-multi-window'; 4 | import {FormsModule} from '@angular/forms'; 5 | 6 | const multiWindowConfig = { 7 | heartbeat: 1000, 8 | newWindowScan: 1000, 9 | }; 10 | 11 | describe('AppComponent', () => { 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [ 15 | AppComponent, 16 | ], 17 | providers: [ 18 | {provide: NGXMW_CONFIG, useValue: multiWindowConfig}, 19 | ], 20 | imports: [ 21 | FormsModule, 22 | ], 23 | }).compileComponents(); 24 | })); 25 | 26 | it('should create the app', () => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | const app = fixture.debugElement.componentInstance; 29 | expect(app).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, OnInit } from '@angular/core'; 2 | import { MultiWindowService, Message, KnownAppWindow } from 'ngx-multi-window'; 3 | import { NameGeneratorService } from './providers/name-generator.service'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'], 9 | }) 10 | export class AppComponent implements OnInit { 11 | ownName: string; 12 | ownId: string; 13 | 14 | windows: KnownAppWindow[] = []; 15 | logs: string[] = []; 16 | 17 | newName: string; 18 | 19 | @HostListener('window:unload') 20 | unloadHandler() { 21 | this.multiWindowService.saveWindow(); 22 | } 23 | 24 | constructor(private multiWindowService: MultiWindowService, private nameGenerator: NameGeneratorService) { 25 | } 26 | 27 | ngOnInit(): void { 28 | this.ownId = this.multiWindowService.id; 29 | this.ownName = this.multiWindowService.name; 30 | if (this.ownName.indexOf(this.ownId) >= 0) { 31 | // This window still has the automatic given name, so generate a fake one for demo reasons 32 | // Generate a random name for the current window, just for fun 33 | this.multiWindowService.name = this.ownName = this.nameGenerator.getRandomFakeName(); 34 | } 35 | this.newName = this.ownName; 36 | this.windows = this.multiWindowService.getKnownWindows(); 37 | 38 | this.multiWindowService.onMessage().subscribe((value: Message) => { 39 | this.logs.unshift('Received a message from ' + value.senderId + ': ' + value.data); 40 | }); 41 | 42 | this.multiWindowService.onWindows().subscribe(knownWindows => this.windows = knownWindows); 43 | } 44 | 45 | public sendMessage(recipientId: string, message: string) { 46 | if (recipientId === this.ownId) { 47 | // Catch sending messages to itself. Trying to do so throws an error from multiWindowService.sendMessage() 48 | this.logs.unshift('Can\'t send messages to itself. Select another window.'); 49 | 50 | return; 51 | } 52 | this.multiWindowService.sendMessage(recipientId, 'customEvent', message).subscribe( 53 | (messageId: string) => { 54 | this.logs.unshift('Message send, ID is ' + messageId); 55 | }, 56 | (error) => { 57 | this.logs.unshift('Message sending failed, error: ' + error); 58 | }, 59 | () => { 60 | this.logs.unshift('Message successfully delivered'); 61 | }); 62 | } 63 | 64 | public removeLogMessage(index: number) { 65 | this.logs.splice(index, 1); 66 | } 67 | 68 | public changeName() { 69 | this.multiWindowService.name = this.ownName = this.newName; 70 | } 71 | 72 | public newWindow() { 73 | const newWindowData = this.multiWindowService.newWindow(); 74 | newWindowData.created.subscribe({ 75 | next: () => { 76 | }, 77 | error: (err) => { 78 | this.logs.unshift('An error occured while waiting for the new window to start consuming messages'); 79 | }, 80 | complete: () => { 81 | this.logs.unshift('The new window with id ' + newWindowData.windowId + ' got created and starts consuming messages'); 82 | } 83 | } 84 | ); 85 | window.open('?' + newWindowData.urlString); 86 | } 87 | 88 | public windowTrackerFunc(item, index) { 89 | return item.id; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { MultiWindowConfig, MultiWindowModule, WindowSaveStrategy } from 'ngx-multi-window'; 8 | 9 | const config: MultiWindowConfig = {windowSaveStrategy: WindowSaveStrategy.SAVE_WHEN_EMPTY}; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent, 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | FormsModule, 18 | MultiWindowModule.forRoot(config), 19 | ], 20 | providers: [Location, {provide: LocationStrategy, useClass: PathLocationStrategy}], 21 | bootstrap: [AppComponent], 22 | }) 23 | export class AppModule { 24 | } 25 | -------------------------------------------------------------------------------- /src/app/providers/name-generator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { NameGeneratorService } from './name-generator.service'; 4 | 5 | describe('NameGeneratorService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [NameGeneratorService], 9 | }); 10 | }); 11 | 12 | it('should be created', inject([NameGeneratorService], (service: NameGeneratorService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/providers/name-generator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class NameGeneratorService { 7 | 8 | constructor() { 9 | } 10 | 11 | private static name1 = ['Black', 'White', 'Gray', 'Brown', 'Red', 'Pink', 'Crimson', 'Carnelian', 'Orange', 'Yellow', 'Ivory', 'Cream', 'Green', 'Viridian', 'Aquamarine', 'Cyan', 'Blue', 'Cerulean', 'Azure', 'Indigo', 'Navy', 'Violet', 'Purple', 'Lavender', 'Magenta', 'Rainbow', 'Iridescent', 'Spectrum', 'Prism', 'Bold', 'Vivid', 'Pale', 'Clear', 'Glass', 'Translucent', 'Misty', 'Dark', 'Light', 'Gold', 'Silver', 'Copper', 'Bronze', 'Steel', 'Iron', 'Brass', 'Mercury', 'Zinc', 'Chrome', 'Platinum', 'Titanium', 'Nickel', 'Lead', 'Pewter', 'Rust', 'Metal', 'Stone', 'Quartz', 'Granite', 'Marble', 'Alabaster', 'Agate', 'Jasper', 'Pebble', 'Pyrite', 'Crystal', 'Geode', 'Obsidian', 'Mica', 'Flint', 'Sand', 'Gravel', 'Boulder', 'Basalt', 'Ruby', 'Beryl', 'Scarlet', 'Citrine', 'Sulpher', 'Topaz', 'Amber', 'Emerald', 'Malachite', 'Jade', 'Abalone', 'Lapis', 'Sapphire', 'Diamond', 'Peridot', 'Gem', 'Jewel', 'Bevel', 'Coral', 'Jet', 'Ebony', 'Wood', 'Tree', 'Cherry', 'Maple', 'Cedar', 'Branch', 'Bramble', 'Rowan', 'Ash', 'Fir', 'Pine', 'Cactus', 'Alder', 'Grove', 'Forest', 'Jungle', 'Palm', 'Bush', 'Mulberry', 'Juniper', 'Vine', 'Ivy', 'Rose', 'Lily', 'Tulip', 'Daffodil', 'Honeysuckle', 'Fuschia', 'Hazel', 'Walnut', 'Almond', 'Lime', 'Lemon', 'Apple', 'Blossom', 'Bloom', 'Crocus', 'Rose', 'Buttercup', 'Dandelion', 'Iris', 'Carnation', 'Fern', 'Root', 'Branch', 'Leaf', 'Seed', 'Flower', 'Petal', 'Pollen', 'Orchid', 'Mangrove', 'Cypress', 'Sequoia', 'Sage', 'Heather', 'Snapdragon', 'Daisy', 'Mountain', 'Hill', 'Alpine', 'Chestnut', 'Valley', 'Glacier', 'Forest', 'Grove', 'Glen', 'Tree', 'Thorn', 'Stump', 'Desert', 'Canyon', 'Dune', 'Oasis', 'Mirage', 'Well', 'Spring', 'Meadow', 'Field', 'Prairie', 'Grass', 'Tundra', 'Island', 'Shore', 'Sand', 'Shell', 'Surf', 'Wave', 'Foam', 'Tide', 'Lake', 'River', 'Brook', 'Stream', 'Pool', 'Pond', 'Sun', 'Sprinkle', 'Shade', 'Shadow', 'Rain', 'Cloud', 'Storm', 'Hail', 'Snow', 'Sleet', 'Thunder', 'Lightning', 'Wind', 'Hurricane', 'Typhoon', 'Dawn', 'Sunrise', 'Morning', 'Noon', 'Twilight', 'Evening', 'Sunset', 'Midnight', 'Night', 'Sky', 'Star', 'Stellar', 'Comet', 'Nebula', 'Quasar', 'Solar', 'Lunar', 'Planet', 'Meteor', 'Sprout', 'Pear', 'Plum', 'Kiwi', 'Berry', 'Apricot', 'Peach', 'Mango', 'Pineapple', 'Coconut', 'Olive', 'Ginger', 'Root', 'Plain', 'Fancy', 'Stripe', 'Spot', 'Speckle', 'Spangle', 'Ring', 'Band', 'Blaze', 'Paint', 'Pinto', 'Shade', 'Tabby', 'Brindle', 'Patch', 'Calico', 'Checker', 'Dot', 'Pattern', 'Glitter', 'Glimmer', 'Shimmer', 'Dull', 'Dust', 'Dirt', 'Glaze', 'Scratch', 'Quick', 'Swift', 'Fast', 'Slow', 'Clever', 'Fire', 'Flicker', 'Flash', 'Spark', 'Ember', 'Coal', 'Flame', 'Chocolate', 'Vanilla', 'Sugar', 'Spice', 'Cake', 'Pie', 'Cookie', 'Candy', 'Caramel', 'Spiral', 'Round', 'Jelly', 'Square', 'Narrow', 'Long', 'Short', 'Small', 'Tiny', 'Big', 'Giant', 'Great', 'Atom', 'Peppermint', 'Mint', 'Butter', 'Fringe', 'Rag', 'Quilt', 'Truth', 'Lie', 'Holy', 'Curse', 'Noble', 'Sly', 'Brave', 'Shy', 'Lava', 'Foul', 'Leather', 'Fantasy', 'Keen', 'Luminous', 'Feather', 'Sticky', 'Gossamer', 'Cotton', 'Rattle', 'Silk', 'Satin', 'Cord', 'Denim', 'Flannel', 'Plaid', 'Wool', 'Linen', 'Silent', 'Flax', 'Weak', 'Valiant', 'Fierce', 'Gentle', 'Rhinestone', 'Splash', 'North', 'South', 'East', 'West', 'Summer', 'Winter', 'Autumn', 'Spring', 'Season', 'Equinox', 'Solstice', 'Paper', 'Motley', 'Torch', 'Ballistic', 'Rampant', 'Shag', 'Freckle', 'Wild', 'Free', 'Chain', 'Sheer', 'Crazy', 'Mad', 'Candle', 'Ribbon', 'Lace', 'Notch', 'Wax', 'Shine', 'Shallow', 'Deep', 'Bubble', 'Harvest', 'Fluff', 'Venom', 'Boom', 'Slash', 'Rune', 'Cold', 'Quill', 'Love', 'Hate', 'Garnet', 'Zircon', 'Power', 'Bone', 'Void', 'Horn', 'Glory', 'Cyber', 'Nova', 'Hot', 'Helix', 'Cosmic', 'Quark', 'Quiver', 'Holly', 'Clover', 'Polar', 'Regal', 'Ripple', 'Ebony', 'Wheat', 'Phantom', 'Dew', 'Chisel', 'Crack', 'Chatter', 'Laser', 'Foil', 'Tin', 'Clever', 'Treasure', 'Maze', 'Twisty', 'Curly', 'Fortune', 'Fate', 'Destiny', 'Cute', 'Slime', 'Ink', 'Disco', 'Plume', 'Time', 'Psychadelic', 'Relic', 'Fossil', 'Water', 'Savage', 'Ancient', 'Rapid', 'Road', 'Trail', 'Stitch', 'Button', 'Bow', 'Nimble', 'Zest', 'Sour', 'Bitter', 'Phase', 'Fan', 'Frill', 'Plump', 'Pickle', 'Mud', 'Puddle', 'Pond', 'River', 'Spring', 'Stream', 'Battle', 'Arrow', 'Plume', 'Roan', 'Pitch', 'Tar', 'Cat', 'Dog', 'Horse', 'Lizard', 'Bird', 'Fish', 'Saber', 'Scythe', 'Sharp', 'Soft', 'Razor', 'Neon', 'Dandy', 'Weed', 'Swamp', 'Marsh', 'Bog', 'Peat', 'Moor', 'Muck', 'Mire', 'Grave', 'Fair', 'Just', 'Brick', 'Puzzle', 'Skitter', 'Prong', 'Fork', 'Dent', 'Dour', 'Warp', 'Luck', 'Coffee', 'Split', 'Chip', 'Hollow', 'Heavy', 'Legend', 'Hickory', 'Mesquite', 'Nettle', 'Rogue', 'Charm', 'Prickle', 'Bead', 'Sponge', 'Whip', 'Bald', 'Frost', 'Fog', 'Oil', 'Veil', 'Cliff', 'Volcano', 'Rift', 'Maze', 'Proud', 'Dew', 'Mirror', 'Shard', 'Salt', 'Pepper', 'Honey', 'Thread', 'Bristle', 'Ripple', 'Glow', 'Zenith']; 12 | private static name2 = ['head', 'crest', 'crown', 'tooth', 'fang', 'horn', 'frill', 'skull', 'bone', 'tongue', 'throat', 'voice', 'nose', 'snout', 'chin', 'eye', 'sight', 'seer', 'speaker', 'singer', 'song', 'chanter', 'howler', 'chatter', 'shrieker', 'shriek', 'jaw', 'bite', 'biter', 'neck', 'shoulder', 'fin', 'wing', 'arm', 'lifter', 'grasp', 'grabber', 'hand', 'paw', 'foot', 'finger', 'toe', 'thumb', 'talon', 'palm', 'touch', 'racer', 'runner', 'hoof', 'fly', 'flier', 'swoop', 'roar', 'hiss', 'hisser', 'snarl', 'dive', 'diver', 'rib', 'chest', 'back', 'ridge', 'leg', 'legs', 'tail', 'beak', 'walker', 'lasher', 'swisher', 'carver', 'kicker', 'roarer', 'crusher', 'spike', 'shaker', 'charger', 'hunter', 'weaver', 'crafter', 'binder', 'scribe', 'muse', 'snap', 'snapper', 'slayer', 'stalker', 'track', 'tracker', 'scar', 'scarer', 'fright', 'killer', 'death', 'doom', 'healer', 'saver', 'friend', 'foe', 'guardian', 'thunder', 'lightning', 'cloud', 'storm', 'forger', 'scale', 'hair', 'braid', 'nape', 'belly', 'thief', 'stealer', 'reaper', 'giver', 'taker', 'dancer', 'player', 'gambler', 'twister', 'turner', 'painter', 'dart', 'drifter', 'sting', 'stinger', 'venom', 'spur', 'ripper', 'swallow', 'devourer', 'knight', 'lady', 'lord', 'queen', 'king', 'master', 'mistress', 'prince', 'princess', 'duke', 'dutchess', 'samurai', 'ninja', 'knave', 'slave', 'servant', 'sage', 'wizard', 'witch', 'warlock', 'warrior', 'jester', 'paladin', 'bard', 'trader', 'sword', 'shield', 'knife', 'dagger', 'arrow', 'bow', 'fighter', 'bane', 'follower', 'leader', 'scourge', 'watcher', 'cat', 'panther', 'tiger', 'cougar', 'puma', 'jaguar', 'ocelot', 'lynx', 'lion', 'leopard', 'ferret', 'weasel', 'wolverine', 'bear', 'raccoon', 'dog', 'wolf', 'kitten', 'puppy', 'cub', 'fox', 'hound', 'terrier', 'coyote', 'hyena', 'jackal', 'pig', 'horse', 'donkey', 'stallion', 'mare', 'zebra', 'antelope', 'gazelle', 'deer', 'buffalo', 'bison', 'boar', 'elk', 'whale', 'dolphin', 'shark', 'fish', 'minnow', 'salmon', 'ray', 'fisher', 'otter', 'gull', 'duck', 'goose', 'crow', 'raven', 'bird', 'eagle', 'raptor', 'hawk', 'falcon', 'moose', 'heron', 'owl', 'stork', 'crane', 'sparrow', 'robin', 'parrot', 'cockatoo', 'carp', 'lizard', 'gecko', 'iguana', 'snake', 'python', 'viper', 'boa', 'condor', 'vulture', 'spider', 'fly', 'scorpion', 'heron', 'oriole', 'toucan', 'bee', 'wasp', 'hornet', 'rabbit', 'bunny', 'hare', 'brow', 'mustang', 'ox', 'piper', 'soarer', 'flasher', 'moth', 'mask', 'hide', 'hero', 'antler', 'chill', 'chiller', 'gem', 'ogre', 'myth', 'elf', 'fairy', 'pixie', 'dragon', 'griffin', 'unicorn', 'pegasus', 'sprite', 'fancier', 'chopper', 'slicer', 'skinner', 'butterfly', 'legend', 'wanderer', 'rover', 'raver', 'loon', 'lancer', 'glass', 'glazer', 'flame', 'crystal', 'lantern', 'lighter', 'cloak', 'bell', 'ringer', 'keeper', 'centaur', 'bolt', 'catcher', 'whimsey', 'quester', 'rat', 'mouse', 'serpent', 'wyrm', 'gargoyle', 'thorn', 'whip', 'rider', 'spirit', 'sentry', 'bat', 'beetle', 'burn', 'cowl', 'stone', 'gem', 'collar', 'mark', 'grin', 'scowl', 'spear', 'razor', 'edge', 'seeker', 'jay', 'ape', 'monkey', 'gorilla', 'koala', 'kangaroo', 'yak', 'sloth', 'ant', 'roach', 'weed', 'seed', 'eater', 'razor', 'shirt', 'face', 'goat', 'mind', 'shift', 'rider', 'face', 'mole', 'vole', 'pirate', 'llama', 'stag', 'bug', 'cap', 'boot', 'drop', 'hugger', 'sargent', 'snagglefoot', 'carpet', 'curtain']; 13 | 14 | private static capFirst(string) { 15 | return string.charAt(0).toUpperCase() + string.slice(1); 16 | } 17 | 18 | private static getRandomInt(min, max) { 19 | return Math.floor(Math.random() * (max - min)) + min; 20 | } 21 | 22 | public getRandomFakeName(): string { 23 | return NameGeneratorService.capFirst(NameGeneratorService.name1[NameGeneratorService.getRandomInt(0, NameGeneratorService.name1.length + 1)]) + ' ' + NameGeneratorService.capFirst(NameGeneratorService.name2[NameGeneratorService.getRandomInt(0, NameGeneratorService.name2.length + 1)]); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nolanus/ngx-multi-window/4974c6321eb6d053cd36df801ddc3e226f8beb45/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nolanus/ngx-multi-window/4974c6321eb6d053cd36df801ddc3e226f8beb45/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxMultiWindow 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | /* eslint-disable global-require */ 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 9 | plugins: [ 10 | require('karma-jasmine'), 11 | require('karma-chrome-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular-devkit/build-angular/plugins/karma') 15 | ], 16 | client: { 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, '../coverage'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true 23 | }, 24 | reporters: ['progress', 'kjhtml'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: ['Chrome'], 30 | singleRun: false 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | // eslint-disable-next-line no-console 12 | platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | /* You can add global styles to this file, and also import other style files */ 3 | /* Space out content a bit */ 4 | body { 5 | padding-top: 1.5rem; 6 | padding-bottom: 1.5rem; 7 | } 8 | 9 | /* Everything but the jumbotron gets side spacing for mobile first views */ 10 | .header, 11 | .footer { 12 | padding-right: 1rem; 13 | padding-left: 1rem; 14 | } 15 | 16 | /* Custom page header */ 17 | .header { 18 | padding-bottom: 1rem; 19 | border-bottom: .05rem solid #e5e5e5; 20 | } 21 | 22 | /* Make the masthead heading the same height as the navigation */ 23 | .header h3 { 24 | margin-top: 0; 25 | margin-bottom: 0; 26 | line-height: 3rem; 27 | } 28 | 29 | .log { 30 | max-height: 100px; 31 | overflow-y: scroll; 32 | } 33 | 34 | .list-group-item-action { 35 | cursor: pointer; 36 | } 37 | 38 | /* Custom page footer */ 39 | .footer { 40 | margin-top: 1.5rem; 41 | padding-top: 1.5rem; 42 | color: #777; 43 | border-top: .05rem solid #e5e5e5; 44 | } 45 | 46 | /* Main marketing message and sign up button */ 47 | .jumbotron { 48 | text-align: center; 49 | border-bottom: .05rem solid #e5e5e5; 50 | } 51 | 52 | .jumbotron .btn { 53 | padding: .75rem 1.5rem; 54 | font-size: 1.5rem; 55 | } 56 | 57 | /* Responsive: Portrait tablets and up */ 58 | @media screen and (min-width: 48em) { 59 | /* Remove the padding we set earlier */ 60 | .header, 61 | .footer { 62 | padding-right: 0; 63 | padding-left: 0; 64 | } 65 | 66 | /* Space out the masthead */ 67 | .header { 68 | margin-bottom: 2rem; 69 | } 70 | 71 | /* Remove the bottom border on the jumbotron for visual effect */ 72 | .jumbotron { 73 | border-bottom: 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "target": "ES2022", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "paths": { 20 | "ngx-multi-window": [ 21 | "projects/ngx-multi-window/src" 22 | ], 23 | "ngx-multi-window/*": [ 24 | "projects/ngx-multi-window/src/*" 25 | ] 26 | }, 27 | "useDefineForClassFields": false 28 | } 29 | } 30 | --------------------------------------------------------------------------------