├── .eslintrc.json ├── .github └── CONTRIBUTING.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── NOTICE ├── README.md ├── appveyor.yml ├── deploy-script.ps1 ├── docs ├── .gitignore ├── _config.yml ├── _includes │ └── navigation.html ├── _layouts │ ├── api.html │ └── default.html ├── cli.md ├── docs │ └── test-matrix.md ├── img │ ├── API Graphic.svg │ ├── Desktop Graphic.svg │ ├── Distribution Graphic.svg │ ├── GitHub-Mark-32px.png │ ├── GitHub-Mark-Light-32px.png │ ├── Hero Graphic.svg │ └── favicon.ico ├── index.md ├── js │ └── main.js ├── manifest.md └── styles │ └── main.css ├── lerna.json ├── package.json ├── packages ├── api-browser │ ├── .npmignore │ ├── LICENCE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── accessible-windows.ts │ │ ├── app.ts │ │ ├── browser-screen.ts │ │ ├── message-service.ts │ │ ├── notification.ts │ │ ├── screen.ts │ │ └── window.ts │ └── tsconfig.json ├── api-demo │ ├── .eslintrc.json │ ├── .npmignore │ ├── README.md │ ├── package.json │ └── src │ │ ├── app │ │ ├── app-api-demo.js │ │ └── app-api.html │ │ ├── bootstrap.min.css │ │ ├── index.html │ │ ├── messaging │ │ ├── messaging-api-demo.js │ │ └── messaging-api.html │ │ ├── notifications │ │ ├── notification-api.html │ │ ├── notification-demo.js │ │ ├── notification-icon.png │ │ └── notification-image.png │ │ ├── style.css │ │ └── window │ │ ├── window-api-demo.js │ │ └── window-api.html ├── api-electron │ ├── .gitattributes │ ├── .npmignore │ ├── LICENCE │ ├── README.md │ ├── bin │ │ └── ssf-electron │ ├── index.js │ ├── main.js │ ├── package.json │ ├── preload.ts │ ├── rollup.config.js │ ├── src │ │ ├── common │ │ │ └── constants.js │ │ └── preload │ │ │ ├── app.ts │ │ │ ├── message-service.ts │ │ │ ├── notification.ts │ │ │ ├── screen.ts │ │ │ └── window.ts │ └── tsconfig.json ├── api-openfin │ ├── .eslintrc.json │ ├── .npmignore │ ├── LICENCE │ ├── README.md │ ├── bin │ │ └── ssf-openfin │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── app.ts │ │ ├── index.ts │ │ ├── main-process.ts │ │ ├── message-service.ts │ │ ├── notification.html │ │ ├── notification.ts │ │ ├── screen.ts │ │ └── window.ts │ └── tsconfig.json ├── api-specification │ ├── .npmignore │ ├── LICENCE │ ├── README.md │ ├── interface │ │ ├── app.ts │ │ ├── event-emitter.ts │ │ ├── message-service.ts │ │ ├── notification.ts │ │ ├── openfin-extension.ts │ │ ├── position.ts │ │ ├── rectangle.ts │ │ ├── screen.ts │ │ ├── window-extension.ts │ │ └── window.ts │ ├── package.json │ └── transform-type-info.js ├── api-symphony-compatibility │ ├── .npmignore │ ├── LICENCE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── mapping.ts │ └── tsconfig.json ├── api-symphony-demo │ ├── package.json │ └── src │ │ └── app.json ├── api-tests │ ├── .eslintrc.json │ ├── LICENCE │ ├── README.md │ ├── demo │ │ ├── index.html │ │ ├── load-url-test.html │ │ └── test-image.png │ ├── generate-test-report.js │ ├── package.json │ ├── reporter.js │ └── test │ │ ├── browser-test-setup.js │ │ ├── electron-test-setup.js │ │ ├── messaging.spec.js │ │ ├── openfin-test-setup.js │ │ ├── setup.js │ │ ├── test-helpers.js │ │ ├── window.core.spec.js │ │ └── window.spec.js └── api-utility │ ├── .npmignore │ ├── LICENCE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── src │ ├── display.ts │ ├── emitter.ts │ └── uri.ts │ ├── test │ ├── display.test.ts │ ├── emitter.test.ts │ ├── globals.ts │ ├── tsconfig.json │ └── uri.test.ts │ └── tsconfig.json ├── test-script.ps1 ├── tslint.json └── validate-licenses.sh /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "standard", 4 | "rules": { 5 | "semi": ["error", "always"], 6 | "space-before-function-paren": ["error", "never"] 7 | }, 8 | "globals": { 9 | "ssf": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ContainerJS 2 | :+1: First off, thanks for taking the time to contribute! :+1: 3 | 4 | # Contributor License Agreement (CLA) 5 | A CLA is a document that specifies how a project is allowed to use your 6 | contribution; they are commonly used in many open source projects. 7 | 8 | **_All_ contributions to _all_ projects hosted by the 9 | [Symphony Software Foundation](https://symphony.foundation/) must be made with 10 | a [Foundation CLA](https://symphonyoss.atlassian.net/wiki/display/FM/Legal+Requirements#LegalRequirements-ContributorLicenseAgreement) 11 | in place, and there are [additional legal requirements](https://symphonyoss.atlassian.net/wiki/display/FM/Legal+Requirements) that must also be met.** 12 | 13 | As a result, PRs submitted to the ContainerJS project cannot be accepted until you have a CLA in place with the Foundation. 14 | 15 | # Contributing Issues 16 | 17 | ## Prerequisites 18 | 19 | * [ ] Have you [searched for duplicates](https://github.com/symphonyoss/containerjs/issues?utf8=%E2%9C%93&q=)? A simple search for exception error messages or a summary of the unexpected behaviour should suffice. 20 | * [ ] Are you running the [latest release of ContainerJS](https://github.com/symphonyoss/containerjs/commits/master)? 21 | 22 | ## Raising an Issue 23 | * Create your issue [here](https://github.com/symphonyoss/containerjs/issues/new). 24 | * New issues contain two templates in the description: bug report and enhancement request. Please pick the most appropriate for your issue, and delete the other. 25 | * Please also tag the new issue with either "Bug" or "Enhancement". 26 | * Please use [Markdown formatting](https://help.github.com/categories/writing-on-github/) 27 | liberally to assist in readability. 28 | * [Code fences](https://help.github.com/articles/creating-and-highlighting-code-blocks/) for exception stack traces and log entries, for example, massively improve readability. 29 | 30 | # Contributing Pull Requests (Code & Docs) 31 | To make review of PRs easier, please: 32 | 33 | * Please make sure your PRs will merge cleanly - PRs that don't are unlikely to be accepted. 34 | * For code contributions, follow the existing code layout. 35 | * For documentation contributions, follow the general structure, language, and tone of the [existing docs](https://github.com/symphonyoss/containerjs/wiki). 36 | * Keep PRs small and cohesive - if you have multiple contributions, please submit them as independent PRs. 37 | * Reference issue #s if your PR has anything to do with an issue (even if it doesn't address it). 38 | * Minimise non-functional changes (e.g. whitespace shenanigans). 39 | * Ensure all new files include a header comment block containing the [Apache License v2.0 and your copyright information](http://www.apache.org/licenses/LICENSE-2.0#apply). 40 | * If necessary (e.g. due to 3rd party dependency licensing requirements), update the [NOTICE file](https://github.com/symphonyoss/containerjs/blob/master/NOTICE) with any new attribution or other notices 41 | * If your contribution includes source code for any Category B-licensed dependencies, add an appropriate notice to this CONTRIBUTING file 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # nvm-windows npm directory 61 | npm 62 | 63 | /packages/api-tests/src/ 64 | /packages/api-specification/src/bootstrap.min.css 65 | build/ 66 | 67 | # Test output 68 | /packages/api-tests/coverage/ 69 | 70 | # Docs output 71 | /packages/api-specification/test-report.json 72 | /packages/api-specification/type-info.json 73 | 74 | # Visual Studio Code 75 | .vscode 76 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "7" 6 | script: 7 | - npm run ci 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ContainerJS - Symphony Software Foundation 2 | Copyright 2017 ScottLogic 3 | 4 | This product includes software developed at the Symphony Software Foundation (http://symphony.foundation). 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS 2 | [![Build Status](https://travis-ci.org/symphonyoss/ContainerJS.svg?branch=master)](https://travis-ci.org/symphonyoss/ContainerJS) 3 | [![Build Status](https://ci.appveyor.com/api/projects/status/v5u6x1hv81k4n8v7/branch/master?svg=true)](https://ci.appveyor.com/project/colineberhardt/containerjs) 4 | [![Symphony Software Foundation - Incubating](https://cdn.rawgit.com/symphonyoss/contrib-toolbox/master/images/ssf-badge-incubating.svg)](https://symphonyoss.atlassian.net/wiki/display/FM/Incubating) 5 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/ContainerJS/Lobby) 6 | 7 | Please visit the [ContainerJS website](https://symphonyoss.github.io/ContainerJS/) for information on getting started, and end-user API documentation. 8 | 9 | ## Development 10 | 11 | This project is a mono-repo, i.e. multiple distinct projects within the same Git repository. This project uses [Lerna](https://github.com/lerna/lerna) to manage the dependencies between these projects and their release process. 12 | 13 | To get started, run the following from the project root: 14 | 15 | ``` 16 | npm install 17 | npm run build 18 | ``` 19 | 20 | This will install Lerna and run `lerna bootstrap`, which runs `npm install` on all the sub-projects, and links any cross dependencies. 21 | 22 | If you want to see ContainerJS in action, the `api-demo` project has a fully-featured demo that can be run against various containers. 23 | 24 | The ContainerJS repo contains the following: 25 | 26 | - `api-specification` - the ContainerJS API specified in TypeScript. 27 | - `api-browser`, `api-electron`, `api-openfin` - various container-specific implementations of this API. 28 | - `api-tests` - a common suite of UI automation tests that exercise the API. 29 | - `api-demo` - a ContainerJS demo application. 30 | - `api-utility` - utility code that is common to the various containers. 31 | - `api-symphony-compatibility`, `api-symphony-demo` - an alternative ContainerJS API that is being debated via the [Symphony Foundation Desktop Wrapper Working Group](https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/Proposed+Standard+API+Specifications). 32 | 33 | ## Website Development 34 | 35 | The website can be found in the `docs` folder. It is a Jekyll site, which is hosted via GitHub. The API documentation is generated from the TypeScript interfaces within the `api-specification` package. To run this build execute the following: 36 | 37 | ``` 38 | npm run docs 39 | ``` 40 | 41 | ### View the Documentation locally 42 | 43 | Install [Ruby](https://www.ruby-lang.org/en/) 44 | 45 | Install Jekyll: 46 | 47 | ``` 48 | gem install jekyll 49 | ``` 50 | 51 | Run Jekyll from within the `docs` folder: 52 | 53 | ``` 54 | cd docs 55 | jekyll serve 56 | ``` 57 | 58 | ### Tests in Documentation 59 | 60 | The documentation also contains the results of the last test runs. To include the test output in the docs: 61 | 62 | Within the `api-test` package, 63 | ``` 64 | npm run test:ci 65 | ``` 66 | 67 | this will run the tests for the `browser`, `electron`, and `OpenFin`, and put the results into the `api-tests\coverage` folder. 68 | 69 | Next run 70 | ``` 71 | npm run report 72 | ``` 73 | 74 | This will generate the test files into the `api-specification` package. Now the test results will be built into the documentation with: 75 | 76 | ``` 77 | npm run docs 78 | ``` 79 | 80 | inside the `api-specification` package. 81 | 82 | ### Release 83 | 84 | To prepare for a release, you will need a fresh clone of the `master` branch of `symphonyoss/ContainerJS`. 85 | 86 | The release process itself is handled by [lerna](https://lernajs.io/), using the independent versioning setting. 87 | 88 | To build the packages in preparation for release: 89 | 90 | ```shell-script 91 | npm install 92 | npm run build 93 | ``` 94 | 95 | To actually release the packages to `npm`, you need an npm account that's a collaborator on the containerjs packages, logged into `npm` on the command line: 96 | 97 | ```shell-script 98 | npm login 99 | ``` 100 | 101 | If you want to test the packages locally, before publishing, you can [npm pack](https://docs.npmjs.com/cli/pack) them, then [npm install](https://docs.npmjs.com/cli/install) into a project from the resulting archive file. 102 | 103 | To start the release process, run: 104 | 105 | ```shell-script 106 | npm run publish 107 | ``` 108 | 109 | and follow the instructions. This will publish each version to `npm`, and then create a commit with the commit message "Publish", tagged with each version that has been published. This commit _needs_ to be pushed directly to `master`, as this combination of commit and tags underpins `lerna`'s release process. 110 | 111 | Check that the packages are showing correctly on `npm`, then push this commit and tags: 112 | 113 | ```shell-script 114 | git push --tags 115 | ``` 116 | 117 | Note that this is a _direct_ commit to `master`, not a PR. This is necessary for two reasons. Firstly because `lerna` relies on the tags that it generates to be reachable from `master`. As tags are not transferred by a PR, this leaves orphaned tags that cause `lerna` to fail. Secondly, by the time the commit has been made, the packages have already been published to `npm`, so a PR holds no real value, and can actually cause more problems with the release if it isn't merged in a timely manner. `master` is currently protected, so as a workaround it requires temporarily removing the admin restriction on pushing to allow a `git push` of the commit. -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | - x64 3 | 4 | environment: 5 | nodejs_version: "7.9.0" 6 | access_token: 7 | secure: j+oUaTR28O5BggYTM09pCva2UHkECRwG0zsMBrhg95oMnC/n18asKUHuVJMG6sQA 8 | github_email: 9 | secure: zfTP/Yo202a1DXy7wNXFB/1QEY+cHW8l5hGJlq8PT7M= 10 | 11 | cache: 12 | - '%LOCALAPPDATA%\OpenFin\runtime' 13 | 14 | install: 15 | - choco install googlechrome 16 | - ps: Install-Product node 17 | - set CI=true 18 | - appveyor-retry npm install 19 | - npm build 20 | 21 | test_script: 22 | - node --version 23 | - npm --version 24 | - npm run ci 25 | - ps: .\test-script.ps1 26 | 27 | 28 | deploy_script: 29 | - ps: .\deploy-script.ps1 30 | 31 | build: off 32 | -------------------------------------------------------------------------------- /deploy-script.ps1: -------------------------------------------------------------------------------- 1 | IF ($env:APPVEYOR_REPO_BRANCH -eq "master" -And (-Not (Test-Path Env:\APPVEYOR_PULL_REQUEST_NUMBER))) { 2 | Write-Host "Publishing docs to gh-pages" 3 | git config --global credential.helper store 4 | Add-Content "$env:USERPROFILE\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n" 5 | git config --global user.email $env:github_email 6 | git config --global user.name "ColinEberhardt" 7 | git add -f docs 8 | git commit -m "Update gh-pages: $env:APPVEYOR_REPO_COMMIT_MESSAGE" 9 | git subtree split --prefix docs -b gh-pages 10 | git push -f origin gh-pages:gh-pages 11 | IF ($LASTEXITCODE -ne "0") { 12 | Write-Warning -Message 'Deploy Failed' 13 | } 14 | } 15 | Else { 16 | Write-Host "Not on branch 'master', not publishing docs" 17 | } 18 | 19 | # Fail if the tests failed 20 | exit [Environment]::GetEnvironmentVariable("TestResult", "User") 21 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # this file is generated by the api-specifications build 2 | Docs.html 3 | test-matrix.md 4 | 5 | # this folder is generated by local jekyll builds 6 | _site 7 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | baseurl: '/ContainerJS' 2 | -------------------------------------------------------------------------------- /docs/_includes/navigation.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /docs/_layouts/api.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 |
6 |
    7 | 8 |
      9 |
    10 | 11 |
      12 |
    13 | 14 |
      15 |
    16 |
17 |
18 |
19 |

Documentation

20 | {{ content }} 21 |
22 |
23 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ContainerJS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | {% include navigation.html %} 21 | {% if page.id == 'home' %} 22 |
23 |
24 |
25 |

Web apps don't just run in browsers.

26 |

27 | Target OpenFin, Electron and other containers from a single codebase. This project contains the current work-in-progress 28 | Symphony Desktop Wrapper. The goal of this project is to provide a common API across multiple HTML5 containers. 29 |

30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 |
Unified API
43 |

ContainerJS provides an abstraction layer, rather than using the underlying container APIs (OpenFin, Electron), you use the ContainerJS APIs.

44 |
45 |
46 | 47 |
Desktop Intergration
48 |

Desktop containers allow HTML5 application to run outside of the browser, allowing a deeper, more intergrated experience.

49 |
50 |
51 | 52 |
Bootstrapping & Distribution
53 |

ContainerJS provides a uniform mechanism for distributing applications, with web-based distribution and an application manifest.

54 |
55 |
56 | 57 |
58 | {% endif %} 59 | 60 | {{ content }} 61 | 62 | 63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cli 3 | layout: default 4 | sectionid: cli 5 | --- 6 | 7 | ## ContainerJS Command Line Interface 8 | 9 | ContainerJS has a command line interface for running an application with OpenFin (`ssf-openfin`) or Electron (`ssf-electron`). 10 | 11 | #### OpenFin CLI 12 | 13 | {% highlight shell_session %} 14 | $ npm install --global containerjs-api-openfin 15 | 16 | Usage: ssf-openfin [options] 17 | 18 | Options: 19 | 20 | -V, --version output the version number 21 | -u, --url [url] Launch url for the application (can be specified in --config instead) 22 | -c, --config [filename] (Optional) ContainerJS config file 23 | -s, --symphony (Optional) Use Symphony compatibility layer 24 | -d, --developer (Optional) Show developer context menu 25 | -o, --output-config [filename] (Optional) Where to output the OpenFin config file 26 | -C, --config-url [url] (Optional) Url to read the new app.json file from to start OpenFin 27 | -f, --openfin-version [version] (Optional) Version of the OpenFin runtime to use, default is stable 28 | -n, --notification [directory] (Optional) Generate an example notification file in the specified directory 29 | -h, --help output usage information 30 | {% endhighlight %} 31 | 32 | #### Electron CLI 33 | 34 | {% highlight shell_session %} 35 | $ npm install --global containerjs-api-electron 36 | 37 | Usage: ssf-electron [options] 38 | 39 | Options: 40 | 41 | -V, --version output the version number 42 | -u, --url [url] Launch url for the application (can be specified in --config instead) 43 | -c, --config [filename] (Optional) ContainerJS config file 44 | -s, --symphony (Optional) Use Symphony compatibility layer 45 | -d, --developer (Optional) Show developer menu 46 | -h, --help output usage information 47 | {% endhighlight %} 48 | -------------------------------------------------------------------------------- /docs/img/API Graphic.svg: -------------------------------------------------------------------------------- 1 | API GraphicCreated with Sketch. -------------------------------------------------------------------------------- /docs/img/Desktop Graphic.svg: -------------------------------------------------------------------------------- 1 | Desktop GraphicCreated with Sketch. -------------------------------------------------------------------------------- /docs/img/Distribution Graphic.svg: -------------------------------------------------------------------------------- 1 | Distribution GraphicCreated with Sketch. -------------------------------------------------------------------------------- /docs/img/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symphonyoss/ContainerJS/38578c4bc77417e88ed44da8b3eae71f1e1f2317/docs/img/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /docs/img/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symphonyoss/ContainerJS/38578c4bc77417e88ed44da8b3eae71f1e1f2317/docs/img/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symphonyoss/ContainerJS/38578c4bc77417e88ed44da8b3eae71f1e1f2317/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: home 3 | layout: default 4 | sectionid: home 5 | --- 6 | 7 | ## Project Status 8 | 9 | ContainerJS is currently under active development, with frequent breaking changes! 10 | 11 | ## Getting Started 12 | 13 | The following describes how to create a simple 'Hello World' application and run it with Electron, OpenFin and a Browser. 14 | 15 | #### Creating the Hello World app 16 | 17 | From within an empty folder, add a minimal HTML file called `index.html`: 18 | 19 | {% highlight html %} 20 | 21 | 22 | 23 |

Hello World!

24 |
25 | ContainerJS status: initialising 26 |
27 | 28 | 29 | 30 | 37 | 38 | {% endhighlight %} 39 | 40 | The above displays a simple welcome message and indicates the status of the ContainerJS APIs. The `containerjs-api.js` is the browser API, that will create the ContainerJS API if no API currently exists. 41 | 42 | This file uses the `ssf` API to handle the container `ready` promise, updating the status text when this lifecycle event occurs. 43 | 44 | #### Starting a local server 45 | 46 | The container loads HTML applications over HTTP, so in order to run this demo application you need to start a local server. If you don't already have a preferred tool, you can use the node `http-server` package: 47 | 48 | {% highlight shell_session %} 49 | $ npm install --global http-server 50 | $ http-server -p 8080 51 | {% endhighlight %} 52 | 53 | This starts a server on port 8080. 54 | 55 | #### Running with OpenFin 56 | 57 | The ContainerJS project provides an OpenFin-based command line tool, which can be installed as follows: 58 | 59 | {% highlight shell_session %} 60 | $ npm install --global containerjs-api-openfin 61 | {% endhighlight %} 62 | 63 | To run your simple 'Hello World' application from within an OpenFin container, execute the following: 64 | 65 | {% highlight shell_session %} 66 | $ ssf-openfin --url http://localhost:8080/index.html 67 | {% endhighlight %} 68 | 69 | You should now see the Hello World application, and see the status update to 'ready'. _Note:_ The notification API 70 | will not work unless a `notification.html` is hosted on a server. An example notification file can be generated by passing 71 | `--notification [outdir]` to `ssf-openfin`. 72 | 73 | For more advanced control of the OpenFin application and initial window, you can use an app.json manifest file. 74 | See [Manifest Files](manifest) for more details. 75 | 76 | For details of the ssf-electron CLI, see [Command Line Interface](cli). 77 | 78 | #### Running with Electron 79 | 80 | The ContainerJS project provides an Electron-based command line tool, which can be installed as follows: 81 | 82 | {% highlight shell_session %} 83 | $ npm install --global containerjs-api-electron 84 | {% endhighlight %} 85 | 86 | From within your 'Hello World' folder, execute the following: 87 | 88 | {% highlight shell_session %} 89 | $ ssf-electron --url http://localhost:8080/index.html 90 | {% endhighlight %} 91 | 92 | You should now see exactly the same app running within Electron. 93 | 94 | For more advanced control of the Electron application and initial window, you can use an app.json manifest file. 95 | See [Manifest Files](manifest) for more details. 96 | 97 | For details of the ssf-electron CLI, see [Command Line Interface](cli). 98 | 99 | #### Browser 100 | 101 | To run your app within a browser, simply navigate to the URL `http://localhost:8080/index.html`. 102 | -------------------------------------------------------------------------------- /docs/js/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (window.location.pathname.split('/').filter(x => x).pop().toLowerCase() === 'docs.html') { 3 | const classMenu = document.getElementById('class-menu'); 4 | const interfaceMenu = document.getElementById('interface-menu'); 5 | const eventMenu = document.getElementById('event-menu'); 6 | const classSections = document.getElementsByClassName('docs-title'); 7 | for (let i = 0; i < classSections.length; i++) { 8 | const id = classSections[i].id; 9 | const name = classSections[i].firstChild.innerText; 10 | let menu = classMenu; 11 | if (id.includes('-interface')) { 12 | menu = interfaceMenu; 13 | } else if (id.includes('-event')) { 14 | menu = eventMenu; 15 | } 16 | 17 | const li = document.createElement('li'); 18 | li.setAttribute('class', 'list-item'); 19 | 20 | const link = document.createElement('a'); 21 | link.setAttribute('class', 'nav-link'); 22 | link.setAttribute('href', '#' + id); 23 | link.text = name; 24 | 25 | li.appendChild(link); 26 | menu.appendChild(li); 27 | } 28 | 29 | document.querySelectorAll('.test-results') 30 | .forEach(testElement => { 31 | testElement.addEventListener('click', () => { 32 | const detailClass = 'test-detail'; 33 | if (testElement.classList.contains(detailClass)) { 34 | testElement.classList.remove(detailClass); 35 | } else { 36 | testElement.classList.add(detailClass); 37 | } 38 | }); 39 | }); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /docs/manifest.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: manifest 3 | layout: default 4 | sectionid: manifest 5 | --- 6 | 7 | ## Application Manifest 8 | 9 | The ssf-openfin and ssf-electron [Command Line Interface](cli) tools can customise the 10 | initial application window using an application manifest file. 11 | 12 | #### Simple manifest file 13 | 14 | Within the same folder as your website, add the following `app.json` manifest file: 15 | 16 | {% highlight json %} 17 | { 18 | "url": "http://localhost:8080/index.html", 19 | "defaultWidth": 600, 20 | "defaultHeight": 600 21 | } 22 | {% endhighlight %} 23 | 24 | The manifest tells the container which URL to load initially, and specifies the initial window size. 25 | 26 | #### Manifest options 27 | 28 | The following manifest options are supported for both Electron and Openfin 29 | 30 | {% highlight javascript %} 31 | { 32 | "url": "http://localhost:8080/index.html", // Initial application URL 33 | "autoShow": true, // Whether to show the main window automatically 34 | "defaultWidth": 600, // Default window width 35 | "defaultHeight": 600, // Default window height 36 | "minWidth": 100, // Minimum window width 37 | "minHeight": 100, // Minimum window height 38 | "maxWidth": 1000, // Maximum window width 39 | "maxHeight": 800, // Maximum window height 40 | } 41 | {% endhighlight %} 42 | 43 | In addition to the above settings, OpenFin also supports all options from the `startup_app` 44 | section of the OpenFin [Application Config](https://openfin.co/application-config/) file. 45 | -------------------------------------------------------------------------------- /docs/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #FCFCFC; 3 | padding-top: 65px; 4 | position: relative; 5 | overflow: visible; 6 | font-family: 'Roboto', sans-serif; 7 | font-size: 16px; 8 | } 9 | 10 | h1, h2 { 11 | color: #5A5886; 12 | font-weight: 700; 13 | } 14 | 15 | h1 { 16 | padding: 0px; 17 | margin: 0px; 18 | } 19 | 20 | h2 { 21 | padding: 0px; 22 | margin: 24px 0px; 23 | font-size: 28px; 24 | } 25 | 26 | h3 { 27 | margin-bottom: 24px; 28 | margin-top: 32px; 29 | font-size: 24px; 30 | font-weight: 700; 31 | } 32 | 33 | h4 { 34 | margin-top: 32px; 35 | } 36 | 37 | section > h4 { 38 | margin: 0px; 39 | display: inline-block; 40 | font-weight: 700; 41 | font-size: 20px; 42 | margin-bottom: 10px; 43 | } 44 | 45 | h5 { 46 | padding: 10px 0px; 47 | margin: 0px; 48 | font-weight: 700; 49 | font-size: 16px; 50 | } 51 | 52 | p { 53 | margin-bottom: 5px; 54 | } 55 | 56 | br { 57 | line-height: 35px; 58 | } 59 | 60 | .lead { 61 | margin-top: 12px; 62 | } 63 | 64 | .code, pre, .code-small { 65 | font-family: monospace; 66 | white-space: pre; 67 | background-color: #eee; 68 | display: inline-block; 69 | padding: 10px 10px; 70 | margin: 5px 0; 71 | font-weight: 500; 72 | font-size: 90%; 73 | } 74 | 75 | .code, pre { 76 | width: 100%; 77 | } 78 | pre .hljs { 79 | background-color: transparent; 80 | padding: 0; 81 | } 82 | pre .hljs .nx { 83 | color: #5A5886; 84 | } 85 | pre .hljs .hljs-built_in { 86 | color: #A0A039; 87 | } 88 | pre .hljs .hljs-comment { 89 | color: #39A000; 90 | } 91 | 92 | .method { 93 | padding-bottom: 32px; 94 | position: relative; 95 | } 96 | 97 | .property { 98 | padding-bottom: 32px; 99 | position: relative; 100 | } 101 | 102 | .navbar-light .navbar-nav .nav-link, .tagline-header, .navbar-light .navbar-brand, .navbar-light .navbar-toggler { 103 | color: #5A5886; 104 | } 105 | 106 | .navbar-brand { 107 | font-weight: 800; 108 | font-size: 20px; 109 | font-family: 'Open Sans', sans-serif; 110 | } 111 | 112 | .jumbotron { 113 | background-color: transparent; 114 | padding: 32px 0px; 115 | } 116 | 117 | #main-image { 118 | width: 100%; 119 | padding: 20px; 120 | margin-top: 20px; 121 | } 122 | 123 | .navbar { 124 | background-color: #FCFCFC; 125 | position: fixed !important; 126 | top: 0px; 127 | width: inherit; 128 | padding: 8px 0px !important; 129 | z-index: 1000; 130 | clear: both; 131 | } 132 | 133 | #nav-container { 134 | margin: 0px; 135 | padding: 0px; 136 | } 137 | 138 | .list-item { 139 | display: block; 140 | margin: 10px; 141 | padding: 3px; 142 | padding-left: 10px; 143 | border-left: 3px solid transparent; 144 | } 145 | 146 | .nav-link { 147 | color: #AAA6A6; 148 | margin-left: 32px; 149 | } 150 | 151 | .nav-link:hover { 152 | color: #5A5886; 153 | } 154 | 155 | .nav-link.active { 156 | color: #5A5886; 157 | border-left: 3px solid #5A5886; 158 | } 159 | 160 | .nav-brand { 161 | width: 25px; 162 | height: 25px; 163 | } 164 | 165 | #docs-menu { 166 | position: fixed; 167 | top: 64px; 168 | bottom: 0px; 169 | overflow-y: auto; 170 | display: inline-block; 171 | padding-bottom: 20px; 172 | } 173 | 174 | ul { 175 | list-style: none; 176 | padding: 0px; 177 | margin: 0px; 178 | } 179 | 180 | .docs-title:before { 181 | content:""; 182 | display: block; 183 | height: 32px; 184 | margin: -8px 0 0; 185 | } 186 | 187 | div > section { 188 | border-bottom: solid 1px #e2e2e2; 189 | } 190 | 191 | section.methods, section.contructors, section.properties { 192 | margin-bottom: 20px; 193 | } 194 | 195 | .test-results { 196 | position: absolute; 197 | top: 0; 198 | right: 0; 199 | text-align: right; 200 | cursor: pointer; 201 | } 202 | .test-results::before { 203 | content: '>'; 204 | display: inline-block; 205 | font-weight: bold; 206 | color: #bebebe; 207 | vertical-align: bottom; 208 | margin-right: 5px; 209 | font-family: 'Courier'; 210 | -webkit-transform: rotate(90deg); 211 | -ms-transform: rotate(90deg); 212 | transform: rotate(90deg); 213 | transition: transform 300ms, -webkit-transform 300ms, -ms-transform 300ms; 214 | } 215 | .test-results.test-detail:before { 216 | -webkit-transform: rotate(-90deg); 217 | -ms-transform: rotate(-90deg); 218 | transform: rotate(-90deg); 219 | } 220 | 221 | .test-collapsible { 222 | position: relative; 223 | overflow-y: hidden; 224 | white-space: nowrap; 225 | transition: height 300ms, margin-top 300ms; 226 | height: 0; 227 | width: 320px; 228 | background-color: #eee; 229 | } 230 | .test-results.test-detail .test-collapsible { 231 | height: 1.5em; 232 | } 233 | .test-collapsible .test-content { 234 | position: absolute; 235 | right: 0; 236 | bottom: 0; 237 | } 238 | 239 | .test-result-title { 240 | display: inline-block; 241 | font-weight: 500; 242 | padding: 0px; 243 | } 244 | 245 | .test-result { 246 | background-color: #bebebe; 247 | padding: 2px 5px; 248 | margin: 0px 5px; 249 | color: #F8FCFA; 250 | font-size: 12px; 251 | border-radius: 3px; 252 | } 253 | 254 | .test-result.test-color-0 { 255 | background-color: #ba5050; 256 | } 257 | .test-result.test-color-10 { 258 | background-color: #ba7a50; 259 | } 260 | .test-result.test-color-25 { 261 | background-color: #ba9f50; 262 | } 263 | .test-result.test-color-50 { 264 | background-color: #baba50; 265 | } 266 | .test-result.test-color-75 { 267 | background-color: #93ba50; 268 | } 269 | .test-result.test-color-100 { 270 | background-color: #50BB88; 271 | } 272 | 273 | .test-pip { 274 | display: inline-block; 275 | width: 5px; 276 | height: 5px; 277 | border-radius: 2px; 278 | padding: 0; 279 | margin: 0 1px; 280 | vertical-align: middle; 281 | } 282 | 283 | /* Fix some issues on small screens */ 284 | @media (max-width: 576px) { 285 | .test-results { 286 | float: none !important; 287 | } 288 | 289 | .navbar { 290 | width: 95% !important; 291 | } 292 | } 293 | 294 | section.row { 295 | padding: 0px; 296 | } 297 | 298 | .secondary-image { 299 | width: 160px; 300 | height: 160px; 301 | margin: 20px; 302 | } 303 | 304 | .tagline { 305 | justify-content: center; 306 | text-align: center; 307 | } 308 | 309 | .tagline-header { 310 | font-size: 18px; 311 | } 312 | 313 | dd, dl { 314 | margin-bottom: 25px; 315 | } 316 | 317 | dl { 318 | margin-left: 25px; 319 | } 320 | 321 | .menu-heading { 322 | color: #5A5886; 323 | } 324 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-rc.3", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "independent" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "bootstrap": "if-env CI=true && lerna bootstrap --concurrency 1 || lerna bootstrap", 6 | "build": "lerna run build", 7 | "test": "npm run lint:js && npm run lint:ts && lerna run test", 8 | "lint:js": "eslint --ignore-path .gitignore **/*.js", 9 | "lint:ts": "tslint --exclude **/node_modules/**/*.* **/*.ts", 10 | "postinstall": "npm run bootstrap", 11 | "test:ui": "cd packages/api-tests && npm run test:ui", 12 | "test:ci": "cd packages/api-tests && npm run test:ci && node generate-test-report.js", 13 | "docs": "cd packages/api-specification && npm run docs", 14 | "ci": "npm test && npm run build", 15 | "publish": "lerna publish --independent" 16 | }, 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/symphonyoss/containerjs.git" 21 | }, 22 | "homepage": "https://github.com/symphonyoss/containerjs#readme", 23 | "devDependencies": { 24 | "eslint": "^3.17.1", 25 | "eslint-config-standard": "^7.0.1", 26 | "eslint-plugin-import": "^2.2.0", 27 | "eslint-plugin-node": "^4.2.0", 28 | "eslint-plugin-promise": "^3.5.0", 29 | "eslint-plugin-standard": "^2.1.1", 30 | "if-env": "^1.0.0", 31 | "lerna": "2.0.0-rc.3", 32 | "marked": "^0.3.6", 33 | "tslint": "^5.5.0", 34 | "typescript": "^2.4.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/api-browser/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | src/ 4 | build/es/ 5 | index.ts 6 | rollup.config.js 7 | tsconfig.json 8 | -------------------------------------------------------------------------------- /packages/api-browser/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS API for Browsers 2 | 3 | This project provides an implementation of ContainerJS which follows the Symphony Desktop Wrapper API Specification for Browsers 4 | For more information, see [The ContainerJS website](https://symphonyoss.github.io/ContainerJS/) 5 | 6 | ## Usage 7 | 8 | Add this package to your project: 9 | 10 | ``` 11 | npm install containerjs-api-browser --save 12 | ``` 13 | 14 | You need to include this code into the project by loading the JavaScript file directly. 15 | -------------------------------------------------------------------------------- /packages/api-browser/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from './src/app'; 2 | import { MessageService } from './src/message-service'; 3 | import { Window } from './src/window'; 4 | import { BrowserNotification as Notification } from './src/notification'; 5 | import { Screen } from './src/screen'; 6 | 7 | let api: any = { 8 | app, 9 | MessageService, 10 | Window, 11 | Notification, 12 | Screen 13 | }; 14 | 15 | if ((window as any).ssf) { 16 | api = (window as any).ssf; 17 | } 18 | 19 | // Need to disable tslint for the next line so we can export default 20 | export default api; // tslint:disable-line 21 | -------------------------------------------------------------------------------- /packages/api-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-browser", 3 | "version": "0.0.8", 4 | "description": "The ContainerJS browser API", 5 | "main": "build/dist/containerjs-api.js", 6 | "scripts": { 7 | "clean": "rimraf build", 8 | "build": "npm run clean && tsc && rollup -c && npm run bundle", 9 | "bundle": "concat -o build/dist/containerjs-api-symphony.js build/dist/containerjs-api.js node_modules/containerjs-api-compatibility/build/dist/containerjs-api-compatibility.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 14 | }, 15 | "author": "", 16 | "license": "Apache-2.0", 17 | "bugs": { 18 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 19 | }, 20 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 21 | "dependencies": { 22 | "html2canvas": "^0.5.0-beta4" 23 | }, 24 | "devDependencies": { 25 | "concat": "^1.0.3", 26 | "containerjs-api-compatibility": "^0.0.6", 27 | "containerjs-api-specification": "^0.0.7", 28 | "containerjs-api-utility": "^0.0.5", 29 | "copyfiles": "^1.2.0", 30 | "rimraf": "^2.6.1", 31 | "rollup": "^0.41.6", 32 | "rollup-plugin-node-resolve": "^2.0.0", 33 | "typescript": "^2.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/api-browser/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | 3 | export default { 4 | entry: 'build/es/index.js', 5 | format: 'umd', 6 | moduleName: 'ssf', 7 | plugins: [resolve()], 8 | dest: 'build/dist/containerjs-api.js' 9 | }; 10 | -------------------------------------------------------------------------------- /packages/api-browser/src/accessible-windows.ts: -------------------------------------------------------------------------------- 1 | const accessibleWindows: Map = new Map(); 2 | 3 | export const initialise = () => { 4 | // If we have an opener, we are not the parent so we need to add it as a window 5 | if (window.opener) { 6 | // The reference to the opener is not the full window object, so there is no onclose handler available 7 | accessibleWindows['parent'] = window.opener; 8 | } else { 9 | window.name = 'parent'; 10 | } 11 | 12 | window.addEventListener('message', event => { 13 | // Redistribute message up/down the tree 14 | distributeMessage(event.data); 15 | }, false); 16 | }; 17 | 18 | export const getAccessibleWindows = () => accessibleWindows; 19 | export const getAccessibleWindow = (name: string) => accessibleWindows[name]; 20 | 21 | export const addAccessibleWindow = (name: string, win: Window) => { 22 | accessibleWindows[name] = win; 23 | }; 24 | 25 | export const removeAccessibleWindow = (name: string) => { 26 | delete accessibleWindows[name]; 27 | }; 28 | 29 | interface MessageDetails { 30 | senderId: string; 31 | windowId: string; // May be wildcard "*" 32 | topic: string; 33 | message: any; 34 | receivedFromId?: string; 35 | } 36 | 37 | export const distributeMessage = (messageDetails: MessageDetails): void => { 38 | // Using receivedFromId avoids sending it back the way it came 39 | const packet = Object.assign({}, messageDetails, { 40 | receivedFromId: window.name 41 | }); 42 | 43 | const windows = getAccessibleWindows(); 44 | for (const name in windows) { 45 | if (windows.hasOwnProperty(name) 46 | && name !== messageDetails.receivedFromId 47 | && name !== messageDetails.senderId) { 48 | const win = windows[name]; 49 | win.postMessage(packet, '*'); 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /packages/api-browser/src/app.ts: -------------------------------------------------------------------------------- 1 | import { initialise as initialiseWindows } from './accessible-windows'; 2 | 3 | let initialised = false; 4 | export class app implements ssf.app { 5 | static ready() { 6 | if (!initialised) { 7 | initialised = true; 8 | initialiseWindows(); 9 | } 10 | return Promise.resolve(); 11 | } 12 | 13 | static setBadgeCount(count: number) { 14 | // Browser doesn't support badge count 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/api-browser/src/browser-screen.ts: -------------------------------------------------------------------------------- 1 | export type BrowserScreen = Screen; 2 | -------------------------------------------------------------------------------- /packages/api-browser/src/message-service.ts: -------------------------------------------------------------------------------- 1 | import { distributeMessage } from './accessible-windows'; 2 | 3 | const listenerMap = new Map(); 4 | 5 | const currentWindowId = (): string => { 6 | return ssf.Window.getCurrentWindow().getId(); 7 | }; 8 | 9 | export class MessageService implements ssf.MessageService { 10 | static send(windowId: string, topic: string, message: any) { 11 | const senderId = currentWindowId(); 12 | 13 | distributeMessage({ 14 | senderId, 15 | windowId, 16 | topic, 17 | message 18 | }); 19 | } 20 | 21 | static subscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void) { 22 | const receiveMessage = (event) => { 23 | const thisId = currentWindowId(); 24 | if ((windowId === '*' || windowId === event.data.senderId) 25 | && (event.data.windowId === '*' || thisId === event.data.windowId) 26 | && event.data.senderId !== thisId 27 | && topic === event.data.topic) { 28 | // Message intended for this window 29 | listener(event.data.message, event.data.senderId); 30 | } 31 | }; 32 | 33 | window.addEventListener('message', receiveMessage, false); 34 | 35 | // Map the arguments to the actual listener that was added 36 | listenerMap.set({ 37 | windowId, 38 | topic, 39 | listener 40 | }, receiveMessage); 41 | } 42 | 43 | static unsubscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void) { 44 | let deleteKey = null; 45 | 46 | // We cant use listenerMap.has() here because reconstructing the key from the arguments is a different object 47 | // I.e. {} !== {} 48 | listenerMap.forEach((value, key) => { 49 | if (key.windowId === windowId && key.topic === topic && key.listener === listener) { 50 | window.removeEventListener('message', value); 51 | deleteKey = key; 52 | } 53 | }); 54 | 55 | listenerMap.delete(deleteKey); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/api-browser/src/notification.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from 'containerjs-api-utility'; 2 | 3 | export class BrowserNotification extends Emitter implements ssf.Notification { 4 | innerNotification: Notification; 5 | 6 | constructor(title: string, options: ssf.NotificationOptions) { 7 | super(); 8 | this.innerNotification = new Notification(title, options); 9 | } 10 | 11 | innerAddEventListener(eventName: string, handler: (...args: any[]) => void) { 12 | this.innerNotification.addEventListener(eventName, handler); 13 | } 14 | 15 | innerRemoveEventListener(eventName: string, handler: (...args: any[]) => void) { 16 | this.innerNotification.removeEventListener(eventName, handler); 17 | } 18 | 19 | static requestPermission(): Promise { 20 | return Notification.requestPermission(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/api-browser/src/screen.ts: -------------------------------------------------------------------------------- 1 | import { BrowserScreen } from './browser-screen'; 2 | 3 | const browserDisplayMap = (display: BrowserScreen, primary: boolean): ssf.Display => { 4 | return { 5 | id: 'primary', 6 | rotation: display.orientation.angle, 7 | scaleFactor: window.devicePixelRatio, 8 | bounds: { 9 | x: screenX, 10 | width: display.width, 11 | y: screenY, 12 | height: display.height 13 | }, 14 | primary 15 | }; 16 | }; 17 | 18 | export class Screen implements ssf.Screen { 19 | static getDisplays() { 20 | return new Promise(resolve => { 21 | resolve([browserDisplayMap(window.screen, true)]); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/api-browser/src/window.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addAccessibleWindow, 3 | removeAccessibleWindow 4 | } from './accessible-windows'; 5 | import { Emitter } from 'containerjs-api-utility'; 6 | import 'html2canvas'; 7 | 8 | declare let html2canvas: any; 9 | 10 | const DEFAULT_OPTIONS = { 11 | width: 800, 12 | height: 600 13 | }; 14 | 15 | let currentWindow = null; 16 | 17 | const getWindowOffsets = (win) => { 18 | const xOffset = (win.outerWidth / 2); 19 | const yOffset = (win.outerHeight / 2); 20 | return [Math.floor(xOffset), Math.floor(yOffset)]; 21 | }; 22 | 23 | export class Window extends Emitter implements ssf.WindowCore { 24 | children: ssf.Window[]; 25 | innerWindow: any; 26 | id: string; 27 | 28 | constructor(options?: ssf.WindowOptions, callback?: (win: Window) => void, errorCallback?: (err?: any) => void) { 29 | super(); 30 | this.children = []; 31 | 32 | if (!options) { 33 | this.innerWindow = window; 34 | this.id = window.name; 35 | if (callback) { 36 | callback(this); 37 | } 38 | } else { 39 | const winOptions = Object.assign({}, DEFAULT_OPTIONS, options); 40 | this.innerWindow = window.open(options.url, options.name, objectToFeaturesString(winOptions)); 41 | this.id = this.innerWindow.name; 42 | const [xOffset, yOffset] = getWindowOffsets(this.innerWindow); 43 | this.setPosition(options.x || (screen.width / 2) - xOffset, options.y || (screen.height / 2) - yOffset); 44 | 45 | const currentWindow = Window.getCurrentWindow(); 46 | const childClose = () => this.innerWindow.close(); 47 | 48 | this.innerWindow.addEventListener('beforeunload', () => { 49 | const index = currentWindow.children.indexOf(this); 50 | if (index !== -1) { 51 | currentWindow.children.splice(index, 1); 52 | currentWindow.innerWindow.removeEventListener('beforeunload', childClose); 53 | } 54 | removeAccessibleWindow(this.innerWindow.name); 55 | }); 56 | 57 | if (options.child) { 58 | currentWindow.children.push(this); 59 | currentWindow.innerWindow.addEventListener('beforeunload', childClose); 60 | } 61 | addAccessibleWindow(options.name, this.innerWindow); 62 | } 63 | 64 | if (callback) { 65 | callback(this); 66 | } 67 | } 68 | 69 | close() { 70 | return this.asPromise(() => this.innerWindow.close()); 71 | } 72 | 73 | getId() { 74 | return this.id; 75 | } 76 | 77 | focus() { 78 | return this.asPromise(() => this.innerWindow.focus()); 79 | } 80 | 81 | blur() { 82 | return this.asPromise(() => this.innerWindow.blur()); 83 | } 84 | 85 | getBounds() { 86 | return this.asPromise(() => ({ 87 | x: this.innerWindow.screenX, 88 | y: this.innerWindow.screenY, 89 | width: this.innerWindow.outerWidth, 90 | height: this.innerWindow.outerHeight 91 | })); 92 | } 93 | 94 | getParentWindow() { 95 | return new Promise(resolve => { 96 | let newWin = null; 97 | if (window.opener) { 98 | newWin = Window.wrap(window.opener); 99 | } 100 | 101 | resolve(newWin); 102 | }); 103 | } 104 | 105 | getPosition() { 106 | return this.asPromise>(() => [this.innerWindow.screenX, this.innerWindow.screenY]); 107 | } 108 | 109 | getSize() { 110 | return this.asPromise>(() => [this.innerWindow.outerWidth, this.innerWindow.outerHeight]); 111 | } 112 | 113 | getTitle() { 114 | return this.asPromise(() => this.innerWindow.name || this.innerWindow.document.title); 115 | } 116 | 117 | // Cannot be anything but true for browser 118 | isMaximizable() { 119 | return this.asPromise(() => true); 120 | } 121 | 122 | // Cannot be anything but true for browser 123 | isMinimizable() { 124 | return this.asPromise(() => true); 125 | } 126 | 127 | // Cannot be anything but true for browser 128 | isResizable() { 129 | return this.asPromise(() => true); 130 | } 131 | 132 | loadURL(url: string) { 133 | return this.asPromise(() => location.href = url); 134 | } 135 | 136 | reload() { 137 | return this.asPromise(() => location.reload()); 138 | } 139 | 140 | setBounds(bounds: ssf.Rectangle) { 141 | return this.asPromise(() => { 142 | this.innerWindow.moveTo(bounds.x, bounds.y); 143 | this.innerWindow.resizeTo(bounds.width, bounds.height); 144 | }); 145 | } 146 | 147 | setPosition(x: number, y: number) { 148 | return this.asPromise(() => this.innerWindow.moveTo(x, y)); 149 | } 150 | 151 | setSize(width: number, height: number) { 152 | return this.asPromise(() => this.innerWindow.resizeTo(width, height)); 153 | } 154 | 155 | innerAddEventListener(event: string, listener: (...args: any[]) => void) { 156 | this.innerWindow.addEventListener(eventMap[event], listener); 157 | } 158 | 159 | innerRemoveEventListener(event: string, listener: (...args: any[]) => void) { 160 | this.innerWindow.removeEventListener(eventMap[event], listener); 161 | } 162 | 163 | postMessage(message: any) { 164 | this.innerWindow.postMessage(message, '*'); 165 | } 166 | 167 | getChildWindows() { 168 | return new Promise(resolve => resolve(this.children)); 169 | } 170 | 171 | asPromise(fn: (...args: any[]) => any): Promise { 172 | return new Promise((resolve, reject) => { 173 | if (this.innerWindow) { 174 | resolve(fn()); 175 | } else { 176 | reject(new Error('The window does not exist or the window has been closed')); 177 | } 178 | }); 179 | } 180 | 181 | static getCurrentWindow(callback?: (win: Window) => void, errorCallback?: (err?: any) => void) { 182 | if (currentWindow) { 183 | return currentWindow; 184 | } 185 | 186 | currentWindow = new Window(null, callback, errorCallback); 187 | return currentWindow; 188 | } 189 | 190 | static wrap(win: BrowserWindow) { 191 | const wrappedWindow = new Window(); 192 | wrappedWindow.innerWindow = win; 193 | wrappedWindow.id = String(win.name); 194 | return wrappedWindow; 195 | } 196 | 197 | capture() { 198 | return html2canvas(this.innerWindow.document.documentElement) 199 | .then((canvas) => { 200 | this.emit('capture'); 201 | return canvas.toDataURL(); 202 | }); 203 | } 204 | } 205 | 206 | const objectToFeaturesString = (features: ssf.WindowOptions) => { 207 | return Object.keys(features).map((key) => { 208 | let value = features[key]; 209 | 210 | // Need to convert booleans to yes/no 211 | if (value === true) { 212 | value = 'yes'; 213 | } else if (value === false) { 214 | value = 'no'; 215 | } 216 | 217 | return `${key}=${value}`; 218 | }).join(','); 219 | }; 220 | 221 | const eventMap = { 222 | 'blur': 'blur', 223 | 'close': 'beforeunload', 224 | 'closed': 'unload', 225 | 'focus': 'focus', 226 | 'hide': 'hidden', 227 | 'message': 'message', 228 | 'show': 'load', 229 | 'resize': 'resize', 230 | 'capture': 'capture' 231 | }; 232 | -------------------------------------------------------------------------------- /packages/api-browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "dom.iterable", 9 | "scripthost" 10 | ], 11 | "outDir": "./build/es", 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "./index.ts", 18 | "node_modules/containerjs-api-specification/interface/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | "ssf": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/api-demo/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /packages/api-demo/README.md: -------------------------------------------------------------------------------- 1 | # api-demo 2 | 3 | Demonstrates the ContainerJS, allowing you to explore the various methods and events interactively. 4 | 5 | ### Development build 6 | 7 | In order to run the demo using a local development build, run the following commands from the project root: 8 | 9 | ``` 10 | npm install 11 | npm run build 12 | ``` 13 | 14 | Then, within this package, you can run Electron, OpenFin, or browser as follows: 15 | 16 | ``` 17 | npm run openfin 18 | npm run electron 19 | npm run browser 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/api-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-demo", 3 | "version": "0.0.8", 4 | "description": "A bundle of containerJS APIs", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "serve:no-browser": "npm run serve -- --no-browser", 9 | "serve": "live-server --mount=/:src --mount=/scripts/containerjs-api:node_modules/containerjs-api-browser/build/dist/containerjs-api.js --mount=/resources/notification.html:node_modules/containerjs-api-openfin/build/dist/notification.html", 10 | "launch:electron": "ssf-electron -u http://localhost:8080/index.html -d", 11 | "electron": "npm-run-all --parallel launch:electron serve:no-browser", 12 | "launch:openfin": "ssf-openfin -u http://localhost:8080/index.html -d", 13 | "openfin": "npm-run-all --parallel launch:openfin serve:no-browser", 14 | "browser": "npm run serve" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 19 | }, 20 | "author": "", 21 | "license": "Apache-2.0", 22 | "bugs": { 23 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 24 | }, 25 | "homepage": "https://github.com/symphonyoss/ContainerJS#readme", 26 | "dependencies": { 27 | "containerjs-api-browser": "^0.0.8", 28 | "containerjs-api-electron": "^0.0.9", 29 | "containerjs-api-openfin": "^0.0.9", 30 | "openfin-cli": "^1.1.5", 31 | "openfin-launcher": "^1.3.12" 32 | }, 33 | "devDependencies": { 34 | "live-server": "^1.2.0", 35 | "npm-run-all": "^4.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/api-demo/src/app/app-api-demo.js: -------------------------------------------------------------------------------- 1 | ssf.app.ready().then(() => { 2 | document.getElementById('set-badge-count').onclick = () => { 3 | ssf.app.setBadgeCount(Number(document.getElementById('badge-count').value)); 4 | }; 5 | }); 6 | -------------------------------------------------------------------------------- /packages/api-demo/src/app/app-api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ContainerJS 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 | 22 | 23 |

setBadgeCount

24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/api-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ContainerJS 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 | 27 |
28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /packages/api-demo/src/messaging/messaging-api-demo.js: -------------------------------------------------------------------------------- 1 | const messageBox = document.getElementById('message-box'); 2 | const sendButton = document.getElementById('send-message'); 3 | const newWindowButton = document.getElementById('new-window'); 4 | const subscribeButton = document.getElementById('subscribe-button'); 5 | const unsubscribeButton = document.getElementById('unsubscribe-button'); 6 | const subscribedPanel = document.getElementById('subscribed-panel'); 7 | 8 | const appReady = ssf.app.ready(); 9 | 10 | appReady.then(() => { 11 | const windowDetailsId = document.getElementById('window-uuid'); 12 | windowDetailsId.innerText = ssf.Window.getCurrentWindow().getId(); 13 | 14 | newWindowButton.onclick = () => { 15 | // Create a random hex string as the window name 16 | const id = (((1 + Math.random()) * 0x1000000) | 0).toString(16).substring(1); 17 | 18 | const isChild = document.getElementById('child').checked; 19 | 20 | const path = location.href.substring(0, location.href.lastIndexOf('/')); 21 | 22 | // eslint-disable-next-line no-new 23 | new ssf.Window({ 24 | child: isChild, 25 | name: id, 26 | show: true, 27 | url: `${path}/messaging-api.html`, 28 | width: 800, 29 | height: 920 30 | }); 31 | }; 32 | 33 | sendButton.onsubmit = e => { 34 | e.preventDefault(); 35 | 36 | const uuid = document.getElementById('uuid').value; 37 | const message = document.getElementById('message').value; 38 | ssf.MessageService.send(uuid, 'test', message); 39 | }; 40 | 41 | const messageReceived = (message, senderId) => { 42 | messageBox.innerText = '\'' + message + '\' from ' + senderId; 43 | }; 44 | 45 | subscribeButton.onclick = () => { 46 | ssf.MessageService.subscribe('*', 'test', messageReceived); 47 | 48 | subscribeButton.style.display = 'none'; 49 | unsubscribeButton.style.display = ''; 50 | subscribedPanel.style.display = ''; 51 | }; 52 | 53 | unsubscribeButton.onclick = () => { 54 | ssf.MessageService.unsubscribe('*', 'test', messageReceived); 55 | 56 | subscribeButton.style.display = ''; 57 | unsubscribeButton.style.display = 'none'; 58 | subscribedPanel.style.display = 'none'; 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /packages/api-demo/src/messaging/messaging-api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ContainerJS 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 | 22 | 23 |

Try me

24 | 25 |
26 | 27 |
Window Id:
28 |

29 | 30 |
31 | 32 |

Create a window to send messages to:

33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 |
52 | 53 |
54 | 55 |
56 | 57 |
Subscribe and unsubscribe from the channel, default is unsubscribed
58 | 59 |
60 | 61 | 62 |
63 | 64 |
65 | 66 | 73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /packages/api-demo/src/notifications/notification-api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ContainerJS 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 | 22 | 23 |

Try me

24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 |

Clicks

38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/api-demo/src/notifications/notification-demo.js: -------------------------------------------------------------------------------- 1 | var button = document.getElementById('notification-test'); 2 | 3 | button.onclick = function() { 4 | ssf.Notification.requestPermission() 5 | .then(function(permission) { 6 | if (permission !== 'granted') { 7 | console.error('notification permission not granted'); 8 | return; 9 | } 10 | 11 | var title = document.getElementById('title').value; 12 | var body = document.getElementById('body').value; 13 | var eventlistElement = document.getElementById('eventlist'); 14 | 15 | // eslint-disable-next-line no-new 16 | const notification = new ssf.Notification(title, { 17 | body: body, 18 | icon: 'notification-icon.png', 19 | image: 'notification-image.png', 20 | template: '/resources/notification.html' 21 | }); 22 | 23 | notification.on('click', () => { 24 | eventlistElement.innerHTML = eventlistElement.innerHTML + '
click fired'; 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/api-demo/src/notifications/notification-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symphonyoss/ContainerJS/38578c4bc77417e88ed44da8b3eae71f1e1f2317/packages/api-demo/src/notifications/notification-icon.png -------------------------------------------------------------------------------- /packages/api-demo/src/notifications/notification-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symphonyoss/ContainerJS/38578c4bc77417e88ed44da8b3eae71f1e1f2317/packages/api-demo/src/notifications/notification-image.png -------------------------------------------------------------------------------- /packages/api-demo/src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-bottom: 30px; 3 | } 4 | 5 | #screen-snippet-test-preview { 6 | display: none; 7 | margin: 20px auto; 8 | box-shadow: 0px 0px 36px 5px rgba(0, 0, 0, 0.4); 9 | } 10 | 11 | #screen-snippet-test-error { 12 | font-style: italic; 13 | } 14 | 15 | #event-log { 16 | height: 300px; 17 | overflow: auto; 18 | } 19 | -------------------------------------------------------------------------------- /packages/api-demo/src/window/window-api-demo.js: -------------------------------------------------------------------------------- 1 | var newWindowButton = document.getElementById('new-window-test'); 2 | var eventLogList = document.getElementById('event-log'); 3 | 4 | const appReady = ssf.app.ready(); 5 | 6 | let win; 7 | 8 | appReady.then(() => { 9 | newWindowButton.onclick = function() { 10 | var url = document.getElementById('url').value; 11 | var windowName = document.getElementById('name').value; 12 | var isAlwaysOnTop = document.getElementById('alwaysOnTop').checked; 13 | var isChild = document.getElementById('child').checked; 14 | var isCentered = document.getElementById('center').checked; 15 | var hasFrame = document.getElementById('frame').checked; 16 | var height = parseInt(document.getElementById('height').value, 10); 17 | var isMaximizable = document.getElementById('maximizable').checked; 18 | var isMinimizable = document.getElementById('minimizable').checked; 19 | var isResizable = document.getElementById('resizable').checked; 20 | var isShown = document.getElementById('show').checked; 21 | var isSkippingTaskbar = document.getElementById('skipTaskbar').checked; 22 | var width = parseInt(document.getElementById('width').value, 10); 23 | win = new ssf.Window({ 24 | alwaysOnTop: isAlwaysOnTop, 25 | child: isChild, 26 | center: isCentered, 27 | frame: hasFrame, 28 | height, 29 | maximizable: isMaximizable, 30 | minimizable: isMinimizable, 31 | name: windowName, 32 | resizable: isResizable, 33 | show: isShown, 34 | skipTaskbar: isSkippingTaskbar, 35 | width, 36 | shadow: true, 37 | url 38 | }, (win) => { 39 | const addListItem = (text) => { 40 | const newElem = document.createElement('li'); 41 | newElem.innerText = text; 42 | newElem.className = 'list-group-item'; 43 | eventLogList.appendChild(newElem); 44 | eventLogList.scrollTop = eventLogList.scrollHeight; 45 | }; 46 | 47 | win.addListener('hide', () => { 48 | addListItem('hide'); 49 | }); 50 | 51 | win.addListener('show', () => { 52 | addListItem('show'); 53 | }); 54 | 55 | win.addListener('blur', () => { 56 | addListItem('blur'); 57 | }); 58 | 59 | win.addListener('focus', () => { 60 | addListItem('focus'); 61 | }); 62 | 63 | win.addListener('close', () => { 64 | addListItem('close'); 65 | }); 66 | 67 | win.addListener('maximize', () => { 68 | addListItem('maximize'); 69 | }); 70 | 71 | win.addListener('unmaximize', () => { 72 | addListItem('unmaximize'); 73 | }); 74 | 75 | win.addListener('minimize', () => { 76 | addListItem('minimize'); 77 | }); 78 | 79 | win.addListener('restore', () => { 80 | addListItem('restore'); 81 | }); 82 | 83 | win.addListener('capture', () => { 84 | addListItem('capture'); 85 | }); 86 | }); 87 | }; 88 | 89 | var closeWindow = document.getElementById('close-window'); 90 | 91 | closeWindow.onclick = () => { 92 | win.close() 93 | .catch((error) => { 94 | console.log(error); 95 | }); 96 | }; 97 | 98 | var hideWindow = document.getElementById('hide-window'); 99 | 100 | hideWindow.onclick = () => { 101 | win.hide() 102 | .catch((error) => { 103 | console.log(error); 104 | }); 105 | }; 106 | 107 | var showWindow = document.getElementById('show-window'); 108 | 109 | showWindow.onclick = () => { 110 | win.show() 111 | .catch((error) => { 112 | console.log(error); 113 | }); 114 | }; 115 | 116 | var focusWindow = document.getElementById('focus-window'); 117 | 118 | focusWindow.onclick = () => { 119 | win.focus() 120 | .catch((error) => { 121 | console.log(error); 122 | }); 123 | }; 124 | 125 | var blurWindow = document.getElementById('blur-window'); 126 | 127 | blurWindow.onclick = () => { 128 | win.blur() 129 | .catch((error) => { 130 | console.log(error); 131 | }); 132 | }; 133 | 134 | var maximizeWindow = document.getElementById('maximize-window'); 135 | 136 | maximizeWindow.onclick = () => { 137 | win.maximize() 138 | .catch((error) => { 139 | console.log(error); 140 | }); 141 | }; 142 | 143 | var unmaximizeWindow = document.getElementById('unmaximize-window'); 144 | 145 | unmaximizeWindow.onclick = () => { 146 | win.unmaximize() 147 | .catch((error) => { 148 | console.log(error); 149 | }); 150 | }; 151 | 152 | var minimizeWindow = document.getElementById('minimize-window'); 153 | 154 | minimizeWindow.onclick = () => { 155 | win.minimize() 156 | .catch((error) => { 157 | console.log(error); 158 | }); 159 | }; 160 | 161 | var restoreWindow = document.getElementById('restore-window'); 162 | 163 | restoreWindow.onclick = () => { 164 | win.restore() 165 | .catch((error) => { 166 | console.log(error); 167 | }); 168 | }; 169 | 170 | var captureWindow = document.getElementById('capture-window'); 171 | var capturePreview = document.getElementById('capture-preview'); 172 | 173 | captureWindow.onclick = () => { 174 | capturePreview.src = ''; 175 | capturePreview.style.display = 'none'; 176 | 177 | win.capture() 178 | .then((dataUri) => { 179 | capturePreview.src = dataUri; 180 | capturePreview.style.display = 'block'; 181 | }) 182 | .catch((error) => { 183 | console.log(error); 184 | }); 185 | }; 186 | }); 187 | -------------------------------------------------------------------------------- /packages/api-demo/src/window/window-api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ContainerJS 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 |
19 | 22 | 23 |

Try me

24 |
25 |

Options

26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 | 71 | 72 |
73 |
74 | 75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 | 89 | 90 |
91 | 92 |
93 | 94 |
95 | 96 |

New window controls

97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
109 | 110 |
111 | 112 | 113 | 114 |
115 | 116 |

Event Log:

117 |
118 |
    119 |
120 |
121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /packages/api-electron/.gitattributes: -------------------------------------------------------------------------------- 1 | # Needed for running the executable on mac/linux 2 | bin/ssf-electron text eol=lf 3 | -------------------------------------------------------------------------------- /packages/api-electron/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | src/preload/ 4 | build/es/ 5 | preload.ts 6 | rollup.config.js 7 | tsconfig.json 8 | -------------------------------------------------------------------------------- /packages/api-electron/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS API for Electron 2 | 3 | This project provides an implementation of ContainerJS which follows the Symphony Desktop Wrapper API Specification for Electron 4 | For more information, see [The ContainerJS website](https://symphonyoss.github.io/ContainerJS/) 5 | 6 | ## Usage 7 | 8 | Add this package to your Electron project: 9 | 10 | ``` 11 | npm install containerjs-api-electron --save 12 | ``` 13 | 14 | ### Running the app 15 | 16 | Use the `ssf-electron` binary to run the application. This can be installed globally by doing: 17 | 18 | ``` 19 | npm install --global containerjs-api-electron 20 | 21 | Usage: ssf-electron [options] 22 | 23 | Options: 24 | 25 | -V, --version output the version number 26 | -u, --url [url] Launch url for the application (can be specified in --config instead) 27 | -c, --config [filename] (Optional) ContainerJS config file 28 | -s, --symphony (Optional) Use Symphony compatibility layer 29 | -d, --developer (Optional) Show developer menu 30 | -h, --help output usage information 31 | ``` 32 | 33 | To run the application, do: 34 | 35 | ``` 36 | ssf-electron --url http://website/index.html 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /packages/api-electron/bin/ssf-electron: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var electron = require('electron'); 4 | var process = require('process'); 5 | var proc = require('child_process'); 6 | var path = require('path'); 7 | 8 | var mainFile = path.join(__dirname, '../main.js'); 9 | 10 | // add the location of the main JS file to the command line args 11 | var args = [mainFile, '--enable-sandbox'].concat(process.argv.slice(2)); 12 | 13 | var child = proc.spawn(electron, args, {stdio: 'inherit'}); 14 | child.on('close', function (code) { 15 | process.exit(code); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/api-electron/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | app, 4 | BrowserWindow, 5 | ipcMain: ipc 6 | } = require('electron'); 7 | const path = require('path'); 8 | const { IpcMessages } = require('./src/common/constants'); 9 | 10 | let win; 11 | const windows = []; 12 | 13 | module.exports = (appJson, useSymphony, showDeveloperMenu) => { 14 | const preloadFile = useSymphony ? 'containerjs-api-symphony.js' : 'containerjs-api.js'; 15 | const preloadPath = path.join(__dirname, 'build', 'dist', preloadFile); 16 | 17 | if (!showDeveloperMenu) { 18 | app.on('browser-window-created', function(e, window) { 19 | window.setMenu(null); 20 | }); 21 | } 22 | 23 | ipc.on(IpcMessages.IPC_SSF_NEW_WINDOW, (e, msg) => { 24 | const options = Object.assign( 25 | {}, 26 | msg.features, 27 | { 28 | webPreferences: { 29 | sandbox: true, 30 | preload: preloadPath 31 | } 32 | } 33 | ); 34 | 35 | if (msg.features && msg.features.child) { 36 | options.parent = BrowserWindow.fromWebContents(e.sender); 37 | } 38 | 39 | const newWindow = new BrowserWindow(options); 40 | newWindow.loadURL(msg.url); 41 | newWindow.on('close', () => { 42 | const index = windows.indexOf(newWindow); 43 | if (index >= 0) { 44 | windows.splice(index, 1); 45 | } 46 | }); 47 | 48 | e.returnValue = newWindow.id; 49 | 50 | windows.push(newWindow); 51 | }); 52 | 53 | ipc.on(IpcMessages.IPC_SSF_SEND_MESSAGE, (e, msg) => { 54 | const senderId = e.sender.id; 55 | const sendToWindow = win => { 56 | // Don't send to self 57 | if (win.id !== senderId) { 58 | win.webContents.send(`${IpcMessages.IPC_SSF_SEND_MESSAGE}-${msg.topic}`, msg.message, senderId); 59 | } 60 | }; 61 | 62 | if (msg.windowId === '*') { 63 | BrowserWindow.getAllWindows().forEach(sendToWindow); 64 | } else { 65 | const windowId = parseInt(msg.windowId, 10); 66 | if (isNaN(windowId)) { 67 | return; 68 | } 69 | 70 | const destinationWindow = BrowserWindow.fromId(windowId); 71 | if (!destinationWindow) { 72 | return; 73 | } 74 | 75 | sendToWindow(destinationWindow); 76 | } 77 | }); 78 | 79 | createInitialWindow(appJson, preloadPath); 80 | }; 81 | 82 | const createInitialWindow = (appJson, preloadPath) => { 83 | // Create an invisible window to run the load script 84 | win = new BrowserWindow({ 85 | width: appJson.defaultWidth || 800, 86 | height: appJson.defaultHeight || 600, 87 | minWidth: appJson.minWidth, 88 | minHeight: appJson.minHeight, 89 | maxWidth: appJson.maxWidth, 90 | maxHeight: appJson.maxHeight, 91 | frame: appJson.frame, 92 | show: appJson.autoShow !== undefined ? appJson.autoShow : true, 93 | webPreferences: { 94 | sandbox: true, 95 | preload: preloadPath 96 | } 97 | }); 98 | 99 | // and load the page used for the hidden window 100 | win.loadURL(appJson.url); 101 | 102 | // Emitted when the window is closed. 103 | win.on('closed', () => { 104 | win = null; 105 | }); 106 | }; 107 | 108 | const ready = (cb) => { 109 | app.on('ready', cb); 110 | }; 111 | 112 | module.exports.app = { 113 | ready 114 | }; 115 | -------------------------------------------------------------------------------- /packages/api-electron/main.js: -------------------------------------------------------------------------------- 1 | const { app } = require('electron'); 2 | const ssfElectron = require('./index.js'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const program = require('commander'); 6 | const fetch = require('node-fetch'); 7 | const packageJson = require('./package.json'); 8 | const promisify = require('util.promisify'); 9 | 10 | const readFileAsync = promisify(fs.readFile); 11 | 12 | program 13 | .version(packageJson.version) 14 | .option('-u, --url [url]', 'Launch url for the application (can be specified in --config instead)', null) 15 | .option('-c, --config [filename]', '(Optional) ContainerJS config file', null) 16 | .option('-s, --symphony', '(Optional) Use Symphony compatibility layer', (v, val) => true, false) 17 | .option('-d, --developer', '(Optional) Show developer menu', (v, val) => true, false) 18 | .parse(process.argv.filter(a => a !== '--enable-sandbox')); 19 | 20 | // Keep a global reference of the window object, if you don't, the window will 21 | // be closed automatically when the JavaScript object is garbage collected. 22 | let win; 23 | 24 | function createWindow() { 25 | loadConfig().then(appJson => { 26 | ssfElectron(appJson, program.symphony, program.developer); 27 | }).catch(err => { 28 | consoleError(err.message); 29 | process.exit(); 30 | }); 31 | } 32 | 33 | function loadConfig() { 34 | if (program.config) { 35 | return readConfigFile() 36 | .then(config => { 37 | if (program.url) { 38 | // Overridden by parameter 39 | config.url = program.url; 40 | } 41 | 42 | if (config.url) { 43 | return config; 44 | } else { 45 | throw new Error('You must specify an URL (--url) or a config file containing an url (--config)'); 46 | } 47 | }); 48 | } else { 49 | if (program.url) { 50 | return Promise.resolve({ 51 | url: program.url 52 | }); 53 | } else { 54 | return Promise.reject(new Error('You must specify an URL (--url) or a config file containing an url (--config)')); 55 | } 56 | } 57 | } 58 | 59 | function readConfigFile() { 60 | const filePath = program.config; 61 | if (filePath.toLowerCase().startsWith('http://') || filePath.toLowerCase().startsWith('https://')) { 62 | return fetch(filePath) 63 | .then(res => res.json()); 64 | } else { 65 | const configFile = path.join(process.cwd(), program.config); 66 | if (fs.existsSync(configFile)) { 67 | return readFileAsync(configFile) 68 | .then(data => JSON.parse(data)); 69 | } else { 70 | return Promise.reject(new Error(`Config file ${configFile} does not exist`)); 71 | } 72 | } 73 | } 74 | 75 | function consoleError(err) { 76 | console.error('\x1b[31m', err, '\x1b[37m'); 77 | } 78 | 79 | ssfElectron.app.ready(createWindow); 80 | 81 | app.on('window-all-closed', () => { 82 | if (process.platform !== 'darwin') { 83 | app.quit(); 84 | } 85 | }); 86 | 87 | app.on('activate', () => { 88 | if (win === null) { 89 | createWindow(); 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /packages/api-electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-electron", 3 | "version": "0.0.9", 4 | "description": "The ContainerJS Electron API", 5 | "main": "build/es/preload.js", 6 | "scripts": { 7 | "clean": "rimraf build", 8 | "build": "npm run clean && tsc && rollup -c && npm run bundle", 9 | "bundle": "concat -o build/dist/containerjs-api-symphony.js build/dist/containerjs-api.js node_modules/containerjs-api-compatibility/build/dist/containerjs-api-compatibility.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 14 | }, 15 | "author": "", 16 | "license": "Apache-2.0", 17 | "bugs": { 18 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 19 | }, 20 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 21 | "dependencies": { 22 | "commander": "^2.11.0", 23 | "electron": "^1.7.5", 24 | "electron-notify": "^0.1.0", 25 | "node-fetch": "^1.7.3", 26 | "util.promisify": "^1.0.0" 27 | }, 28 | "bin": { 29 | "ssf-electron": "./bin/ssf-electron" 30 | }, 31 | "devDependencies": { 32 | "@types/electron-notify": "^0.1.5", 33 | "concat": "^1.0.3", 34 | "containerjs-api-compatibility": "^0.0.6", 35 | "containerjs-api-specification": "^0.0.7", 36 | "containerjs-api-utility": "^0.0.5", 37 | "copyfiles": "^1.2.0", 38 | "rimraf": "^2.6.1", 39 | "rollup": "^0.41.6", 40 | "rollup-plugin-commonjs": "^8.0.2", 41 | "rollup-plugin-node-resolve": "^3.0.0", 42 | "typescript": "^2.4.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/api-electron/preload.ts: -------------------------------------------------------------------------------- 1 | import { app } from './src/preload/app'; 2 | import { MessageService } from './src/preload/message-service'; 3 | import { Window } from './src/preload/window'; 4 | import { Notification } from './src/preload/notification'; 5 | import { Screen } from './src/preload/screen'; 6 | 7 | export { 8 | app, 9 | MessageService, 10 | Window, 11 | Notification, 12 | Screen 13 | }; 14 | -------------------------------------------------------------------------------- /packages/api-electron/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | 4 | export default { 5 | entry: 'build/es/preload.js', 6 | format: 'umd', 7 | moduleName: 'ssf', 8 | dest: 'build/dist/containerjs-api.js', 9 | plugins: [ 10 | nodeResolve({ 11 | jsnext: true, 12 | main: true 13 | }), 14 | 15 | commonjs({ 16 | ignore: ['electron'] 17 | }) 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /packages/api-electron/src/common/constants.js: -------------------------------------------------------------------------------- 1 | // Add new constants in alphabetical order 2 | exports.IpcMessages = { 3 | IPC_SSF_NEW_WINDOW: 'ssf-new-window', 4 | IPC_SSF_NOTIFICATION: 'ssf-notification', 5 | IPC_SSF_SEND_MESSAGE: 'ssf-send-message' 6 | }; 7 | -------------------------------------------------------------------------------- /packages/api-electron/src/preload/app.ts: -------------------------------------------------------------------------------- 1 | const electronApp = require('electron').remote.app; 2 | 3 | export class app implements ssf.app { 4 | static ready() { 5 | return Promise.resolve(); 6 | } 7 | 8 | static setBadgeCount(count: number) { 9 | electronApp.setBadgeCount(count); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/api-electron/src/preload/message-service.ts: -------------------------------------------------------------------------------- 1 | const ipc = require('electron').ipcRenderer; 2 | import { IpcMessages } from '../common/constants'; 3 | 4 | const listenerMap = new Map(); 5 | 6 | export class MessageService implements ssf.MessageService { 7 | static send(windowId: string, topic: string, message: any) { 8 | ipc.send(IpcMessages.IPC_SSF_SEND_MESSAGE, { 9 | windowId, 10 | topic, 11 | message 12 | }); 13 | } 14 | 15 | static subscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void) { 16 | const receiveMessage = (event, message, sender) => { 17 | // Check this was from the correct window 18 | if (windowId === sender.toString() || windowId === '*') { 19 | listener(message, sender); 20 | } 21 | }; 22 | 23 | ipc.on(`${IpcMessages.IPC_SSF_SEND_MESSAGE}-${topic}`, receiveMessage); 24 | 25 | // Map the arguments to the actual listener that was added 26 | listenerMap.set({ 27 | windowId, 28 | topic, 29 | listener 30 | }, receiveMessage); 31 | } 32 | 33 | static unsubscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void) { 34 | let deleteKey = null; 35 | 36 | // We cant use listenerMap.has() here because reconstructing the key from the arguments is a different object 37 | // I.e. {} !== {} 38 | listenerMap.forEach((value, key) => { 39 | if (key.windowId === windowId && key.topic === topic && key.listener === listener) { 40 | ipc.removeListener(`${IpcMessages.IPC_SSF_SEND_MESSAGE}-${topic}`, value); 41 | deleteKey = key; 42 | } 43 | }); 44 | 45 | listenerMap.delete(deleteKey); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/api-electron/src/preload/notification.ts: -------------------------------------------------------------------------------- 1 | const remote = require('electron').remote; 2 | const eNotify = remote.require('electron-notify'); 3 | import { Uri, Emitter } from 'containerjs-api-utility'; 4 | 5 | const PERMISSION_GRANTED: ssf.NotificationPermission = 'granted'; 6 | 7 | // Image style moves the image below the icon and title/body, 8 | // To match the style of native notifications 9 | const imageStyle = { 10 |       overflow:  'hidden', 11 | display:  'block', 12 | position: 'absolute', 13 | bottom: 10 14 |     }; 15 | const HEIGHT_WITHOUT_IMAGE = 65; 16 | const HEIGHT_WITH_IMAGE = 100; 17 | 18 | export class Notification extends Emitter implements ssf.Notification { 19 | constructor(title: string, options: ssf.NotificationOptions) { 20 | super(); 21 | 22 | if (!options) { 23 | options = {}; 24 | } 25 | 26 | eNotify.setConfig({ 27 | appIcon: Uri.getAbsoluteUrl(options.icon), 28 | height:  options.image ? HEIGHT_WITH_IMAGE : HEIGHT_WITHOUT_IMAGE, 29 | defaultStyleImage:  imageStyle 30 | }); 31 | 32 | eNotify.notify({ 33 | title, 34 | text: options.body, 35 | image: Uri.getAbsoluteUrl(options.image), 36 | onClickFunc: data => this.emit('click', data) 37 | }); 38 | } 39 | 40 | static requestPermission(): Promise { 41 | return Promise.resolve(PERMISSION_GRANTED); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/api-electron/src/preload/screen.ts: -------------------------------------------------------------------------------- 1 | const { remote } = require('electron'); 2 | const ElectronScreen = remote.screen; 3 | 4 | const electronDisplayMap = (display: Electron.Display, primary: boolean): ssf.Display => { 5 | return { 6 | id: display.id.toString(), 7 | rotation: display.rotation, 8 | scaleFactor: display.scaleFactor, 9 | bounds: display.bounds, 10 | primary 11 | }; 12 | }; 13 | 14 | export class Screen implements ssf.Screen { 15 | static getDisplays() { 16 | return new Promise(resolve => { 17 | const primaryDisplay = electronDisplayMap(ElectronScreen.getPrimaryDisplay(), true); 18 | const nonePrimaryDisplays = ElectronScreen.getAllDisplays() 19 | .filter(d => d.id.toString() !== primaryDisplay.id) 20 | .map(display => electronDisplayMap(display, false)); 21 | 22 | resolve([primaryDisplay].concat(nonePrimaryDisplays)); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/api-electron/src/preload/window.ts: -------------------------------------------------------------------------------- 1 | const { 2 | ipcRenderer: ipc, 3 | remote 4 | } = require('electron'); 5 | const { BrowserWindow, nativeImage } = remote; 6 | const request = remote.require('request'); 7 | import { Emitter, Display } from 'containerjs-api-utility'; 8 | import { MessageService } from './message-service'; 9 | import { IpcMessages } from '../common/constants'; 10 | 11 | let currentWindow = null; 12 | const isUrlPattern = /^https?:\/\//i; 13 | 14 | export class Window extends Emitter implements ssf.Window { 15 | innerWindow: Electron.BrowserWindow; 16 | id: string; 17 | 18 | constructor(options?: ssf.WindowOptions, callback?: (window: Window) => void, errorCallback?: () => void) { 19 | super(); 20 | 21 | MessageService.subscribe('*', 'ssf-window-message', (data?: any) => { 22 | this.emit('message', { data }); 23 | }); 24 | 25 | if (!options) { 26 | this.innerWindow = remote.getCurrentWindow(); 27 | this.id = String(this.innerWindow.id); 28 | if (callback) { 29 | callback(this); 30 | } 31 | return this; 32 | } 33 | 34 | const electronOptions = Object.assign({}, options); 35 | 36 | Display.getDisplayAlteredPosition(options.display, { x: options.x || 0, y: options.y || 0 }).then(({ x, y }) => { 37 | if (x !== undefined) { 38 | electronOptions.x = x; 39 | } 40 | if (y !== undefined) { 41 | electronOptions.y = y; 42 | } 43 | 44 | // Allow relative urls (e.g. /index.html and demo/demo.html) 45 | if (!isUrlPattern.test(electronOptions.url) && electronOptions.url !== 'about:blank') { 46 | if (electronOptions.url.startsWith('/')) { 47 | // File at root 48 | electronOptions.url = location.origin + electronOptions.url; 49 | } else { 50 | // Relative to current file 51 | const pathSections = location.pathname.split('/').filter(x => x); 52 | pathSections.splice(-1); 53 | const currentPath = pathSections.join('/'); 54 | electronOptions.url = location.origin + '/' + currentPath + electronOptions.url; 55 | } 56 | } 57 | 58 | const features = Object.assign({}, electronOptions, { title: electronOptions.name }); 59 | 60 | this.id = ipc.sendSync(IpcMessages.IPC_SSF_NEW_WINDOW, { 61 | url: features.url, 62 | name: features.name, 63 | features 64 | }); 65 | this.innerWindow = BrowserWindow.fromId(parseInt(this.id)); 66 | 67 | if (callback) { 68 | callback(this); 69 | } 70 | }); 71 | } 72 | 73 | blur() { 74 | return this.asPromise(this.innerWindow.blur); 75 | } 76 | 77 | close() { 78 | return this.asPromise(this.innerWindow.close); 79 | } 80 | 81 | flashFrame(flag: boolean) { 82 | return this.asPromise(this.innerWindow.flashFrame, flag); 83 | } 84 | 85 | focus() { 86 | return this.asPromise(this.innerWindow.focus); 87 | } 88 | 89 | getBounds() { 90 | return this.asPromise(this.innerWindow.getBounds); 91 | } 92 | 93 | getId() { 94 | return this.id; 95 | } 96 | 97 | getMaximumSize() { 98 | return this.asPromise>(this.innerWindow.getMaximumSize); 99 | } 100 | 101 | getMinimumSize() { 102 | return this.asPromise>(this.innerWindow.getMinimumSize); 103 | } 104 | 105 | getParentWindow() { 106 | return this.asPromise(this.innerWindow.getParentWindow).then((win) => { 107 | if (win) { 108 | const parentWin = new Window(null, null, null); 109 | parentWin.innerWindow = win; 110 | parentWin.id = String(win.id); 111 | return parentWin; 112 | } 113 | return null; 114 | }); 115 | } 116 | 117 | getPosition() { 118 | return this.asPromise>(this.innerWindow.getPosition); 119 | } 120 | 121 | getSize() { 122 | return this.asPromise>(this.innerWindow.getSize); 123 | } 124 | 125 | getTitle() { 126 | return this.asPromise(this.innerWindow.getTitle); 127 | } 128 | 129 | hasShadow() { 130 | return this.asPromise(this.innerWindow.hasShadow); 131 | } 132 | 133 | hide() { 134 | return this.asPromise(this.innerWindow.hide); 135 | } 136 | 137 | isAlwaysOnTop() { 138 | return this.asPromise(this.innerWindow.isAlwaysOnTop); 139 | } 140 | 141 | isMaximizable() { 142 | return this.asPromise(this.innerWindow.isMaximizable); 143 | } 144 | 145 | isMaximized() { 146 | return this.asPromise(this.innerWindow.isMaximized); 147 | } 148 | 149 | isMinimizable() { 150 | return this.asPromise(this.innerWindow.isMinimizable); 151 | } 152 | 153 | isMinimized() { 154 | return this.asPromise(this.innerWindow.isMinimized); 155 | } 156 | 157 | isResizable() { 158 | return this.asPromise(this.innerWindow.isResizable); 159 | } 160 | 161 | isVisible() { 162 | return this.asPromise(this.innerWindow.isVisible); 163 | } 164 | 165 | loadURL(url: string) { 166 | return this.asPromise(this.innerWindow.loadURL, url); 167 | } 168 | 169 | maximize() { 170 | return this.asPromise(this.innerWindow.maximize); 171 | } 172 | 173 | minimize() { 174 | return this.asPromise(this.innerWindow.minimize); 175 | } 176 | 177 | reload() { 178 | return this.asPromise(this.innerWindow.reload); 179 | } 180 | 181 | restore() { 182 | return this.asPromise(this.innerWindow.restore); 183 | } 184 | 185 | setAlwaysOnTop(alwaysOnTop: boolean) { 186 | return this.asPromise(this.innerWindow.setAlwaysOnTop, alwaysOnTop); 187 | } 188 | 189 | setBounds(bounds: ssf.Rectangle) { 190 | return this.asPromise(this.innerWindow.setBounds, bounds); 191 | } 192 | 193 | setIcon(icon: string) { 194 | const req = request.defaults({ encoding: null }); 195 | return new Promise((resolve, reject) => { 196 | req.get(icon, (err, res, body) => { 197 | const image = nativeImage.createFromBuffer(Buffer.from(body)); 198 | if (image.isEmpty()) { 199 | reject(new Error('Image could not be created from the URL')); 200 | } 201 | this.asPromise(this.innerWindow.setIcon, image).then(() => { 202 | resolve(); 203 | }); 204 | } 205 | ); 206 | }); 207 | } 208 | 209 | setMaximizable(maximizable: boolean) { 210 | return this.asPromise(this.innerWindow.setMaximizable, maximizable); 211 | } 212 | 213 | setMaximumSize(width: number, height: number) { 214 | return this.asPromise(this.innerWindow.setMaximumSize, width, height); 215 | } 216 | 217 | setMinimizable(minimizable: boolean) { 218 | return this.asPromise(this.innerWindow.setMinimizable, minimizable); 219 | } 220 | 221 | setMinimumSize(width: number, height: number) { 222 | return this.asPromise(this.innerWindow.setMinimumSize, width, height); 223 | } 224 | 225 | setPosition(x: number, y: number) { 226 | return this.asPromise(this.innerWindow.setPosition, x, y); 227 | } 228 | 229 | setResizable(resizable: boolean) { 230 | return this.asPromise(this.innerWindow.setResizable, resizable); 231 | } 232 | 233 | setSize(width: number, height: number) { 234 | return this.asPromise(this.innerWindow.setSize, width, height); 235 | } 236 | 237 | setSkipTaskbar(skipTaskbar: boolean) { 238 | return this.asPromise(this.innerWindow.setSkipTaskbar, skipTaskbar); 239 | } 240 | 241 | show() { 242 | return this.asPromise(this.innerWindow.show); 243 | } 244 | 245 | unmaximize() { 246 | return this.asPromise(this.innerWindow.unmaximize); 247 | } 248 | 249 | asPromise(windowFunction: (...args: any[]) => any, ...args: any[]): Promise { 250 | return new Promise((resolve) => { 251 | resolve(windowFunction(...args)); 252 | }); 253 | } 254 | 255 | innerAddEventListener(event: string, listener: (...args: any[]) => void) { 256 | (this.innerWindow as NodeJS.EventEmitter).addListener(event, listener); 257 | } 258 | 259 | innerRemoveEventListener(event: string, listener: (...args: any[]) => void) { 260 | (this.innerWindow as NodeJS.EventEmitter).removeListener(event, listener); 261 | } 262 | 263 | postMessage(message: any) { 264 | MessageService.send(this.id, 'ssf-window-message', message); 265 | } 266 | 267 | getChildWindows() { 268 | return new Promise>(resolve => { 269 | const children = []; 270 | this.innerWindow.getChildWindows().forEach(win => { 271 | const child = new Window(null, null, null); 272 | child.innerWindow = win; 273 | child.id = String(win.id); 274 | children.push(child); 275 | }); 276 | resolve(children); 277 | }); 278 | } 279 | 280 | static getCurrentWindow(callback: (win: Window) => void, errorCallback: (err?: any) => void) { 281 | if (currentWindow) { 282 | return currentWindow; 283 | } 284 | 285 | currentWindow = new Window(null, callback, errorCallback); 286 | return currentWindow; 287 | } 288 | 289 | static wrap(win: Electron.BrowserWindow) { 290 | const wrappedWindow = new Window(); 291 | wrappedWindow.innerWindow = win; 292 | wrappedWindow.id = String(win.id); 293 | return wrappedWindow; 294 | } 295 | 296 | static getById(id: string) { 297 | return new Promise(resolve => { 298 | if (!isNaN(parseInt(id))) { 299 | const bw = BrowserWindow.fromId(parseInt(id)); 300 | resolve(bw === null ? null : Window.wrap(bw)); 301 | return; 302 | } 303 | resolve(null); 304 | }); 305 | } 306 | 307 | static getAll() { 308 | return new Promise(resolve => resolve(BrowserWindow.getAllWindows().map(Window.wrap))); 309 | } 310 | 311 | capture() { 312 | return new Promise((resolve) => { 313 | this.innerWindow.capturePage((image: any) => { 314 | const dataUri = 'data:image/png;base64,' + image.toPng().toString('base64'); 315 | this.emit('capture'); 316 | resolve(dataUri); 317 | }); 318 | }); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /packages/api-electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "dom.iterable", 9 | "scripthost" 10 | ], 11 | "outDir": "./build/es", 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "./preload.ts", 18 | "node_modules/containerjs-api-specification/interface/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-openfin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "fin": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/api-openfin/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | src/ 4 | build/es/ 5 | .eslintrc.json 6 | index.ts 7 | rollup.config.json 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /packages/api-openfin/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS API for OpenFin 2 | 3 | This project provides an implementation of ContainerJS which follows the Symphony Desktop Wrapper API Specification for OpenFin 4 | For more information, see [The ContainerJS website](https://symphonyoss.github.io/ContainerJS/) 5 | 6 | ## Usage 7 | 8 | Add this package to your OpenFin project: 9 | 10 | ``` 11 | npm install containerjs-api-openfin --save 12 | ``` 13 | 14 | ### Running the app 15 | 16 | Use the `ssf-openfin` binary to run the application. This can be installed globally by doing: 17 | 18 | ``` 19 | npm install --global containerjs-api-electron 20 | 21 | Usage: ssf-openfin [options] 22 | 23 | Options: 24 | 25 | -V, --version output the version number 26 | -u, --url [url] Launch url for the application (can be specified in --config instead) 27 | -c, --config [filename] (Optional) ContainerJS config file 28 | -s, --symphony (Optional) Use Symphony compatibility layer 29 | -d, --developer (Optional) Show developer context menu 30 | -o, --output-config [filename] (Optional) Where to output the OpenFin config file 31 | -C, --config-url [url] (Optional) Url to read the new app.json file from to start OpenFin 32 | -f, --openfin-version [version] (Optional) Version of the OpenFin runtime to use, default is stable 33 | -n, --notification [directory] (Optional) Generate an example notification file in the specified directory 34 | -h, --help output usage information 35 | ``` 36 | 37 | To run the application, do: 38 | 39 | ``` 40 | ssf-openfin --url http://website/index.html 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /packages/api-openfin/bin/ssf-openfin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const openfinLauncher = require('openfin-launcher'); 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | const os = require('os'); 7 | const program = require('commander'); 8 | const fetch = require('node-fetch'); 9 | const packageJson = require('../package.json'); 10 | const promisify = require('util.promisify'); 11 | const readFileAsync = promisify(fs.readFile); 12 | 13 | program 14 | .version(packageJson.version) 15 | .option('-u, --url [url]', 'Launch url for the application (can be specified in --config instead)', null) 16 | .option('-c, --config [filename]', '(Optional) ContainerJS config file', null) 17 | .option('-s, --symphony', '(Optional) Use Symphony compatibility layer', (v, val) => true, false) 18 | .option('-d, --developer', '(Optional) Show developer context menu', (v, val) => true, false) 19 | .option('-o, --output-config [filename]', '(Optional) Where to output the OpenFin config file', null) 20 | .option('-C, --config-url [url]', '(Optional) Url to read the new app.json file from to start OpenFin') 21 | .option('-f, --openfin-version [version]', '(Optional) Version of the OpenFin runtime to use, default is stable', 'stable') 22 | .option('-n, --notification [directory]', '(Optional) Generate an example notification file in the specified directory') 23 | .parse(process.argv); 24 | 25 | const openfinConfigFile = program.outputConfig 26 | ? path.join(process.cwd(), program.outputConfig) 27 | : path.join(os.tmpdir(), 'ssf-openfin-app-config.json'); 28 | 29 | const preloadFile = program.symphony ? 'containerjs-api-symphony.js' : 'containerjs-api.js'; 30 | const preloadPath = path.join(__dirname, `../build/dist/${preloadFile}`); 31 | const notificationFile = path.join(__dirname, '../build/dist/notification.html'); 32 | 33 | const openfinConfig = { 34 | 'devtools_port': 9090, 35 | 'startup_app': {}, 36 | 'runtime': { 37 | 'version': program.openfinVersion 38 | }, 39 | 'shortcut': {} 40 | }; 41 | 42 | loadConfig().then(parsedConfig => { 43 | openfinConfig.startup_app = prepareConfig(parsedConfig); 44 | 45 | fs.writeFileSync(openfinConfigFile, JSON.stringify(openfinConfig)); 46 | if (program.notification) { 47 | fs.copySync(notificationFile, program.notification); 48 | } 49 | 50 | openfinLauncher.launchOpenFin({ 51 | configPath: program.configUrl || openfinConfigFile 52 | }) 53 | .fail(() => { 54 | consoleError('Failed to launch OpenFin'); 55 | }); 56 | }).catch(err => { 57 | consoleError(err.message); 58 | process.exit(); 59 | }); 60 | 61 | function loadConfig() { 62 | if (program.config) { 63 | return readConfigFile() 64 | .then(config => { 65 | if (program.url) { 66 | // Overridden by parameter 67 | config.url = program.url; 68 | } 69 | 70 | if (config.url) { 71 | return config; 72 | } else { 73 | throw new Error('You must specify an URL (--url) or a config file containing an url (--config)'); 74 | } 75 | }); 76 | } else { 77 | if (program.url) { 78 | return Promise.resolve({ 79 | url: program.url 80 | }); 81 | } else { 82 | return Promise.reject(new Error('You must specify an URL (--url) or a config file containing an url (--config)')); 83 | } 84 | } 85 | } 86 | 87 | function readConfigFile() { 88 | const filePath = program.config; 89 | if (filePath.toLowerCase().startsWith('http://') || filePath.toLowerCase().startsWith('https://')) { 90 | return fetch(filePath) 91 | .then(res => res.json()); 92 | } else { 93 | const configFile = path.join(process.cwd(), program.config); 94 | if (fs.existsSync(configFile)) { 95 | return readFileAsync(configFile) 96 | .then(data => JSON.parse(data)); 97 | } else { 98 | return Promise.reject(new Error(`Config file ${configFile} does not exist`)); 99 | } 100 | } 101 | } 102 | 103 | function prepareConfig(config) { 104 | config.preload = preloadPath; 105 | config.contextMenu = !!program.developer; 106 | 107 | // Make it so that neither name or uuid are required properties 108 | const appId = getAppId(config); 109 | if (!config.name) { 110 | config.name = appId; 111 | } 112 | if (!config.uuid) { 113 | config.uuid = appId; 114 | } 115 | 116 | if (config.autoShow === undefined) { 117 | // Default to autoShow if not specified 118 | config.autoShow = true; 119 | } 120 | 121 | // Default size matches Electron config 122 | if (!config.defaultWidth) { 123 | config.defaultWidth = 800; 124 | } 125 | if (!config.defaultHeight) { 126 | config.defaultHeight = 600; 127 | } 128 | 129 | return config; 130 | } 131 | 132 | function getAppId(config) { 133 | const appId = config.uuid || config.name; 134 | 135 | if (!appId) { 136 | // Generate an app Id from the url 137 | return config.url 138 | .replace(/(http:\/\/|https:\/\/)/, '') 139 | .replace(/(:|\/|\.)/g, '_') 140 | } 141 | return appId; 142 | } 143 | 144 | function consoleError(err) { 145 | console.error('\x1b[31m', err, '\x1b[37m'); 146 | } 147 | -------------------------------------------------------------------------------- /packages/api-openfin/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from './src/app'; 2 | import { MessageService } from './src/message-service'; 3 | import { Window } from './src/window'; 4 | import { Notification } from './src/notification'; 5 | import { Screen } from './src/screen'; 6 | 7 | export { 8 | app, 9 | MessageService, 10 | Window, 11 | Notification, 12 | Screen 13 | }; 14 | -------------------------------------------------------------------------------- /packages/api-openfin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-openfin", 3 | "version": "0.0.9", 4 | "description": "The ContainerJS OpenFin API", 5 | "main": "build/lib/index.js", 6 | "module": "build/es/index.js", 7 | "jsnext:main": "build/es/index.js", 8 | "browser": "build/dist/containerjs-api.js", 9 | "scripts": { 10 | "clean": "rimraf build", 11 | "build": "npm run clean && tsc && rollup -c && copyfiles -f src/notification.html ./build/dist && npm run bundle", 12 | "bundle": "concat -o build/dist/containerjs-api-symphony.js build/dist/containerjs-api.js node_modules/containerjs-api-compatibility/build/dist/containerjs-api-compatibility.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 17 | }, 18 | "author": "", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 22 | }, 23 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 24 | "devDependencies": { 25 | "@types/openfin": "^17.0.2", 26 | "concat": "^1.0.3", 27 | "containerjs-api-compatibility": "^0.0.6", 28 | "containerjs-api-specification": "^0.0.7", 29 | "containerjs-api-utility": "^0.0.5", 30 | "copyfiles": "^1.2.0", 31 | "rimraf": "^2.6.1", 32 | "rollup": "^0.41.6", 33 | "rollup-plugin-node-resolve": "^3.0.0", 34 | "typescript": "^2.4.1" 35 | }, 36 | "bin": { 37 | "ssf-openfin": "./bin/ssf-openfin" 38 | }, 39 | "dependencies": { 40 | "commander": "^2.10.0", 41 | "fs-extra": "^4.0.0", 42 | "node-fetch": "^1.7.3", 43 | "openfin-launcher": "^1.3.12", 44 | "util.promisify": "^1.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/api-openfin/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | 3 | export default { 4 | entry: 'build/es/index.js', 5 | format: 'umd', 6 | moduleName: 'ssf', 7 | dest: 'build/dist/containerjs-api.js', 8 | plugins: [ 9 | resolve({ 10 | jsnext: true, 11 | main: true 12 | }) 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /packages/api-openfin/src/app.ts: -------------------------------------------------------------------------------- 1 | import { createMainProcess } from './main-process'; 2 | 3 | let initialisePromise = null; 4 | export class app implements ssf.app { 5 | static ready() { 6 | if (!initialisePromise) { 7 | initialisePromise = new Promise((resolve) => { 8 | fin.desktop.main(() => createMainProcess(resolve)); 9 | }); 10 | } 11 | return initialisePromise; 12 | } 13 | 14 | static setBadgeCount(count: number) { 15 | // Openfin doesn't support badge count 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/api-openfin/src/index.ts: -------------------------------------------------------------------------------- 1 | import './window.interface'; 2 | import './message-service.interface'; 3 | -------------------------------------------------------------------------------- /packages/api-openfin/src/main-process.ts: -------------------------------------------------------------------------------- 1 | import MessageService from './message-service'; 2 | 3 | type ChildTree = { 4 | name: string, 5 | children: ChildTree[] 6 | }; 7 | 8 | type NewWindowMessage = { 9 | windowName: string, 10 | parentName: string 11 | }; 12 | // Code that is "evaled" when the main window has been opened, sets up 13 | // All the InterApplicationBus listeners for window events to keep track of state 14 | const mainWindowCode = () => { 15 | const childTree: ChildTree[] = []; 16 | 17 | const addParent = (parentName: string, windowName: string, tree: ChildTree[]): boolean => { 18 | const win = tree.find(w => w.name === parentName); 19 | if (win) { 20 | win.children.push({ 21 | name: windowName, 22 | children: [] 23 | }); 24 | return true; 25 | } else { 26 | return tree.some((childWin) => { 27 | return addParent(parentName, windowName, childWin.children); 28 | }); 29 | } 30 | }; 31 | 32 | const getParentWindow = (name: string, parentName: string, tree: ChildTree[]) => { 33 | const win = tree.find(w => w.name === name); 34 | if (win) { 35 | return parentName; 36 | } else { 37 | let parentName: string = null; 38 | tree.some((childWin) => { 39 | parentName = getParentWindow(name, childWin.name, childWin.children); 40 | return parentName !== null; 41 | }); 42 | return parentName; 43 | } 44 | }; 45 | 46 | const getChildWindows = (name: string, tree: ChildTree[]) => { 47 | const win = tree.find(w => w.name === name); 48 | if (win) { 49 | return win.children.map(c => c.name); 50 | } else { 51 | let childNames: string[] = []; 52 | tree.some((childWin) => { 53 | childNames = getChildWindows(name, childWin.children); 54 | return childNames && childNames.length > 0; 55 | }); 56 | return childNames; 57 | } 58 | }; 59 | 60 | const deleteWindow = (name: string, tree: ChildTree[]) => { 61 | const index = tree.findIndex(w => w.name === name); 62 | if (index >= 0) { 63 | tree.splice(index, 1); 64 | } else { 65 | tree.forEach((childWin) => { 66 | deleteWindow(name, childWin.children); 67 | }); 68 | } 69 | }; 70 | 71 | const closeChildren = (uuid: string) => { 72 | const childUuids = getChildWindows(uuid, childTree); 73 | childUuids.forEach((child) => { 74 | fin.desktop.Application.wrap(child).close(true); 75 | closeChildren(child); 76 | }); 77 | }; 78 | 79 | const getAllWindows = (node: ChildTree[]) => { 80 | let windows = []; 81 | node.forEach((win) => { 82 | windows.push(win.name); 83 | if (win.children) { 84 | windows = windows.concat(getAllWindows(win.children)); 85 | } 86 | }); 87 | return windows; 88 | }; 89 | 90 | fin.desktop.InterApplicationBus.subscribe('*', 'ssf-new-window', (data: NewWindowMessage) => { 91 | const app = fin.desktop.Application.wrap(data.windowName); 92 | app.getWindow().addEventListener('closed', () => { 93 | closeChildren(data.windowName); 94 | deleteWindow(data.windowName, childTree); 95 | // If that was the last window to close, force close this window too 96 | if (childTree.length === 0) { 97 | fin.desktop.Window.getCurrent().close(); 98 | } 99 | }); 100 | 101 | if (data.parentName === null) { 102 | childTree.push({ 103 | name: data.windowName, 104 | children: [] 105 | }); 106 | } else { 107 | if (!addParent(data.parentName, data.windowName, childTree)) { 108 | // No parent in tree, make a new one 109 | childTree.push({ 110 | name: data.parentName, 111 | children: [{ 112 | name: data.windowName, 113 | children: [] 114 | }] 115 | }); 116 | } 117 | } 118 | }); 119 | 120 | fin.desktop.InterApplicationBus.subscribe('*', 'ssf-get-parent-window', (name: string, uuid: string) => { 121 | const parent = getParentWindow(name, null, childTree); 122 | fin.desktop.InterApplicationBus.send(uuid, 'ssf-parent-window', parent); 123 | }); 124 | 125 | fin.desktop.InterApplicationBus.subscribe('*', 'ssf-get-child-windows', (name: string, uuid: string) => { 126 | const children = getChildWindows(name, childTree); 127 | fin.desktop.InterApplicationBus.send(uuid, 'ssf-child-windows', children); 128 | }); 129 | 130 | fin.desktop.InterApplicationBus.subscribe('*', 'ssf-get-all-windows', (data: any, uuid: string) => { 131 | const windows = getAllWindows(childTree); 132 | fin.desktop.InterApplicationBus.send(uuid, 'ssf-all-windows', windows); 133 | }); 134 | 135 | fin.desktop.Window.getCurrent().addEventListener('close-requested', () => { 136 | fin.desktop.InterApplicationBus.publish('ssf-close-all', ''); 137 | fin.desktop.Window.getCurrent().close(true); 138 | }); 139 | }; 140 | 141 | export const createMainProcess = (done: (err?: any) => void) => { 142 | // Populate the current window variable 143 | ssf.Window.getCurrentWindow(); 144 | 145 | fin.desktop.InterApplicationBus.subscribe('*', 'ssf-close-all', () => { 146 | fin.desktop.Window.getCurrent().close(true); 147 | }); 148 | 149 | // Create the main window, if the window already exists, the success callback isn't ran 150 | // And the already open window is returned 151 | const app = new fin.desktop.Application({ 152 | url: 'about:blank', 153 | name: 'mainWindow', 154 | uuid: 'mainWindow', 155 | mainWindowOptions: { 156 | autoShow: false 157 | } 158 | }, () => { 159 | app.run(() => { 160 | // Method executeJavaScript only takes a string, but writing the code as a string means we lose typescript checking 161 | const body = mainWindowCode.toString().slice(mainWindowCode.toString().indexOf('{') + 1, mainWindowCode.toString().lastIndexOf('}')); 162 | const mainWindow = app.getWindow(); 163 | mainWindow.executeJavaScript(body, () => { 164 | // Tell the mainWindow about this window 165 | const uuid = fin.desktop.Window.getCurrent().uuid; 166 | fin.desktop.InterApplicationBus.publish('ssf-new-window', { 167 | windowName: uuid, 168 | parentName: null 169 | }); 170 | done(); 171 | }); 172 | }); 173 | }, (err) => done(err)); 174 | }; 175 | -------------------------------------------------------------------------------- /packages/api-openfin/src/message-service.ts: -------------------------------------------------------------------------------- 1 | const listenerMap = new Map(); 2 | 3 | export class MessageService implements ssf.MessageService { 4 | // Window ID should be in the form 'application-uuid:window-name' 5 | static send(windowId: string, topic: string, message: any) { 6 | const [appId, windowName] = windowId.split(':'); 7 | 8 | if (appId && windowName) { 9 | fin.desktop.InterApplicationBus.send(appId, windowName, topic, message); 10 | } else if (appId) { 11 | fin.desktop.InterApplicationBus.send(appId, topic, message); 12 | } 13 | } 14 | 15 | static subscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void) { 16 | const thisWindowId = fin.desktop.Window.getCurrent().uuid; 17 | const receiveMessage = (message, senderId) => { 18 | // Don't send to self 19 | if (senderId !== thisWindowId) { 20 | listener(message, senderId); 21 | } 22 | }; 23 | 24 | // Map the arguments to the actual listener that was added 25 | listenerMap.set({ 26 | windowId, 27 | topic, 28 | listener 29 | }, receiveMessage); 30 | 31 | const [appId, windowName] = windowId.split(':'); 32 | 33 | if (appId && windowName) { 34 | fin.desktop.InterApplicationBus.subscribe(appId, windowName, topic, receiveMessage); 35 | } else if (appId) { 36 | fin.desktop.InterApplicationBus.subscribe(appId, topic, receiveMessage); 37 | } 38 | } 39 | 40 | static unsubscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void) { 41 | let deleteKey = null; 42 | let receiveMessage = null; 43 | 44 | // We cant use listenerMap.has() here because reconstructing the key from the arguments is a different object 45 | // I.e. {} !== {} 46 | listenerMap.forEach((value, key) => { 47 | if (key.windowId === windowId && key.topic === topic && key.listener === listener) { 48 | receiveMessage = value; 49 | deleteKey = key; 50 | } 51 | }); 52 | 53 | if (deleteKey) { 54 | listenerMap.delete(deleteKey); 55 | 56 | const [appId, windowName] = windowId.split(':'); 57 | 58 | if (appId && windowName) { 59 | fin.desktop.InterApplicationBus.unsubscribe(appId, windowName, topic, receiveMessage); 60 | } else if (appId) { 61 | fin.desktop.InterApplicationBus.unsubscribe(appId, topic, receiveMessage); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/api-openfin/src/notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 36 | 37 | 38 | 39 |
40 |

41 |

42 |
43 | 44 | 59 | 60 | -------------------------------------------------------------------------------- /packages/api-openfin/src/notification.ts: -------------------------------------------------------------------------------- 1 | import { Uri, Emitter } from 'containerjs-api-utility'; 2 | const PERMISSION_GRANTED: ssf.NotificationPermission = 'granted'; 3 | const DEFAULT_TEMPLATE: string = 'notification.html'; 4 | 5 | export class Notification extends Emitter implements ssf.Notification { 6 | constructor(title: string, options: ssf.NotificationOptions) { 7 | super(); 8 | 9 | if (!options) { 10 | options = {}; 11 | } 12 | 13 | const message = { 14 | title, 15 | text: options.body, 16 | image: Uri.getAbsoluteUrl(options.image), 17 | icon: Uri.getAbsoluteUrl(options.icon) 18 | }; 19 | 20 | const template = options.template || DEFAULT_TEMPLATE; 21 | 22 | new fin.desktop.Notification({ 23 | url: Uri.getAbsoluteUrl(template), 24 | message, 25 | onClick: data => this.emit('click', data) 26 | }); 27 | } 28 | 29 | static requestPermission(): Promise { 30 | return Promise.resolve(PERMISSION_GRANTED); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/api-openfin/src/screen.ts: -------------------------------------------------------------------------------- 1 | const ofDisplayMap = (display: fin.MonitorInfoDetail, primary: boolean): ssf.Display => { 2 | return { 3 | id: display.deviceId, 4 | rotation: window.screen.orientation.angle, 5 | scaleFactor: window.devicePixelRatio, 6 | primary, 7 | bounds: { 8 | x: display.monitorRect.left, 9 | y: display.monitorRect.top, 10 | width: display.monitorRect.right - display.monitorRect.left, 11 | height: display.monitorRect.bottom - display.monitorRect.top 12 | } 13 | }; 14 | }; 15 | 16 | export class Screen implements ssf.Screen { 17 | static getDisplays() { 18 | return new Promise(resolve => { 19 | fin.desktop.System.getMonitorInfo((info) => { 20 | const displays = []; 21 | info.nonPrimaryMonitors.forEach(monitor => displays.push(ofDisplayMap(monitor, false))); 22 | displays.push(ofDisplayMap(info.primaryMonitor, true)); 23 | resolve(displays); 24 | }); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/api-openfin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "dom.iterable", 9 | "scripthost" 10 | ], 11 | "outDir": "./build/es", 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "./index.ts", 18 | "node_modules/containerjs-api-specification/interface/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-specification/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | test-report.json 4 | transform-type-info.js 5 | type-info.json 6 | -------------------------------------------------------------------------------- /packages/api-specification/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS API Specification 2 | 3 | This project defines the Symphony Desktop Wrapper API Specification 4 | For more information, see [The ContainerJS website](https://symphonyoss.github.io/ContainerJS/) 5 | -------------------------------------------------------------------------------- /packages/api-specification/interface/app.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | /** 3 | * Manage the ContainerJS application 4 | */ 5 | class app { 6 | /** 7 | * Wait until the API has bootstrapped before it is ready to use 8 | * 9 | * Note that some APIs may fail if the application tries to call 10 | * them before the API layer has finished bootstrapping. 11 | * 12 | *
13 |      * ssf.app.ready().then(() => {
14 |      *  console.log('Application is running');
15 |      * });
16 |      * 
17 | * 18 | * @returns A promise that resolves when the API has finished bootstrapping. 19 | */ 20 | static ready(): Promise; 21 | 22 | /** 23 | * Sets the counter badge for current app. Setting the count to 0 will hide the badge. This 24 | * is currently only supported in Electron on Mac and Linux. 25 | * @param count - the integer count for the app. 26 | */ 27 | static setBadgeCount(count: number); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/api-specification/interface/event-emitter.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | /** 3 | * Exposes methods that allow subscribing to particular events 4 | * 5 | * EventEmitter is the base class for event emitting objects, 6 | * such as Window. 7 | * 8 | *
  9 |    * ssf.Window.getCurrentWindow().on('focus', () => {
 10 |    *  console.log('Window received focus');
 11 |    * });
 12 |    * 
13 | */ 14 | abstract class EventEmitter { 15 | /** 16 | * Adds a listener that runs when the specified event occurs. Alias for on(). 17 | * 18 | *
 19 |      * window.addListener('blur', () => {
 20 |      *   console.log('blurred');
 21 |      * });
 22 |      * 
23 | * 24 | * @param event The event to listen for. 25 | * @param listener The function to run when the event occurs. 26 | */ 27 | addListener(event: string, listener: Function): EventEmitter; 28 | 29 | /** 30 | * Adds a listener that runs when the specified event occurs. Alias for addListener(). 31 | * 32 | *
 33 |      * window.on('blur', () => {
 34 |      *   console.log('blurred');
 35 |      * });
 36 |      * 
37 | * 38 | * @param event The event to listen for. 39 | * @param listener The function to run when the event occurs. 40 | */ 41 | on(event: string, listener: Function): EventEmitter; 42 | 43 | /** 44 | * Adds a listener that runs once when the specified event occurs, then is removed. 45 | * 46 | *
 47 |      * window.once('show', () => {
 48 |      *   console.log('shown');
 49 |      * });
 50 |      * 
51 | * 52 | * @param event The event to listen for. 53 | * @param listener The function to run once when the event occurs. 54 | */ 55 | once(event: string, listener: Function): EventEmitter; 56 | 57 | /** 58 | * Get all event names with active listeners. 59 | */ 60 | eventNames(): Array; 61 | 62 | /** 63 | * Get the number of listeners currently listening for an event. 64 | * @param event The event to get the number of listeners for. 65 | */ 66 | listenerCount(event: string): number; 67 | 68 | /** 69 | * Get all listeners for an event. 70 | * @param event The event to get the listeners for. 71 | */ 72 | listeners(event: string): Array; 73 | 74 | /** 75 | * Remove a listener from an event. Note: this must be the same function object. 76 | * 77 | *
 78 |      * const listener = () => {
 79 |      *   console.log('blurred');
 80 |      * };
 81 |      * window.addListener('blur', listener);
 82 |      *
 83 |      * //...
 84 |      *
 85 |      * window.removeListener('blur', listener);
 86 |      * 
87 | * 88 | * @param event The event to remove the listener from. 89 | * @param listener The listener to remove. Must be the same object that was passed to addListener() 90 | */ 91 | removeListener(event: string, listener: Function): EventEmitter; 92 | 93 | /** 94 | * Removes all listeners from a given event, or all events if no event is passed. 95 | * @param event The event to remove the listeners from. 96 | */ 97 | removeAllListeners(event?: string): EventEmitter; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/api-specification/interface/message-service.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | /** 3 | * Send messages between windows 4 | * 5 | * Messages can be sent between windows that are part of the same application. 6 | * Subscribe to messages based on a topic name, and either send messages 7 | * from or to a particular window id, or else distribute to all listening windows. 8 | * Messages are sent asynchronously. 9 | * 10 | *
11 |    * // Listen for messages from any window with the subject "test-subject"
12 |    * ssf.MessageService.subscribe('*', 'test-subject', (message, sender) => {
13 |    *   console.log(`${message} from ${sender}`);
14 |    * });
15 |    *
16 |    * // Send a message to all windows listening to "test-subject"
17 |    * ssf.MessageService.send('*', 'test-subject', 'This is a test message');
18 |    * 
19 | */ 20 | class MessageService { 21 | /** 22 | * Send a message to a specific window 23 | * @param windowId - The id of the window to send the message to. Can be a wildcard '*' to send to all listening windows. 24 | * @param topic - The topic of the message. 25 | * @param message - The message to send. 26 | */ 27 | static send(windowId: string, topic: string, message: string|object): void; 28 | 29 | /** 30 | * Subscribe to message from a window/topic 31 | * @param windowId - The id of the window to listen to messages from. Can be a wildcard '*' to listen to all windows. 32 | * @param topic - The topic to listen for. 33 | * @param listener - The function to run when a message is received. The message and sender window id are passed as a parameter to the function. 34 | */ 35 | static subscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void): void; 36 | 37 | /** 38 | * Unsubscribe from a window/topic 39 | * @param windowId - The id of the window that the listener was subscribed to or the wildcard '*'. 40 | * @param topic - The topic that was being listened to. 41 | * @param listener - The function that was passed to subscribe. Note: this must be the same function object. 42 | */ 43 | static unsubscribe(windowId: string, topic: string, listener: (message: string|object, sender: string) => void): void; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/api-specification/interface/notification.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | 3 | /** 4 | * Options that can be passed to the notification constructor 5 | */ 6 | interface NotificationOptions { 7 | /** 8 | * The text to display underneath the title text. 9 | */ 10 | body?: string; 11 | /** 12 | * The URL of an icon to be displayed in the notification. 13 | */ 14 | icon?: string; 15 | /** 16 | * The URL of an image to be displayed in the notification. 17 | */ 18 | image?: string; 19 | /** 20 | * The URL of the notification template for OpenFin. Can be 21 | * relative to the current URL. Default: "template.html" 22 | */ 23 | template?: string; 24 | } 25 | type NotificationPermission = 'default' | 'denied' | 'granted'; 26 | 27 | /** 28 | * Events that are fired by Notification 29 | */ 30 | interface NotificationEvent { 31 | /** Fires when the notification is clicked */ 32 | click: 'click'; 33 | } 34 | 35 | /** 36 | * Creates a new Desktop Notification. 37 | * 38 | * Notifications are created via a constructor which takes a title and 39 | * an options object with body text and additional view options. 40 | * 41 | * A Notification emits click events when the user clicks on it 42 | * 43 | *
44 |    * ssf.Notification.requestPermission().then(result => {
45 |    *   if (result === 'granted') {
46 |    *     const notification = new Notification(
47 |    *       'My Title',
48 |    *       {
49 |    *         body: 'My body text'
50 |    *       });
51 |    *
52 |    *     notification.on('click', () => {
53 |    *       // Respond to click event
54 |    *       console.log('Notification was clicked');
55 |    *     });
56 |    *   }
57 |    * })
58 |    * 
59 | * 60 | * OpenFin requires a template html file to render the notification. 61 | * A template (notification.html) is included with the containerjs bundle, 62 | * and by default ssf.Notification will try to find it in the same location 63 | * as the current URL. To specify a different URL for the template, set the 64 | * template setting in NotificationOptions 65 | * 66 | *
67 |    * const notification = new ssf.Notification(
68 |    *   'My Title',
69 |    *   {
70 |    *     body: 'My body text',
71 |    *     template: '/resource/notification.html'
72 |    *   });
73 |    * 
74 | * 75 | * Notification is an EventEmitter. 76 | * See NotificationEvent for a list of events. 77 | */ 78 | class Notification extends ssf.EventEmitter { 79 | /** 80 | * Create a notification 81 | * @param title - The title text of the notification. 82 | * @param options - The notification options. 83 | */ 84 | constructor(title: string, options: NotificationOptions); 85 | 86 | /** 87 | * Request permission to create notifications 88 | * 89 | * If required, ask the user for permission to create desktop notifications. 90 | * Some containers don't require permission so will resolve the promise 91 | * immediately with the result "granted" 92 | * 93 | * @returns A promise which resolves to a string value "granted" or "denied". 94 | */ 95 | static requestPermission(): Promise; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/api-specification/interface/openfin-extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * @hidden 4 | */ 5 | declare namespace fin { 6 | interface OpenFinWindow { 7 | uuid: string; 8 | executeJavaScript(code: string, callback?: Function, errorCallback?: Function): void; 9 | } 10 | 11 | interface WindowOptions { 12 | preload?: string; 13 | title?: string; 14 | shadow?: boolean; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/api-specification/interface/position.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | /** 3 | * Position of an object on screen 4 | */ 5 | export interface Position { 6 | /** 7 | * Horizontal position in pixels 8 | */ 9 | x: number; 10 | /** 11 | * Vertical position in pixels 12 | */ 13 | y: number; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/api-specification/interface/rectangle.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | /** 3 | * Position and size of an object on screen 4 | */ 5 | export interface Rectangle { 6 | /** 7 | * Horizontal position in pixels 8 | */ 9 | x: number; 10 | /** 11 | * Vertical position in pixels 12 | */ 13 | y: number; 14 | /** 15 | * Width in pixels 16 | */ 17 | width: number; 18 | /** 19 | * Height in pixels 20 | */ 21 | height: number; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/api-specification/interface/screen.ts: -------------------------------------------------------------------------------- 1 | declare namespace ssf { 2 | /** 3 | * Exposes methods that relate to the Screen and Display Monitors. 4 | * 5 | *
 6 |    * ssf.Screen.getDisplays().then(displays => {
 7 |    *   console.log(`${displays.length} displays`);
 8 |    * });
 9 |    * 
10 | */ 11 | export abstract class Screen { 12 | /** 13 | * Get all the monitor displays that are available. 14 | * 15 | * Note that the Browser API does not support multiple displays, so it 16 | * assumes the display the browser is running in is the only display. 17 | * @returns A promise which resolves to an array of available displays. 18 | */ 19 | static getDisplays(): Promise>; 20 | } 21 | 22 | /** 23 | * Information about the user's display. Note that the Browser API does not support multiple 24 | * displays, so it assumes the display the browser is running in is the only display. 25 | * 26 | * The Screen class can be used to retrieve display information. 27 | */ 28 | export interface Display { 29 | /** 30 | * Unique Id of the display. 31 | */ 32 | id: string; 33 | 34 | /** 35 | * Current rotation of the display, can be 0, 90, 180, 270. 36 | */ 37 | rotation: number; 38 | 39 | /** 40 | * How much the display has been scaled. 41 | */ 42 | scaleFactor: number; 43 | 44 | /** 45 | * If the display is the primary display 46 | */ 47 | primary: boolean; 48 | 49 | /** 50 | * Bounds of the display. 51 | */ 52 | bounds: ssf.Rectangle; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/api-specification/interface/window-extension.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Both of these are needed for screen orientation in OpenFin and Browser. 3 | * This is because screen orientation is a non standard feature, even though 4 | * it is implemented in Chrome and FireFox browsers, so there are no typings. 5 | * This gives us the typings, although a very stripped down version 6 | */ 7 | 8 | /** 9 | * @ignore 10 | * @hidden 11 | */ 12 | declare interface Screen { 13 | orientation: Orientation; 14 | } 15 | 16 | /** 17 | * @ignore 18 | * @hidden 19 | */ 20 | interface Orientation { 21 | angle: number; 22 | } 23 | -------------------------------------------------------------------------------- /packages/api-specification/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-specification", 3 | "version": "0.0.7", 4 | "description": "The interface for the ContainerJS API", 5 | "scripts": { 6 | "typedoc2html": "node transform-type-info.js --testfile test-report.json --outfile ../../docs/Docs.html", 7 | "typedoc": "typedoc --mode file --json type-info.json interface", 8 | "docs": "npm run typedoc && npm run typedoc2html" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 13 | }, 14 | "author": "", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 18 | }, 19 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 20 | "dependencies": { 21 | "@types/openfin": "^17.0.2", 22 | "commander": "^2.9.0", 23 | "copyfiles": "^1.2.0", 24 | "electron": "^1.7.5", 25 | "gray-matter": "^2.1.1", 26 | "html2json": "^1.0.2", 27 | "json-transforms": "^1.1.2", 28 | "jspath": "^0.3.4", 29 | "rimraf": "^2.6.1", 30 | "typedoc": "^0.7.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | src/ 4 | index.ts 5 | rollup.config.js 6 | tsconfig.json 7 | 8 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS Compatibility API 2 | 3 | This project provides an mapping from the Symphony API that can be found [here](https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/) to ContainerJS. 4 | For more information, see [The ContainerJS website](https://symphonyoss.github.io/ContainerJS/) 5 | 6 | ## Usage 7 | 8 | Add this package to your project: 9 | 10 | ``` 11 | npm install containerjs-api-compatibility --save 12 | ``` 13 | 14 | The compatibility file must be included directly like so: 15 | ``` 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/index.ts: -------------------------------------------------------------------------------- 1 | import { map } from './src/mapping'; 2 | 3 | const ssf = (window as any).ssf; 4 | const api = (ssf && ssf.registerBoundsChange) 5 | ? ssf 6 | : map.ssf; 7 | 8 | // The current Symphony UI requires that the API is present within the SYM_API namespace, although SymphonyElectron supports both 9 | // Https://github.com/symphonyoss/SymphonyElectron/blob/299e75eca328375468cc3d0bf34ae9ca73e445f6/js/preload/preloadMain.js#L229 10 | (window as any).SYM_API = api; 11 | 12 | /* tslint:disable:no-default-export */ 13 | export default api; 14 | /* tslint:enable:no-default-export */ 15 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-compatibility", 3 | "version": "0.0.6", 4 | "description": "A compatibility layer between symphony electron and ContainerJS", 5 | "main": "build/dist/containerjs-api-compatibility.js", 6 | "scripts": { 7 | "clean": "rimraf build", 8 | "build": "npm run clean && tsc && rollup -c" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 13 | }, 14 | "author": "", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 18 | }, 19 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 20 | "dependencies": { 21 | }, 22 | "devDependencies": { 23 | "copyfiles": "^1.2.0", 24 | "containerjs-api-specification": "^0.0.7", 25 | "rimraf": "^2.6.1", 26 | "rollup": "^0.43.0", 27 | "typescript": "^2.4.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: 'build/es/index.js', 3 | format: 'umd', 4 | moduleName: 'ssf', 5 | dest: 'build/dist/containerjs-api-compatibility.js' 6 | }; 7 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/src/mapping.ts: -------------------------------------------------------------------------------- 1 | const containerjsSsf = ssf; 2 | 3 | export namespace map { 4 | export class ssf { 5 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/Activate+API */ 6 | static activate(windowName: string) { 7 | // Could use window.focus in containerjs, but no way of selecting window by name yet 8 | return; 9 | } 10 | 11 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/Activity+API */ 12 | static registerActivityDetection(throttle: number, callback: Function) { 13 | // Not implemented in containerjs 14 | return; 15 | } 16 | 17 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/BadgeCount+API */ 18 | static setBadgeCount(count: number) { 19 | // See: https://github.com/symphonyoss/ContainerJS/issues/318 20 | (containerjsSsf as any).app.setBadgeCount(count); 21 | } 22 | 23 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/getMediaSources+API */ 24 | static getMediaSources(options: any, callback: Function) { 25 | // Not implemented in containerjs 26 | return; 27 | } 28 | 29 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/RegisterBoundsChange+API */ 30 | static registerBoundsChange(callback: Function) { 31 | // Not fully implemented in containerjs, only listens to the current window, not its children as well 32 | containerjsSsf.Window.getCurrentWindow().addListener('resize', () => { 33 | containerjsSsf.Window.getCurrentWindow().getBounds().then((bounds) => { 34 | callback(bounds); 35 | }); 36 | }); 37 | containerjsSsf.Window.getCurrentWindow().addListener('move', () => { 38 | containerjsSsf.Window.getCurrentWindow().getBounds().then((bounds) => { 39 | callback(bounds); 40 | }); 41 | }); 42 | } 43 | 44 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/Version+API */ 45 | static getVersionInfo() { 46 | // Not implemented in containerjs 47 | return new Promise((resolve, reject) => { 48 | reject(new Error('Not currently implemented')); 49 | }); 50 | } 51 | 52 | static registerLogger() { 53 | // We don't have any need to register a logger for the API layer 54 | } 55 | } 56 | 57 | export namespace ssf { 58 | /** API defined at https://symphonyoss.atlassian.net/wiki/display/WGDWAPI/ScreenSnippet+API */ 59 | export class ScreenSnippet { 60 | capture() { 61 | return new containerjsSsf.Window().capture(); 62 | } 63 | } 64 | 65 | export class Notification { 66 | constructor(title: string, options: NotificationOptions) { 67 | return new containerjsSsf.Notification(title, options); 68 | } 69 | 70 | static permission: string = 'granted'; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/api-symphony-compatibility/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "dom.iterable", 9 | "scripthost" 10 | ], 11 | "outDir": "./build/es", 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "./index.ts", 18 | "node_modules/containerjs-api-specification/interface/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-symphony-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-symphony-demo", 3 | "version": "0.0.6", 4 | "description": "A demo project using the compatibility layer between ContainerJS and Symphony", 5 | "private": true, 6 | "scripts": { 7 | "electron": "ssf-electron -c ./src/app.json --symphony", 8 | "openfin": "ssf-openfin -c ./src/app.json --symphony" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 13 | }, 14 | "author": "", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 18 | }, 19 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 20 | "dependencies": { 21 | "containerjs-api-electron": "^0.0.9", 22 | "containerjs-api-openfin": "^0.0.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/api-symphony-demo/src/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://my.symphony.com", 3 | "name": "SSF Desktop Wrapper", 4 | "uuid": "containerjs", 5 | "autoShow": true 6 | } -------------------------------------------------------------------------------- /packages/api-tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/api-tests/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS Testing 2 | This project contains the Spectron UI tests for the OpenFin and Electron APIs 3 | 4 | ## Testing 5 | ``` 6 | npm i 7 | npm run test:ui 8 | ``` 9 | _Note:_ `test:ui` is needed so these tests are not run on travis (as OpenFin tests do not currently work on Linux) 10 | -------------------------------------------------------------------------------- /packages/api-tests/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Visible
8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/api-tests/demo/load-url-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
LOADED
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/api-tests/demo/test-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symphonyoss/ContainerJS/38578c4bc77417e88ed44da8b3eae71f1e1f2317/packages/api-tests/demo/test-image.png -------------------------------------------------------------------------------- /packages/api-tests/generate-test-report.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const electronTestOutput = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'coverage', 'electron.json'))); 4 | const openfinTestOutput = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'coverage', 'openfin.json'))); 5 | const browserTestOutput = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'coverage', 'browser.json'))); 6 | 7 | const testTable = { 8 | electron: {}, 9 | openfin: {}, 10 | browser: {} 11 | }; 12 | 13 | // Read as 10% or less, 25% or less etc 14 | const percentageColors = { 15 | 10: '#f45858', 16 | 25: '#f2f23c', 17 | 50: '#aeea3f', 18 | 75: '#85ea4f', 19 | 100: '#50ce5b' 20 | }; 21 | 22 | const getColorCode = (percent) => { 23 | let colorCode = ''; 24 | Object.keys(percentageColors).some((key) => { 25 | if (percent <= parseInt(key)) { 26 | colorCode = percentageColors[key]; 27 | return true; 28 | } 29 | return false; 30 | }); 31 | return colorCode; 32 | }; 33 | 34 | const testStatus = { 35 | pass: 'pass', 36 | skip: 'pending', 37 | error: 'error' 38 | }; 39 | 40 | const listOfTests = []; 41 | const tagCount = {}; 42 | // generate the total test count 43 | const generateTotals = (testOutput) => { 44 | testOutput.forEach((test) => { 45 | if (!listOfTests.find((t) => t === test.title)) { 46 | listOfTests.push(test.title); 47 | test.tags.forEach((tag) => { 48 | if (test.status !== testStatus.skip) { 49 | if (tagCount[tag]) { 50 | tagCount[tag]++; 51 | } else { 52 | tagCount[tag] = 1; 53 | } 54 | } else if (!tagCount[tag]) { 55 | tagCount[tag] = 0; 56 | } 57 | }); 58 | } 59 | }); 60 | }; 61 | 62 | const passCount = (table, tag, test) => { 63 | if (!table[tag]) { 64 | table[tag] = 0; 65 | } 66 | 67 | if (test.status === testStatus.pass) { 68 | table[tag]++; 69 | } 70 | }; 71 | 72 | electronTestOutput.forEach((test) => { 73 | test.tags.forEach((tag) => passCount(testTable.electron, tag, test)); 74 | }); 75 | 76 | openfinTestOutput.forEach((test) => { 77 | test.tags.forEach((tag) => passCount(testTable.openfin, tag, test)); 78 | }); 79 | 80 | browserTestOutput.forEach((test) => { 81 | test.tags.forEach((tag) => passCount(testTable.browser, tag, test)); 82 | }); 83 | 84 | generateTotals(electronTestOutput); 85 | generateTotals(openfinTestOutput); 86 | generateTotals(browserTestOutput); 87 | 88 | const sortedTags = Object.keys(tagCount).sort(); 89 | 90 | const outputJson = {}; 91 | 92 | let markdownString = '| Method | Electron | OpenFin | Browser |\n|:---|:---:|:---:|:---:|\n'; 93 | 94 | const createColumn = (color, passed, total) => { 95 | return `${passed}/${total}`; 96 | }; 97 | 98 | sortedTags.forEach((tag) => { 99 | const electronPassed = testTable.electron[tag] || 0; 100 | const openfinPassed = testTable.openfin[tag] || 0; 101 | const browserPassed = testTable.browser[tag] || 0; 102 | const total = tagCount[tag]; 103 | const label = tag.substring(1); // Removes the # from the front of the tag 104 | const electronColor = total > 0 ? getColorCode((electronPassed / total) * 100) : ''; 105 | const openfinColor = total > 0 ? getColorCode((openfinPassed / total) * 100) : ''; 106 | const browserColor = total > 0 ? getColorCode((browserPassed / total) * 100) : ''; 107 | markdownString += `|${label}|${createColumn(electronColor, electronPassed, total)}|${createColumn(openfinColor, openfinPassed, total)}|${createColumn(browserColor, browserPassed, total)}|\n`; 108 | outputJson[label] = { 109 | electron: { 110 | passed: electronPassed, 111 | total 112 | }, 113 | openfin: { 114 | passed: openfinPassed, 115 | total 116 | } 117 | }; 118 | 119 | // The browser doesn't implement all the methods that openfin and electron do. 120 | // Only add the browser results if we actually have run any tests. 121 | if (testTable.browser[tag]) { 122 | outputJson[label].browser = { 123 | passed: browserPassed, 124 | total 125 | }; 126 | } 127 | }); 128 | 129 | const ghPagesMarkdown = 130 | `--- 131 | id: testMatrix 132 | title: Test Report 133 | permalink: docs/test-matrix.html 134 | layout: docs 135 | sectionid: docs 136 | ---\n 137 | {: width="100%"} 138 | `; 139 | 140 | const docsPath = path.join(__dirname, '..', '..', 'docs'); 141 | if (!fs.existsSync(docsPath)) { 142 | fs.mkdirSync(docsPath); 143 | } 144 | fs.writeFileSync(path.join(docsPath, 'test-matrix.md'), ghPagesMarkdown + markdownString); 145 | 146 | fs.writeFileSync(path.join(__dirname, '..', 'api-specification', 'test-report.json'), JSON.stringify(outputJson)); 147 | -------------------------------------------------------------------------------- /packages/api-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-tests", 3 | "version": "0.0.8", 4 | "license": "Apache-2.0", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "test:ui": "npm run test:electron && npm run test:browser && npm run test:openfin", 9 | "test:electron": "rimraf src && copyfiles -f \"demo/**\" src && cross-env MOCHA_CONTAINER=electron mocha test/*.js", 10 | "test:ci:electron": "npm run test:electron -- --reporter reporter.js", 11 | "test:openfin": "npm run clean:cache && rimraf src && copyfiles -f \"demo/**\" src && cross-env MOCHA_CONTAINER=openfin mocha test/*.js", 12 | "test:ci:openfin": "npm run test:openfin -- --reporter reporter.js", 13 | "test:browser": "rimraf src && copyfiles -f \"demo/**\" src && copyfiles -f \"node_modules/containerjs-api-browser/build/dist/**\" src && cross-env MOCHA_CONTAINER=browser mocha test/*.js", 14 | "test:ci:browser": "npm run test:browser -- --reporter reporter.js", 15 | "clean:cache": "rimraf %LOCALAPPDATA%/OpenFin/cache", 16 | "report": "node generate-test-report.js && copyfiles -f \"test-report.json\" \"../api-specification\"" 17 | }, 18 | "dependencies": { 19 | "assert": "^1.4.1", 20 | "containerjs-api-browser": "^0.0.8", 21 | "containerjs-api-electron": "^0.0.9", 22 | "containerjs-api-openfin": "^0.0.9", 23 | "electron": "1.6.6", 24 | "live-server": "^1.2.0", 25 | "mocha": "^3.2.0", 26 | "openfin-cli": "^1.1.5", 27 | "spectron": "^3.6.2", 28 | "webdriverio": "^4.8.0" 29 | }, 30 | "devDependencies": { 31 | "copyfiles": "^1.2.0", 32 | "cross-env": "^4.0.0", 33 | "rimraf": "^2.6.1", 34 | "selenium-standalone": "^6.4.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/api-tests/reporter.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | module.exports = JsonReporter; 6 | const testContainer = process.env.MOCHA_CONTAINER; 7 | 8 | let jsonOutput = []; 9 | // Regex: (# followed by at least 1 word character, dot or bracket followed by 1 space) one or more times 10 | // Example: #ssf.Window.hide #electron 11 | const testTagPattern = /(#[\w.()]+[\s]?)+/; 12 | 13 | function JsonReporter(runner) { 14 | mocha.reporters.Base.call(this, runner); 15 | var passes = 0; 16 | var pending = 0; 17 | var failures = 0; 18 | 19 | runner.on('suite', function(suite) { 20 | console.log(suite.title); 21 | }); 22 | 23 | runner.on('pass', function(test) { 24 | passes++; 25 | const tags = test.title.match(testTagPattern); 26 | jsonOutput.push({ 27 | title: test.fullTitle(), 28 | status: 'pass', 29 | container: testContainer, 30 | tags: tags === null ? [] : tags[0].split(' ') 31 | }); 32 | console.log('%s %s', mocha.reporters.Base.symbols.ok, test.title); 33 | }); 34 | 35 | // Runs when a test has been skipped 36 | runner.on('pending', function(test) { 37 | pending++; 38 | const tags = test.title.match(testTagPattern); 39 | jsonOutput.push({ 40 | title: test.fullTitle(), 41 | status: 'pending', 42 | container: testContainer, 43 | tags: tags === null ? [] : tags[0].split(' ') 44 | }); 45 | console.log('%s %s', '-', test.title); 46 | }); 47 | 48 | runner.on('fail', function(test, err) { 49 | failures++; 50 | const tags = test.title.match(testTagPattern); 51 | jsonOutput.push({ 52 | title: test.fullTitle(), 53 | status: 'fail', 54 | container: testContainer, 55 | tags: tags === null ? [] : tags[0].split(' ') 56 | }); 57 | console.log('%s %s -- error: %s', mocha.reporters.Base.symbols.err, test.title, err.message); 58 | }); 59 | 60 | runner.on('end', function() { 61 | console.log('pass:%d pending:%d failure:%d', passes, pending, failures); 62 | if (!fs.existsSync(path.join(process.cwd(), 'coverage'))) { 63 | fs.mkdirSync(path.join(process.cwd(), 'coverage')); 64 | } 65 | fs.writeFileSync(path.join(process.cwd(), 'coverage', testContainer + '.json'), JSON.stringify(jsonOutput)); 66 | process.exit(failures > 0 ? 1 : 0); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /packages/api-tests/test/browser-test-setup.js: -------------------------------------------------------------------------------- 1 | const webdriverio = require('webdriverio'); 2 | const selenium = require('selenium-standalone'); 3 | 4 | const options = { 5 | drivers: { 6 | chrome: { 7 | version: '2.27', 8 | arch: process.arch, 9 | baseURL: 'https://chromedriver.storage.googleapis.com' 10 | } 11 | }, 12 | desiredCapabilities: { 13 | browserName: 'chrome', 14 | chromeOptions: { 15 | // Disables "Chrome is being controlled by testing software" banner. 16 | args: ['disable-infobars'] 17 | } 18 | } 19 | }; 20 | 21 | // This is designed to replicate the Spectron Application object, apart from launching chrome instead of Electron. 22 | class Application { 23 | constructor(timeout) { 24 | this.client = undefined; 25 | this.running = false; 26 | this.driver = undefined; 27 | this.installed = false; 28 | this.timeout = timeout; 29 | } 30 | 31 | start() { 32 | return new Promise((resolve, reject) => { 33 | if (!this.installed) { 34 | selenium.install((err) => { 35 | if (err) { 36 | this.running = false; 37 | reject(err); 38 | } else { 39 | this.installed = true; 40 | this.startSelenium(resolve, reject); 41 | } 42 | }); 43 | } else { 44 | this.startSelenium(resolve, reject); 45 | } 46 | }); 47 | } 48 | 49 | startSelenium(resolve, reject) { 50 | selenium.start((err, child) => { 51 | if (err) { 52 | this.running = false; 53 | reject(err); 54 | } else { 55 | this.running = true; 56 | this.driver = child; 57 | // Start chrome at index.html 58 | this.client = webdriverio.remote(options).init().url('http://localhost:5000/index.html'); 59 | this.client.timeouts('script', this.timeout); 60 | this.client.timeouts('implicit', this.timeout); 61 | // old type 'page load' needed for some browsers 62 | this.client.timeouts('page load', this.timeout); 63 | this.client.timeouts('pageLoad', this.timeout); 64 | // Implements the spectron helper method 65 | this.client.getWindowCount = () => { 66 | return this.client.windowHandles().then(handles => handles.value.length); 67 | }; 68 | 69 | resolve(); 70 | } 71 | }); 72 | } 73 | 74 | stop() { 75 | this.running = false; 76 | this.client.end(); 77 | } 78 | 79 | restart() { 80 | this.stop(); 81 | this.start(); 82 | } 83 | 84 | isRunning() { 85 | return this.running; 86 | } 87 | }; 88 | 89 | module.exports = (timeout) => { 90 | return new Application(timeout); 91 | }; 92 | -------------------------------------------------------------------------------- /packages/api-tests/test/electron-test-setup.js: -------------------------------------------------------------------------------- 1 | const Application = require('spectron').Application; 2 | const path = require('path'); 3 | const electronPath = path.join(__dirname, '..', 'node_modules', '.bin', 'ssf-electron'); 4 | 5 | module.exports = (timeout) => { 6 | const extension = process.platform === 'win32' ? '.cmd' : ''; 7 | return new Application({ 8 | path: electronPath + extension, 9 | args: ['-u', 'http://localhost:5000/index.html'] 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/api-tests/test/openfin-test-setup.js: -------------------------------------------------------------------------------- 1 | const Application = require('spectron').Application; 2 | const path = require('path'); 3 | const spawn = require('child_process').spawn; 4 | 5 | module.exports = (timeout) => { 6 | const openfinPath = path.join(__dirname, '..', 'node_modules', '.bin', 'ssf-openfin'); 7 | const commandLine = `${openfinPath} -u http://localhost:5000/index.html -o ./src/openfinapp.json -f 6.49.20.22 -C http://localhost:5000/openfinapp.json`; 8 | 9 | if (process.platform === 'win32') { 10 | spawn('cmd.exe', ['/c', commandLine]); 11 | } else { 12 | spawn('/bin/bash', ['-c', commandLine]); 13 | } 14 | 15 | return new Application({ 16 | connectionRetryCount: 1, 17 | connectionRetryTimeout: timeout, 18 | startTimeout: timeout, 19 | waitTimeout: timeout, 20 | debuggerAddress: '127.0.0.1:9090' 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/api-tests/test/setup.js: -------------------------------------------------------------------------------- 1 | const liveServer = require('live-server'); 2 | 3 | const params = { 4 | port: 5000, 5 | host: '127.0.0.1', 6 | root: 'src', 7 | open: false, 8 | ignore: '*', 9 | logLevel: 0 10 | }; 11 | 12 | before(() => { 13 | liveServer.start(params); 14 | }); 15 | 16 | after(() => { 17 | liveServer.shutdown(); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/api-tests/test/test-helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, no-new */ 2 | const executeAsyncJavascript = (client, script, ...args) => { 3 | // script is passed a callback as its final argument 4 | return client.executeAsync(script, ...args); 5 | }; 6 | 7 | // In OpenFin, an extra hidden window gets created that interferes 8 | // with tests that counts windows or selects windows by index. This 9 | // initialisation gets the added window handles, so they can be 10 | // excluded later 11 | let addedHandles = []; 12 | const initialiseWindows = (client) => { 13 | // Script to wait for app.ready() 14 | const script = (callback) => { 15 | if (window.ssf) { 16 | ssf.app.ready().then(() => { 17 | callback(); 18 | }); 19 | } else { 20 | callback(); 21 | } 22 | }; 23 | 24 | // Get a list of any extra window handles that get create during initialisation 25 | return executeAsyncJavascript(client, script) 26 | .then(() => client.windowHandles()) 27 | .then(handles => { 28 | // The first one is the real main window, and we want 29 | // to exclude the rest 30 | addedHandles = handles.value.splice(1); 31 | }); 32 | }; 33 | 34 | // Get a list of window handles excluding windows added during initialisation 35 | const getWindowHandles = (client) => { 36 | return client.windowHandles() 37 | .then(result => { 38 | return result.value.filter(h => addedHandles.indexOf(h) === -1); 39 | }); 40 | }; 41 | 42 | const selectWindow = (client, index) => { 43 | return getWindowHandles(client) 44 | .then(handles => client.window(handles[index])); 45 | }; 46 | 47 | const openNewWindow = (client, options) => { 48 | const script = (options, callback) => { 49 | ssf.app.ready().then(() => { 50 | new ssf.Window(options, (win) => { 51 | window.newWin = win; 52 | callback(win.getId()); 53 | }); 54 | }); 55 | }; 56 | return executeAsyncJavascript(client, script, options); 57 | }; 58 | /* eslint-enable no-undef, no-new */ 59 | 60 | const countWindows = (client) => { 61 | return getWindowHandles(client) 62 | .then(winHandles => winHandles.length); 63 | }; 64 | 65 | const chainPromises = (promises) => promises.reduce((acc, cur) => acc.then(cur), Promise.resolve()); 66 | 67 | module.exports = { 68 | executeAsyncJavascript, 69 | initialiseWindows, 70 | selectWindow, 71 | openNewWindow, 72 | countWindows, 73 | chainPromises 74 | }; 75 | -------------------------------------------------------------------------------- /packages/api-utility/.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | build/test/ 5 | src/ 6 | test/ 7 | index.ts 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /packages/api-utility/README.md: -------------------------------------------------------------------------------- 1 | # ContainerJS utilities 2 | 3 | This project provides some utility classes used by ContainerJS 4 | For more information, see [The ContainerJS website](https://symphonyoss.github.io/ContainerJS/) 5 | -------------------------------------------------------------------------------- /packages/api-utility/index.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from './src/uri'; 2 | import { Emitter } from './src/emitter'; 3 | import { Display } from './src/display'; 4 | 5 | export { 6 | Uri, 7 | Emitter, 8 | Display 9 | }; 10 | -------------------------------------------------------------------------------- /packages/api-utility/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containerjs-api-utility", 3 | "version": "0.0.5", 4 | "description": "Utility classes and functions for ContainerJS", 5 | "main": "build/es/index.js", 6 | "typings": "build/es/index", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/symphonyoss/ContainerJS.git" 10 | }, 11 | "author": "", 12 | "license": "Apache-2.0", 13 | "bugs": { 14 | "url": "https://github.com/symphonyoss/ContainerJS/issues" 15 | }, 16 | "scripts": { 17 | "clean": "rimraf build", 18 | "build": "npm run clean && tsc", 19 | "build:test": "rimraf build/test && tsc -p test/tsconfig.json -m CommonJS --outdir ./build/test", 20 | "test": "npm run build:test && tape build/test/test/**/*.test.js | tap-diff" 21 | }, 22 | "homepage": "https://symphonyoss.github.io/ContainerJS/", 23 | "devDependencies": { 24 | "@types/node": "^8.0.32", 25 | "@types/proxyquire": "^1.3.27", 26 | "@types/sinon": "^2.3.2", 27 | "@types/tape": "^4.2.30", 28 | "containerjs-api-specification": "^0.0.7", 29 | "proxyquire": "^1.8.0", 30 | "rimraf": "^2.6.1", 31 | "sinon": "^2.3.8", 32 | "tap-diff": "^0.1.1", 33 | "tape": "^4.8.0", 34 | "typescript": "^2.4.1" 35 | }, 36 | "dependencies": { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/api-utility/src/display.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Only necessary in Electron and OpenFin as browser does not know about 3 | * more than 1 screen. 4 | */ 5 | export class Display { 6 | static getDisplayAlteredPosition(displayId: string, position: ssf.Position) { 7 | if (!displayId) { 8 | return Promise.resolve({x: undefined, y: undefined}); 9 | } 10 | 11 | return ssf.Screen.getDisplays().then((displays) => { 12 | const display = displays.filter(d => d.id === displayId)[0]; 13 | return { 14 | x: display.bounds.x + position.x, 15 | y: display.bounds.y + position.y 16 | }; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/api-utility/src/emitter.ts: -------------------------------------------------------------------------------- 1 | export abstract class Emitter implements ssf.EventEmitter { 2 | eventListeners: Map void)[]> = new Map(); 3 | 4 | constructor() { 5 | this.eventListeners = new Map(); 6 | } 7 | 8 | innerAddEventListener(event: string, listener: (...args: any[]) => void) { 9 | // No-op default implementation to be overridden if required 10 | } 11 | innerRemoveEventListener(event: string, listener: (...args: any[]) => void) { 12 | // No-op default implementation to be overridden if required 13 | } 14 | 15 | emit(event: string, data?: any) { 16 | if (this.eventListeners.has(event)) { 17 | this.eventListeners.get(event).forEach(listener => listener(data)); 18 | } 19 | } 20 | 21 | addListener(event: string, listener: (...args: any[]) => void) { 22 | if (this.eventListeners.has(event)) { 23 | const temp = this.eventListeners.get(event); 24 | temp.push(listener); 25 | this.eventListeners.set(event, temp); 26 | } else { 27 | this.eventListeners.set(event, [listener]); 28 | } 29 | this.innerAddEventListener(event, listener); 30 | return this; 31 | } 32 | 33 | on(event: string, listener: (...args: any[]) => void) { 34 | return this.addListener(event, listener); 35 | } 36 | 37 | eventNames() { 38 | return Array.from(this.eventListeners.keys()); 39 | } 40 | 41 | listenerCount(event: string) { 42 | return this.eventListeners.has(event) ? this.eventListeners.get(event).length : 0; 43 | } 44 | 45 | listeners(event: string) { 46 | return this.eventListeners.get(event); 47 | } 48 | 49 | once(event: string, listener: (...args: any[]) => void) { 50 | // Remove the listener once it is called 51 | const unsubscribeListener = (evt) => { 52 | this.removeListener(event, unsubscribeListener); 53 | listener(evt); 54 | }; 55 | 56 | this.on(event, unsubscribeListener); 57 | return this; 58 | } 59 | 60 | removeListener(event: string, listener: (...args: any[]) => void) { 61 | if (this.eventListeners.has(event)) { 62 | const listeners = this.eventListeners.get(event); 63 | const index = listeners.indexOf(listener); 64 | if (index >= 0) { 65 | listeners.splice(index, 1); 66 | listeners.length > 0 67 | ? this.eventListeners.set(event, listeners) 68 | : this.eventListeners.delete(event); 69 | } 70 | } 71 | 72 | this.innerRemoveEventListener(event, listener); 73 | return this; 74 | } 75 | 76 | removeAllListeners(eventName?: string) { 77 | const removeAllListenersForEvent = (event) => { 78 | if (this.eventListeners.has(event)) { 79 | this.eventListeners.get(event).forEach((listener) => { 80 | this.innerRemoveEventListener(event, listener); 81 | }); 82 | this.eventListeners.delete(event); 83 | } 84 | }; 85 | 86 | if (eventName) { 87 | removeAllListenersForEvent(eventName); 88 | } else { 89 | this.eventListeners.forEach((value, key) => removeAllListenersForEvent(key)); 90 | } 91 | 92 | return this; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/api-utility/src/uri.ts: -------------------------------------------------------------------------------- 1 | const isUrlPattern = /^https?:\/\//i; 2 | 3 | export class Uri { 4 | static getAbsoluteUrl(url: string): string { 5 | if (url && !isUrlPattern.test(url)) { 6 | const path = url.startsWith('/') 7 | ? location.origin 8 | : location.href.substring(0, location.href.lastIndexOf('/') + 1); 9 | 10 | return `${path}${url}`; 11 | } 12 | return url; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api-utility/test/display.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import * as sinon from 'sinon'; 3 | import { globals } from './globals'; 4 | import { Display } from '../src/display'; 5 | 6 | const getDisplaysStub = sinon.stub(); 7 | const ssf = { 8 | Screen: { 9 | getDisplays: getDisplaysStub 10 | } 11 | }; 12 | 13 | test('Display getDisplayAlteredPosition without displayId returns undefined x/y', t => { 14 | globals({ ssf }); 15 | 16 | Display.getDisplayAlteredPosition(undefined, { x: 10, y: 50 }).then(result => { 17 | t.deepEqual(result, { x: undefined, y: undefined }); 18 | 19 | t.end(); 20 | }); 21 | }); 22 | 23 | test('Display getDisplayAlteredPosition with known displayId returns x/y within display', t => { 24 | globals({ ssf }); 25 | 26 | const displays = [ 27 | { id: 'screen-100', bounds: { x: 0, y: 0 } }, 28 | { id: 'screen-101', bounds: { x: 100, y: 100 } } 29 | ]; 30 | getDisplaysStub.returns(Promise.resolve(displays)); 31 | 32 | Display.getDisplayAlteredPosition('screen-101', { x: 10, y: 50 }).then(result => { 33 | t.deepEqual(result, { x: 110, y: 150 }); 34 | 35 | t.end(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/api-utility/test/emitter.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import * as sinon from 'sinon'; 3 | import { Emitter } from '../src/emitter'; 4 | 5 | // Derived target class using stubbed inner add/remove 6 | class EmitterTarget extends Emitter { 7 | addListenerStub: sinon.SinonStub = sinon.stub(); 8 | removeListenerStub: sinon.SinonStub = sinon.stub(); 9 | 10 | innerAddEventListener(event: string, listener: (...args: any[]) => void) { 11 | this.addListenerStub(event, listener); 12 | } 13 | innerRemoveEventListener(event: string, listener: (...args: any[]) => void) { 14 | this.removeListenerStub(event, listener); 15 | } 16 | } 17 | 18 | test('Emitter emit without listeners does nothing', t => { 19 | const target = new EmitterTarget(); 20 | const listener = sinon.stub(); 21 | target.addListener('otherevent', listener); 22 | 23 | target.emit('testevent', { test: 'test-data' }); 24 | 25 | t.assert(listener.notCalled, 'should not have called listener'); 26 | t.end(); 27 | }); 28 | 29 | test('Emitter addListener with multiple listeners emit calls each matching listener', t => { 30 | const target = new EmitterTarget(); 31 | const listener1 = sinon.stub(); 32 | const listener2 = sinon.stub(); 33 | const listener3 = sinon.stub(); 34 | target.addListener('testevent', listener1); 35 | target.addListener('otherevent', listener2); 36 | target.addListener('testevent', listener3); 37 | 38 | target.emit('testevent', { test: 'test-data' }); 39 | 40 | t.assert(listener1.calledWith({ test: 'test-data' }), 'should have called first listener'); 41 | t.assert(listener2.notCalled, 'should not have called second listener'); 42 | t.assert(listener3.calledWith({ test: 'test-data' }), 'should have called third listener'); 43 | t.end(); 44 | }); 45 | 46 | test('Emitter addListener calls innerAddEventListener', t => { 47 | const target = new EmitterTarget(); 48 | const listener = sinon.stub(); 49 | 50 | target.addListener('testevent', listener); 51 | 52 | t.assert(target.addListenerStub.calledWith('testevent', listener), 53 | 'should have called innerAddEventListener'); 54 | t.assert(target.removeListenerStub.notCalled, 55 | 'should not have called innerRemoveEventListener'); 56 | t.end(); 57 | }); 58 | 59 | test('Emitter on calls innerAddEventListener', t => { 60 | const target = new EmitterTarget(); 61 | const listener = sinon.stub(); 62 | 63 | target.on('testevent', listener); 64 | 65 | t.assert(target.addListenerStub.calledWith('testevent', listener), 66 | 'should have called innerAddEventListener'); 67 | t.assert(target.removeListenerStub.notCalled, 68 | 'should not have called innerRemoveEventListener'); 69 | t.end(); 70 | }); 71 | 72 | test('Emitter eventNames returns listened to events', t => { 73 | const target = new EmitterTarget(); 74 | const stub = sinon.stub(); 75 | 76 | target.addListener('testevent1', sinon.stub()); 77 | target.addListener('testevent2', stub); 78 | target.addListener('testevent3', sinon.stub()); 79 | target.removeListener('testevent2', stub); 80 | 81 | const result = target.eventNames(); 82 | 83 | t.deepEqual(result, ['testevent1', 'testevent3']); 84 | t.end(); 85 | }); 86 | 87 | test('Emitter listenerCount returns active listener count', t => { 88 | const target = new EmitterTarget(); 89 | const stub = sinon.stub(); 90 | 91 | target.addListener('testevent', sinon.stub()); 92 | target.addListener('testevent', stub); 93 | target.addListener('testevent', sinon.stub()); 94 | target.removeListener('testevent', stub); 95 | 96 | const result = target.listenerCount('testevent'); 97 | 98 | t.deepEqual(result, 2); 99 | t.end(); 100 | }); 101 | 102 | test('Emitter listenerCount returns active listener count', t => { 103 | const target = new EmitterTarget(); 104 | const stub1 = sinon.stub(); 105 | const stub2 = sinon.stub(); 106 | const stub3 = sinon.stub(); 107 | 108 | target.addListener('testevent', stub1); 109 | target.addListener('testevent', stub2); 110 | target.addListener('testevent', stub3); 111 | target.removeListener('testevent', stub2); 112 | 113 | const result = target.listeners('testevent'); 114 | 115 | t.deepEqual(result, [stub1, stub3]); 116 | t.end(); 117 | }); 118 | 119 | test('Emitter once calls listener once only', t => { 120 | const target = new EmitterTarget(); 121 | const stub = sinon.stub(); 122 | 123 | target.once('testevent', stub); 124 | t.equal(target.listenerCount('testevent'), 1, 'should have 1 listener'); 125 | 126 | target.emit('testevent'); 127 | 128 | t.assert(stub.calledOnce, 'should have called listener once'); 129 | t.equal(target.listenerCount('testevent'), 0, 'should have no listeners'); 130 | 131 | target.emit('testevent'); 132 | t.assert(stub.calledOnce, 'should not have called listener twice'); 133 | 134 | t.end(); 135 | }); 136 | 137 | test('Emitter removeListener should not call listener again', t => { 138 | const target = new EmitterTarget(); 139 | const stub1 = sinon.stub(); 140 | const stub2 = sinon.stub(); 141 | 142 | target.addListener('testevent', stub1); 143 | target.addListener('testevent', stub2); 144 | target.removeListener('testevent', stub2); 145 | 146 | target.emit('testevent'); 147 | 148 | t.assert(stub1.calledOnce, 'should have called first listener'); 149 | t.assert(stub2.notCalled, 'should not have called second listener'); 150 | t.end(); 151 | }); 152 | 153 | test('Emitter removeListener calls innerRemoveEventListener', t => { 154 | const target = new EmitterTarget(); 155 | const listener = sinon.stub(); 156 | 157 | target.addListener('testevent', listener); 158 | target.addListenerStub.reset(); 159 | 160 | target.removeListener('testevent', listener); 161 | 162 | t.assert(target.addListenerStub.notCalled, 163 | 'should not have called innerAddEventListener'); 164 | t.assert(target.removeListenerStub.calledWith('testevent', listener), 165 | 'should have called innerRemoveEventListener'); 166 | t.end(); 167 | }); 168 | 169 | test('Emitter removeAllListeners with eventName removes all listeners for event', t => { 170 | const target = new EmitterTarget(); 171 | const stub1 = sinon.stub(); 172 | const stub2 = sinon.stub(); 173 | const stub3 = sinon.stub(); 174 | 175 | target.addListener('testevent1', stub1); 176 | target.addListener('testevent2', stub2); 177 | target.addListener('testevent2', stub3); 178 | 179 | target.removeAllListeners('testevent2'); 180 | 181 | t.deepEqual(target.eventNames(), ['testevent1']); 182 | 183 | target.emit('testevent1'); 184 | target.emit('testevent2'); 185 | 186 | t.assert(stub1.called, 'should have called first listener'); 187 | t.assert(stub2.notCalled, 'should not have called second listener'); 188 | t.assert(stub3.notCalled, 'should not have called third listener'); 189 | 190 | t.end(); 191 | }); 192 | 193 | test('Emitter removeAllListeners without eventName removes all listeners for all events', t => { 194 | const target = new EmitterTarget(); 195 | const stub1 = sinon.stub(); 196 | const stub2 = sinon.stub(); 197 | const stub3 = sinon.stub(); 198 | 199 | target.addListener('testevent1', stub1); 200 | target.addListener('testevent2', stub2); 201 | target.addListener('testevent2', stub3); 202 | 203 | target.removeAllListeners(); 204 | 205 | t.deepEqual(target.eventNames(), []); 206 | 207 | target.emit('testevent1'); 208 | target.emit('testevent2'); 209 | 210 | t.assert(stub1.notCalled, 'should not have called first listener'); 211 | t.assert(stub2.notCalled, 'should not have called second listener'); 212 | t.assert(stub3.notCalled, 'should not have called third listener'); 213 | 214 | t.end(); 215 | }); 216 | -------------------------------------------------------------------------------- /packages/api-utility/test/globals.ts: -------------------------------------------------------------------------------- 1 | export const stubGlobal = (name: string, object: any) => { 2 | const g: any = global; 3 | g[name] = object; 4 | }; 5 | 6 | export const globals = (api: any) => { 7 | for (const name in api) { 8 | if (api.hasOwnProperty(name)) { 9 | stubGlobal(name, api[name]); 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/api-utility/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "dom.iterable" 9 | ], 10 | "target": "es5", 11 | "declaration": true 12 | }, 13 | "include": [ 14 | "**/*.ts", 15 | "../node_modules/containerjs-api-specification/interface/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/api-utility/test/uri.test.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { globals } from './globals'; 3 | import { Uri } from '../src/uri'; 4 | 5 | const location = { 6 | origin: 'http://test-site', 7 | href: 'http://test-site/path/index.html' 8 | }; 9 | 10 | test('Uri getAbsoluteUrl with absolute path expect same path', (t) => { 11 | globals({ location }); 12 | 13 | const result = Uri.getAbsoluteUrl('http://other-site/other-path/image.jpg'); 14 | 15 | t.equal(result, 'http://other-site/other-path/image.jpg'); 16 | t.end(); 17 | }); 18 | 19 | test('Uri getAbsoluteUrl with relative to root path expect absolute path', (t) => { 20 | globals({ location }); 21 | 22 | const result = Uri.getAbsoluteUrl('/other-path/image.jpg'); 23 | 24 | t.equal(result, 'http://test-site/other-path/image.jpg'); 25 | t.end(); 26 | }); 27 | 28 | test('Uri getAbsoluteUrl with relative to current path expect absolute path', (t) => { 29 | globals({ location }); 30 | 31 | const result = Uri.getAbsoluteUrl('image.jpg'); 32 | 33 | t.equal(result, 'http://test-site/path/image.jpg'); 34 | t.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/api-utility/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2015", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "dom.iterable", 9 | "scripthost" 10 | ], 11 | "outDir": "./build/es", 12 | "target": "es5", 13 | "declaration": true 14 | }, 15 | "include": [ 16 | "./index.ts", 17 | "node_modules/containerjs-api-specification/interface/*.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /test-script.ps1: -------------------------------------------------------------------------------- 1 | [Environment]::SetEnvironmentVariable("TestResult", "0", "User") 2 | 3 | # Don't run tests on PRs 4 | IF ($env:APPVEYOR_REPO_BRANCH -eq "master" -And (-Not (Test-Path Env:\APPVEYOR_PULL_REQUEST_NUMBER))) { 5 | cd .\packages\api-tests 6 | 7 | # Run tests individually so we don't fail the build 8 | npm run test:ci:electron 9 | IF ($LASTEXITCODE -ne "0") { 10 | Write-Warning -Message 'Electron Tests Failed' 11 | [Environment]::SetEnvironmentVariable("TestResult", "1", "User") 12 | } 13 | npm run test:ci:openfin 14 | IF ($LASTEXITCODE -ne "0") { 15 | Write-Warning -Message 'OpenFin Tests Failed' 16 | [Environment]::SetEnvironmentVariable("TestResult", "1", "User") 17 | } 18 | npm run test:ci:browser 19 | IF ($LASTEXITCODE -ne "0") { 20 | Write-Warning -Message 'Browser Tests Failed' 21 | [Environment]::SetEnvironmentVariable("TestResult", "1", "User") 22 | } 23 | 24 | npm run report 25 | 26 | cd ..\.. 27 | npm run docs 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-return-shorthand": true, 4 | "comment-format": [true, 5 | "check-space", 6 | "check-uppercase" 7 | ], 8 | "curly":[true], 9 | "eofline": true, 10 | "indent": [true, 11 | "spaces" 12 | ], 13 | "jsdoc-format": true, 14 | "no-default-export": true, 15 | "no-internal-module": true, 16 | "no-null-keyword": false, 17 | "no-switch-case-fall-through": true, 18 | "no-trailing-whitespace": [true, "ignore-template-strings"], 19 | "no-var-keyword": true, 20 | "object-literal-shorthand": true, 21 | "one-line": [true, 22 | "check-open-brace", 23 | "check-whitespace" 24 | ], 25 | "only-arrow-functions": [ 26 | true 27 | ], 28 | "prefer-const": true, 29 | "quotemark": [true, 30 | "single", 31 | "avoid-escape" 32 | ], 33 | "semicolon": [true, "always", "ignore-bound-class-methods"], 34 | "triple-equals": [true, "allow-null-check"], 35 | "typedef": [ 36 | true, 37 | "parameter", 38 | "member-variable-declaration", 39 | "property-declaration" 40 | ], 41 | "typedef-whitespace": [ 42 | true, 43 | { 44 | "call-signature": "nospace", 45 | "index-signature": "nospace", 46 | "parameter": "nospace", 47 | "property-declaration": "nospace", 48 | "variable-declaration": "nospace" 49 | }, 50 | { 51 | "call-signature": "onespace", 52 | "index-signature": "onespace", 53 | "parameter": "onespace", 54 | "property-declaration": "onespace", 55 | "variable-declaration": "onespace" 56 | } 57 | ], 58 | "whitespace": [true, 59 | "check-branch", 60 | "check-decl", 61 | "check-operator", 62 | "check-module", 63 | "check-separator", 64 | "check-type" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /validate-licenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # A simple script to check licenses of transitive npm packages 4 | 5 | # Install lerna to run the project build and node-license-validator to check licenses of transitive deps 6 | npm install -g lerna node-license-validator 7 | 8 | # Run npm install only for production (bundled) dependencies 9 | npm install --production 10 | 11 | # Check licenses for each lerna package 12 | for d in packages/*; do 13 | echo "Validating licenses on $d..." 14 | node-license-validator --dir $d --allow-licenses WTFPL Apache Apache-2 "Apache License, Version 2.0" BSD-like BSD BSD-2-Clause BSD-3-Clause Apache-2.0 MIT ISC Unlicense MIT/X11 "MIT / http://rem.mit-license.org" "Public Domain" --allow-packages buffers@0.1.1 extsprintf@1.0.2 map-stream@0.1.0 verror@1.3.6 15 | done 16 | --------------------------------------------------------------------------------