Repository: electron/electron-api-demos
Branch: master
Commit: 26b3d1d57adc
Files: 115
Total size: 144.0 KB
Directory structure:
gitextract_8wwg7s1f/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── sentinel.yml
├── .gitignore
├── .node-version
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── README.md
├── assets/
│ ├── app-icon/
│ │ └── mac/
│ │ └── app.icns
│ ├── code-blocks.js
│ ├── css/
│ │ ├── about.css
│ │ ├── demo.css
│ │ ├── fonts/
│ │ │ ├── SourceSansPro-Black.otf
│ │ │ ├── SourceSansPro-BlackIt.otf
│ │ │ ├── SourceSansPro-Bold.otf
│ │ │ ├── SourceSansPro-BoldIt.otf
│ │ │ ├── SourceSansPro-ExtraLight.otf
│ │ │ ├── SourceSansPro-ExtraLightIt.otf
│ │ │ ├── SourceSansPro-It.otf
│ │ │ ├── SourceSansPro-Light.otf
│ │ │ ├── SourceSansPro-LightIt.otf
│ │ │ ├── SourceSansPro-Regular.otf
│ │ │ ├── SourceSansPro-Semibold.otf
│ │ │ └── SourceSansPro-SemiboldIt.otf
│ │ ├── github.css
│ │ ├── global.css
│ │ ├── nativize.css
│ │ ├── nav.css
│ │ ├── print.css
│ │ ├── section.css
│ │ └── variables.css
│ ├── demo-btns.js
│ ├── ex-links.js
│ ├── imports.js
│ ├── mac/
│ │ ├── child.plist
│ │ ├── info.plist
│ │ └── parent.plist
│ ├── nav.js
│ └── normalize-shortcuts.js
├── cli.js
├── docs.md
├── index.html
├── license.md
├── main-process/
│ ├── communication/
│ │ ├── async-msg.js
│ │ └── sync-msg.js
│ ├── menus/
│ │ ├── application-menu.js
│ │ ├── context-menu.js
│ │ └── shortcuts.js
│ ├── native-ui/
│ │ ├── dialogs/
│ │ │ ├── error.js
│ │ │ ├── information.js
│ │ │ ├── open-file.js
│ │ │ └── save.js
│ │ ├── drag/
│ │ │ └── drag.js
│ │ └── tray/
│ │ └── tray.js
│ └── system/
│ ├── app-information.js
│ └── protocol-handler.js
├── main.js
├── package.json
├── renderer-process/
│ ├── communication/
│ │ ├── async-msg.js
│ │ ├── invisible-msg.js
│ │ └── sync-msg.js
│ ├── media/
│ │ └── desktop-capturer.js
│ ├── menus/
│ │ └── context-menu.js
│ ├── native-ui/
│ │ ├── dialogs/
│ │ │ ├── error.js
│ │ │ ├── information.js
│ │ │ ├── open-file.js
│ │ │ └── save.js
│ │ ├── drag/
│ │ │ └── drag.js
│ │ ├── ex-links-file-manager/
│ │ │ ├── ex-links.js
│ │ │ └── file-manager.js
│ │ ├── notifications/
│ │ │ ├── advanced-notification.js
│ │ │ └── basic-notification.js
│ │ └── tray/
│ │ └── tray.js
│ ├── system/
│ │ ├── app-information.js
│ │ ├── copy.js
│ │ ├── paste.js
│ │ ├── protocol-handler.js
│ │ ├── screen-information.js
│ │ ├── sys-information.js
│ │ └── version-information.js
│ └── windows/
│ ├── create-window.js
│ ├── frameless-window.js
│ ├── manage-window.js
│ ├── process-crash.js
│ ├── process-hang.js
│ └── using-window-events.js
├── script/
│ ├── installer.js
│ ├── mas.sh
│ ├── release.js
│ └── windows-store.js
├── sections/
│ ├── about.html
│ ├── communication/
│ │ ├── invisible.html
│ │ └── ipc.html
│ ├── media/
│ │ └── desktop-capturer.html
│ ├── menus/
│ │ ├── menus.html
│ │ └── shortcuts.html
│ ├── native-ui/
│ │ ├── dialogs.html
│ │ ├── drag.html
│ │ ├── ex-links-file-manager.html
│ │ ├── notifications.html
│ │ └── tray.html
│ ├── system/
│ │ ├── app-sys-information.html
│ │ ├── clipboard.html
│ │ ├── protocol-handler.html
│ │ └── protocol-link.html
│ └── windows/
│ ├── crash-hang.html
│ ├── manage-modal.html
│ ├── modal-toggle-visibility.html
│ ├── modal.html
│ ├── process-crash.html
│ ├── process-hang.html
│ └── windows.html
└── test/
├── index.js
└── setup.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
================================================
FILE: .github/workflows/sentinel.yml
================================================
name: Generate Sentinel Report
on:
repository_dispatch:
types: generate-sentinel-report
jobs:
build-and-test:
name: Build Project
runs-on: ubuntu-latest
steps:
- name: Check Out Repository
uses: actions/checkout@v2
- name: Install Dependencies
run: npm ci
- name: Run Test Suite
run: npm run generate-test-report
- name: Upload Test Report
uses: actions/upload-artifact@v1
with:
name: report
path: report.json
send-data:
name: Generate Report
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Download Report
uses: actions/download-artifact@v1
with:
name: report
- name: Send Data to Sentinel
uses: codebytere/sentinel-client@v1
================================================
FILE: .gitignore
================================================
node_modules
out
npm-debug.log
================================================
FILE: .node-version
================================================
v8.9.3
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "node"
- "lts/*"
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
script:
- npm test
- npm run package:linux
branches:
only:
- master
cache:
directories:
- node_modules
notifications:
email:
on_success: never
on_failure: change
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses, without explicit permission
- Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [coc@electronjs.org](mailto:coc@electronjs.org). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from http://contributor-covenant.org/version/1/3/0/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Electron API Demos
[](http://standardjs.com)
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable
behavior to [coc@electronjs.org](mailto:coc@electronjs.org).
**See the [documentation](docs.md) for information on how this project works.**
## Code Style & ES6
This project uses the [JavaScript Standard](http://standardjs.com) style and limited ES6 syntax.
Because this project is intended for beginners we stick to mostly vanilla JavaScript. One of the features we want illustrate about Electron, however, is that you can use most of ES6 out of the box. To that end, in this project we use these parts of ES6:
- [`const`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const)
- [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let)
- [string templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)
## Pull Requests and Issues
Tips for making an easier-to-review contribution:
- Please provide a description.
- Include screenshots and animated GIFs whenever possible.
- Use short, present tense commit messages.
================================================
FILE: ISSUE_TEMPLATE.md
================================================
* What operating system are you using?
* What version of Node.js is on your system?
================================================
FILE: README.md
================================================
# Electron API Demos
[](https://travis-ci.org/electron/electron-api-demos)
[](http://standardjs.com)
This is a desktop app that interactively and with sample code demonstrates core features of the [Electron](http://electronjs.org) API. It's built with Electron, too, of course. This app works on Windows, macOS and Linux operating systems.
Use this app to see what you can do with Electron and use the source code to learn how to create a basic Electron app.

---
This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable
behavior to [coc@electronjs.org](mailto:coc@electronjs.org).
Follow [@ElectronJS](https://twitter.com/electronjs) on Twitter for important
announcements. Visit the [electron website](http://electronjs.org).
## Using
You can [download the latest release](https://github.com/electron/electron-api-demos/releases) for your operating system or build it yourself (see below).
## Building
You'll need [Node.js](https://nodejs.org) installed on your computer in order to build this app.
```bash
$ git clone https://github.com/electron/electron-api-demos
$ cd electron-api-demos
$ npm install
$ npm start
```
If you don't wish to clone, you can [download the source code](https://github.com/electron/electron-api-demos/archive/master.zip).
For easier developing you can launch the app in fullscreen with DevTools open:
```bash
$ npm run dev
```
## Extending
[Read the docs](docs.md) to learn more about how this app is built or how to [add a new demo](docs.md#add-a-section-or-demo).
## Translations
* Simplified Chinese translation of this app is available at [`demopark/electron-api-demos-Zh_CN`](https://github.com/demopark/electron-api-demos-Zh_CN).
* Traditional Chinese translation of this app is available at [`CalvertYang/electron-api-demos-zh-Hant`](https://github.com/CalvertYang/electron-api-demos-zh-Hant).
* Japanese translation of this app is available at [`LeeDDHH/electron-api-demos-ja`](https://github.com/LeeDDHH/electron-api-demos-ja).
Note: these versions are maintained by outside contributors and may not always be in sync with this version.
================================================
FILE: assets/code-blocks.js
================================================
const fs = require('fs')
const path = require('path')
const codeBlocksWithPaths = document.querySelectorAll('code[data-path]')
Array.prototype.forEach.call(codeBlocksWithPaths, (code) => {
const codePath = path.join(__dirname, '..', code.dataset.path)
const extension = path.extname(codePath)
code.classList.add(`language-${extension.substring(1)}`)
code.textContent = fs.readFileSync(codePath)
})
document.addEventListener('DOMContentLoaded', () => {
const highlight = require('highlight.js')
const codeBlocks = document.querySelectorAll('pre code')
Array.prototype.forEach.call(codeBlocks, (code) => {
highlight.highlightBlock(code)
})
})
================================================
FILE: assets/css/about.css
================================================
/* Welcome ------------------------ */
.about {
--about-space: 4rem;
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
overflow-x: hidden;
overflow-y: auto;
padding: 0;
background-color: hsl(0,0%,98%);
pointer-events: none;
visibility: hidden;
opacity: 0;
transform: scale(1.1);
transition: visibility 0s .12s linear , opacity .12s ease-in, transform .12s ease-in;
}
.about.is-shown {
pointer-events: auto;
visibility: visible;
opacity: 1;
transform: scale(1);
transition: visibility 0s 0s linear , opacity .24s ease-out, transform .24s ease-out;
}
.about-wrapper {
margin: auto;
}
.about-header {
padding: var(--about-space) 0;
border-bottom: 1px solid hsl(0,0%,88%);
}
.about-logo {
display: block;
margin: 0 auto;
width: 320px; /* TODO: Adjust asset to this size */
max-width: 100%;
}
.about-sections {
max-width: 680px;
padding: 0 var(--about-space);
}
.about-section {
margin: var(--about-space) 0;
}
.about h2 {
text-align: center;
margin: 0 0 1em 0;
font-size: 1.5em;
color: hsl(0, 0%, 55%);
}
.about .about-code h2 {
color: hsl(330, 65%, 55%);
}
.about .play-along h2 {
color: hsl(222, 53%, 50%);
}
.about-button {
display: block;
margin: 0 auto;
padding: .4em 1.2em;
font: inherit;
font-size: 1.6em;
color: inherit;
border: 2px solid;
border-radius: 4px;
background-color: transparent;
}
.about-button:focus {
outline: none;
border-color: hsl(0,0%,88%);
}
footer.about-section {
text-align: center;
}
.rainbow-button-wrapper {
--rainbow-button-width: 170px;
--rainbow-button-height: 50px;
--rainbow-button-width-inner: 164px;
--rainbow-button-height-inner: 44px;
--rainbow-color-1: hsl(116, 30%, 36%);
--rainbow-color-2: hsl(194, 60%, 36%);
--rainbow-color-3: hsl(222, 53%, 50%);
--rainbow-color-4: hsl(285, 47%, 46%);
--rainbow-color-5: hsl(330, 65%, 48%);
--rainbow-color-6: hsl(32, 79%, 49%);
--rainbow-color-7: hsl(53, 84%, 50%);
display: inline-block;
width: var(--rainbow-button-width);
height: var(--rainbow-button-height);
position: relative;
overflow: hidden;
border-radius: 5px;
}
.rainbow-button-wrapper:before {
display: block;
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 600px;
height: var(--rainbow-button-height);
background: #CCC;
background: linear-gradient(to right, var(--rainbow-color-1) 0%, var(--rainbow-color-2) 14%, var(--rainbow-color-3) 28%, var(--rainbow-color-4) 42%, var(--rainbow-color-5) 56%, var(--rainbow-color-6) 70%, var(--rainbow-color-7) 84%, var(--rainbow-color-1) 100%);
background-position: -200px 0;
transition: all 0.5s;
content: "";
}
.rainbow-button-wrapper button {
display: block;
width: var(--rainbow-button-width-inner);
height: var(--rainbow-button-height-inner);
position: absolute;
z-index: 3;
top: 3px;
left: 3px;
border: none;
background: white;
color: black;
font-size: 1.3rem;
}
.rainbow-button-wrapper:hover:before {
background-position: 200px 0;
}
@media (min-width: 940px) {
.about-header {
align-self: center;
padding: var(--about-space);
border-right: 1px solid hsl(0,0%,88%);
border-bottom: none;
}
.about-wrapper {
display: flex;
}
}
================================================
FILE: assets/css/demo.css
================================================
/* Demo */
.demo:first-of-type {
margin-top: 2rem;
}
.demo:last-of-type {
margin-bottom: 2rem;
}
@media (min-width: 940px) {
.demo:last-of-type {
margin-bottom: 4rem;
}
}
.demo-wrapper {
position: relative;
max-width: 740px;
margin: 0 auto;
padding: 0 2rem;
}
/* Toggle Button ----------------------------- */
.demo-toggle-button {
position: relative;
display: block;
margin: 0;
padding: .5em 1.5em;
line-height: 1.5;
font: inherit;
font-weight: 600;
font-size: 1.2em;
text-align: left;
border: none;
color: inherit;
background-color: transparent;
transition: border-color .12s;
outline: none;
}
.demo-toggle-button:before,
.demo-toggle-button:after {
content: "";
position: absolute;
left: 0;
width: 2px;
height: 50%;
background-color: hsl(0,0%,88%);
transition: transform .2s cubic-bezier(.4,.1,0,1);
}
.demo-toggle-button:before {
top: 0;
transform-origin: bottom center;
transform: translateX(.7em) rotate(-30deg) scale(.75);
}
.demo-toggle-button:after {
bottom: 0;
transform-origin: top center;
transform: translateX(.7em) rotate(30deg) scale(.75);
}
.is-open .demo-toggle-button:before,
.is-open .demo-toggle-button:after {
transform: rotate(0deg);
}
.demo-toggle-button:focus:before,
.demo-toggle-button:focus:after {
background-color: currentColor;
}
/* Meta info */
.demo-meta {
margin-top: .2em;
font-size: 11px;
font-weight: 300;
text-transform: uppercase;
color: var(--color-subtle);
}
.demo-meta-divider {
margin: 0 .5em;
}
/* Demo Box ----------------------------- */
.demo-box {
display: none;
position: relative;
padding: 2em;
margin-top: 1em;
margin-bottom: 2em;
border-radius: 6px;
border: 1px solid var(--color-border);
background-color: var(--color-bg);
}
.demo-box:before {
content: "";
position: absolute;
top: -11px;
width: 20px;
height: 20px;
background-color: inherit;
border-top: inherit;
border-right: inherit;
border-top-right-radius: 3px;
transform: rotate(-45deg);
}
.is-open .demo-box {
display: block;
animation: demo-box-fade-in .2s cubic-bezier(0, .20, .20, .96);
}
@keyframes demo-box-fade-in {
0% { opacity: 0; transform: translateY(-20px); }
100% { opacity: 1; transform: translateY(0); }
}
.demo-box > p:first-child {
margin-top: 0;
}
.demo-box h5 {
font-size: 1em;
margin-bottom: .6em;
}
/* Demo Controls ----------------------------- */
.demo-controls {
display: flex;
align-items: center;
}
.demo-button {
align-self: flex-start;
margin-right: 1em;
border: 2px solid;
border-radius: 4px;
font: inherit;
font-size: 1.2em;
padding: .4em 1.2em;
color: inherit;
background-color: transparent;
}
.demo-button:focus {
outline: none;
background-color: white;
}
.demo-button:active {
border-color: var(--color-border);
}
.demo-input {
flex: 1;
border: 2px solid var(--color-border);
border-radius: 4px;
font: inherit;
font-size: 1.2em;
padding: .4em .8em;
color: inherit;
background-color: transparent;
}
.demo-input:focus {
outline: none;
border-color: hsl(0,0%,80%);
background-color: white;
}
.demo-response {
flex: 1;
word-break: break-word;
}
.smooth-appear {
opacity: 1;
transition: opacity .5s ease-in-out;
}
.disappear {
opacity: 0;
}
.demo-button.smooth-disappear:focus {
outline: inherit;
border-color: inherit;
background-color: inherit;
}
/* ProTip ----------------------------- */
.demo-protip {
margin-top: 2rem;
padding: 1.5rem 2rem 2rem 2rem;
border: 1px solid hsla(0,0%,0%,.06);
border-radius: 6px;
background: var(--color-accent) linear-gradient(hsla(0,0%,100%,.85), hsla(0,0%,100%,.85));
}
.demo-protip h2 {
margin: 0 0 .5rem 0;
}
.demo-protip strong {
font-weight: 600;
}
================================================
FILE: assets/css/github.css
================================================
/*
github.com style (c) Vasily Polovnyov
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
================================================
FILE: assets/css/global.css
================================================
/* Fonts ---------------------------- */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: local('Source Code Pro'), local('SourceCodePro'), url(fonts/SourceCodePro-Regular.ttf) format('truetype');
}
/* Global ---------------------------- */
* {
box-sizing: border-box;
}
html {
height: 100%;
font-family: 'BlinkMacSystemFont', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif;
font-size: 14px;
line-height: 1.5;
overflow: hidden; /* Prevents rubber-band scrolling of the whole "page" */
color: var(--color);
background-color: #fff; /* To cover OSes with no default background color */
}
body {
margin: 0;
height: 100%;
display: flex;
}
a {
color: var(--color-link);
}
h1,
h2,
h3 {
margin-top: 0;
line-height: 1.5;
}
h1 {
font-size: 1.5em;
font-weight: 600;
}
h2 {
font-size: 1.3em;
font-weight: normal;
}
h3 {
font-size: 1.12em;
font-weight: 600;
}
table {
width: 100%;
border-spacing: 0;
border: 1px solid hsla(0,0%,0%,.08);
border-width: 0 1px 1px 0;
}
th {
background-color: hsla(0,0%,50%,.06);
}
th,
td {
text-align: center;
border: 1px solid hsla(0,0%,0%,.08);
border-width: 1px 0 0 1px;
}
svg {
fill: currentColor;
}
/* Code */
code, kbd {
font-family: 'Source Code Pro', monospace;
border-radius: 4px;
padding: 1px 4px;
white-space: nowrap;
color: hsl(0,0%,36%);
background-color: hsla(0,0%,60%,.15);
}
pre, kbd {
font-size: 13px;
overflow: auto;
padding: 1em;
margin: 0;
border-radius: 4px;
border: 1px solid;
border-color: var(--color-border);
background-color: white;
}
pre code {
white-space: pre;
}
pre > .hljs {
color: var(--color-subtle);
background-color: white;
}
kbd {
padding: 0.5em;
}
/* Utilities ---------------------------- */
.u-avoid-clicks {
pointer-events: none;
}
/* Visually hidden, but will be read by screen readers */
.u-visible-to-screen-reader {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.no-display {
display: none;
}
/* Content ------------------ */
.content {
flex: 1;
position: relative;
overflow: hidden;
visibility: hidden;
opacity: 0;
}
.content.is-shown {
visibility: visible;;
opacity: 1;
}
/* Hacks ---------------------------- */
/* Fixes horizontal scrolling in code blocks on OS X El Cap (10.11.3), retina screen
*
* By adding an invisible outline property, it will force a repaint
* which enables the scrolling.
*/
.hljs:hover,
.hljs:active {
outline: 1px solid transparent;
}
================================================
FILE: assets/css/nativize.css
================================================
/*
** nativize.css
** Makes the UI feel more native
*/
html {
font-family: sans-serif;
user-select: none; /* disable selection */
-webkit-user-drag: none; /* disable dragging */
cursor: default; /* use default cursor */
}
body {
margin: 0; /* remove default margin */
}
/* enable text selection */
.is-selectable,
pre,
code {
user-select: text;
cursor: auto;
}
/* Buttons and links */
button{
cursor: default;
}
/* Internal links */
a {
cursor: pointer;
text-decoration: none;
border-bottom: 1px dashed;
outline: none;
}
/* New window (target) + external links */
a[target],
a[href^="https://"],
a[href^="http://"] {
border-bottom: 1px solid;
}
a:hover,
a:focus {
border-bottom: none;
}
/* Images */
img {
-webkit-user-drag: none; /* disable dragging */
}
================================================
FILE: assets/css/nav.css
================================================
/* Nav */
.nav {
width: 340px;
overflow-x: hidden;
overflow-y: auto;
color: var(--color-subtle);
border-right: 1px solid var(--color-border);
background-color: var(--color-bg);
visibility: hidden;
opacity: 0;
}
.nav.is-shown {
visibility: visible;;
opacity: 1;
}
.nav-header {
position: relative;
padding: 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.nav-title {
text-transform: uppercase;
font-weight: 300;
line-height: 1;
margin: 0;
}
.nav-title strong {
font-weight: 600;
color: var(--color-strong);
}
.nav-header-icon {
position: absolute;
width: 36px;
height: 36px;
top: 1.5rem; /* magic */
right: 1.75rem; /* magic */
}
.nav-item {
padding: .5em 0;
}
.nav-icon {
width: 16px;
height: 16px;
vertical-align: top;
margin-right: .25rem;
}
.nav-category {
margin: .2em 0;
padding-left: 2rem;
font-size: 11px;
font-weight: normal;
text-transform: uppercase;
}
.nav-button {
display: block;
width: 100%;
padding: .3rem;
padding-left: calc(2rem + 16px + .5rem); /* padding + icon + magic */
line-height: 2;
text-align: left;
font: inherit;
font-size: 13px;
color: inherit;
border: none;
background-color: transparent;
cursor: default;
outline: none;
}
.nav-button:hover,
.nav-button:focus:not(.is-selected) {
background-color: hsla(0,0%,0%,.1);
}
.nav-button.is-selected {
background-color: var(--color-accent);
}
.nav-button.is-selected,
.nav-button.is-selected em {
color: #fff;
}
.nav-button.is-selected:focus {
opacity: .8;
}
.nav-button em {
font-style: normal;
font-weight: 600;
color: var(--color-strong);
pointer-events: none; /* makes it invisible to clicks */
}
.nav-footer {
margin-top: 1rem;
padding: 2rem;
border-top: 1px solid var(--color-border);
text-align: center;
}
.nav-footer-icon {
width: calc(770px / 6.5);
height: calc(88px / 6.5);
}
.nav-footer a {
outline: none;
}
.nav-footer-button {
display: block;
width: 100%;
padding: 0;
margin-bottom: .75rem;
line-height: 2;
text-align: left;
font: inherit;
font-size: 13px;
color: inherit;
border: none;
background-color: transparent;
cursor: default;
outline: none;
text-align: center;
}
.nav-footer-button:focus {
color: var(--color-strong);
}
.nav-footer-logo {
color: hsl(0,0%,66%);
}
.nav-footer-logo:focus {
color: hsl(0,0%,33%);
}
/* Remove border on the logo */
.nav-footer-logo.nav-footer-logo {
border-bottom: none;
}
================================================
FILE: assets/css/print.css
================================================
@media print {
body {
background: none;
color: black !important;
font-size: 70%;
margin: 0; padding: 0;
}
h1 {
font-size: 22px;
}
.nav, button, .demo-box:before,
header p {
display: none;
}
.demo-box, h2,
pre, code {
padding: 0 !important;
margin: 0 !important;
}
header {
padding: 0 0 10px 0;
}
code, .support {
font-size: 10px;
}
}
================================================
FILE: assets/css/section.css
================================================
/* Section ------------------ */
.section {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
color: var(--color-accent);
/* Hide */
pointer-events: none;
visibility: hidden;
opacity: 0;
transform: translateX(-20px);
transition: visibility 0s .12s linear , opacity .12s ease-in, transform .12s ease-in;
}
.section.is-shown {
pointer-events: auto;
visibility: visible;
opacity: 1;
transform: translateX(0);
transition: visibility 0s 0s linear , opacity .36s ease-out, transform .36s ease-out;
}
.section h3,
.section p {
color: var(--color);
}
.section-wrapper {
position: relative;
max-width: 740px;
margin: 0 auto;
padding: 2rem 2rem 1rem 2rem;
border-bottom: 1px solid var(--color-border);
}
@media (min-width: 940px) {
.section-wrapper {
padding-top: 4rem;
}
}
.section-icon {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: .5em;
}
================================================
FILE: assets/css/variables.css
================================================
/* Custom Properties */
:root {
--color: hsl(0,0%,22%);
--color-subtle: hsl(0,0%,44%);
--color-strong: hsl(0,0%,11%);
--color-link: hsl(0,0%,22%);
--color-border: hsl(0,0%,88%);
--color-bg: hsl(0,0%,96%);
--color-accent: black; /* Fallback */
}
/* Category Colors */
.u-category-windows { --color-accent: hsl(116, 30%, 36%); }
.u-category-menu { --color-accent: hsl(194, 60%, 36%); }
.u-category-native-ui { --color-accent: hsl(222, 53%, 50%); }
.u-category-communication { --color-accent: hsl(285, 47%, 46%); }
.u-category-system { --color-accent: hsl(330, 65%, 48%); }
.u-category-media { --color-accent: hsl( 36, 77%, 34%); }
================================================
FILE: assets/demo-btns.js
================================================
const settings = require('electron-settings')
const demoBtns = document.querySelectorAll('.js-container-target')
// Listen for demo button clicks
Array.prototype.forEach.call(demoBtns, (btn) => {
btn.addEventListener('click', (event) => {
const parent = event.target.parentElement
// Toggles the "is-open" class on the demo's parent element.
parent.classList.toggle('is-open')
// Saves the active demo if it is open, or clears it if the demo was user
// collapsed by the user
if (parent.classList.contains('is-open')) {
settings.set('activeDemoButtonId', event.target.getAttribute('id'))
} else {
settings.delete('activeDemoButtonId')
}
})
})
// Default to the demo that was active the last time the app was open
const buttonId = settings.get('activeDemoButtonId')
if (buttonId) {
document.getElementById(buttonId).click()
}
================================================
FILE: assets/ex-links.js
================================================
const shell = require('electron').shell
const links = document.querySelectorAll('a[href]')
Array.prototype.forEach.call(links, (link) => {
const url = link.getAttribute('href')
if (url.indexOf('http') === 0) {
link.addEventListener('click', (e) => {
e.preventDefault()
shell.openExternal(url)
})
}
})
================================================
FILE: assets/imports.js
================================================
const links = document.querySelectorAll('link[rel="import"]')
// Import and add each page to the DOM
Array.prototype.forEach.call(links, (link) => {
let template = link.import.querySelector('.task-template')
let clone = document.importNode(template.content, true)
if (link.href.match('about.html')) {
document.querySelector('body').appendChild(clone)
} else {
document.querySelector('.content').appendChild(clone)
}
})
================================================
FILE: assets/mac/child.plist
================================================
com.apple.security.app-sandboxcom.apple.security.inherit
================================================
FILE: assets/mac/info.plist
================================================
CFBundleURLTypesCFBundleURLSchemeselectron-api-demosCFBundleURLNameElectron API Demos ProtocolElectronTeamIDVEKTX9H2N7
================================================
FILE: assets/mac/parent.plist
================================================
com.apple.security.app-sandboxcom.apple.security.application-groupsVEKTX9H2N7.com.github.electron-api-demoscom.apple.security.files.user-selected.read-write
================================================
FILE: assets/nav.js
================================================
const settings = require('electron-settings')
document.body.addEventListener('click', (event) => {
if (event.target.dataset.section) {
handleSectionTrigger(event)
} else if (event.target.dataset.modal) {
handleModalTrigger(event)
} else if (event.target.classList.contains('modal-hide')) {
hideAllModals()
}
})
function handleSectionTrigger (event) {
hideAllSectionsAndDeselectButtons()
// Highlight clicked button and show view
event.target.classList.add('is-selected')
// Display the current section
const sectionId = `${event.target.dataset.section}-section`
document.getElementById(sectionId).classList.add('is-shown')
// Save currently active button in localStorage
const buttonId = event.target.getAttribute('id')
settings.set('activeSectionButtonId', buttonId)
}
function activateDefaultSection () {
document.getElementById('button-windows').click()
}
function showMainContent () {
document.querySelector('.js-nav').classList.add('is-shown')
document.querySelector('.js-content').classList.add('is-shown')
}
function handleModalTrigger (event) {
hideAllModals()
const modalId = `${event.target.dataset.modal}-modal`
document.getElementById(modalId).classList.add('is-shown')
}
function hideAllModals () {
const modals = document.querySelectorAll('.modal.is-shown')
Array.prototype.forEach.call(modals, (modal) => {
modal.classList.remove('is-shown')
})
showMainContent()
}
function hideAllSectionsAndDeselectButtons () {
const sections = document.querySelectorAll('.js-section.is-shown')
Array.prototype.forEach.call(sections, (section) => {
section.classList.remove('is-shown')
})
const buttons = document.querySelectorAll('.nav-button.is-selected')
Array.prototype.forEach.call(buttons, (button) => {
button.classList.remove('is-selected')
})
}
function displayAbout () {
document.querySelector('#about-modal').classList.add('is-shown')
}
// Default to the view that was active the last time the app was open
const sectionId = settings.get('activeSectionButtonId')
if (sectionId) {
showMainContent()
const section = document.getElementById(sectionId)
if (section) section.click()
} else {
activateDefaultSection()
displayAbout()
}
================================================
FILE: assets/normalize-shortcuts.js
================================================
const normalize = require('electron-shortcut-normalizer')
let shortcuts = document.querySelectorAll('kbd.normalize-to-platform')
Array.prototype.forEach.call(shortcuts, (shortcut) => {
shortcut.innerText = normalize(shortcut.innerText, process.platform)
})
================================================
FILE: cli.js
================================================
#!/usr/bin/env node
const {spawn} = require('child_process')
const electron = require('electron')
const path = require('path')
const appPath = path.join(__dirname, 'main.js')
const args = [appPath].concat(process.argv.slice(2))
const proc = spawn(electron, args, {stdio: 'inherit'})
proc.on('close', (code) => process.exit(code))
================================================
FILE: docs.md
================================================
# Documentation
This app has been developed to be a lightweight Electron app, demonstrating how to create a basic Electron app with a few exceptions that have been made for the sake of code organization in regards to the demos themselves.
All of the sample code shown in the app _is the actual code used in the app_. These JavaScript bits have been pulled out into their own file and organized by process (main or renderer) and then by section (communication, menus, native UI, media, system, windows).
This was done for maintainability—code updates only have to be made in one place—and organization—it's easy to find the sample code you're looking for.
All of the pages (or views) are separate `.html` files which are appended onto the `index.html` using [HTML imports](http://www.html5rocks.com/en/tutorials/webcomponents/imports/).
Are you looking to add a demo? Jump to the [add a new demo section](#add-a-section-or-demo).
## Folder Structure

#### `assets`
This directory contains assets for the app itself: CSS, fonts, images and shared JavaScript libraries or helpers.
#### `main-process`
This directory contains sub folders for each demo section that requires JavaScript in the main process. This structure is mirrored in the `renderer-process` directory.
The `main.js` file, located in the root, takes each `.js` file in these directories and executes them.
#### `renderer-process`
This directory contains sub folders for each demo section that requires JavaScript in the renderer process. This structure is mirrored in the `main-process` directory.
Each of the HTML page views requires the corresponding renderer-process `.js` files that it needs for its demo.
Each page view reads the content of its relevant main and renderer process files and adds them to the page for the snippets.
#### `sections`
This directory contains sub folders for each demo section. These subfolders contain the HTML files for each of the demo pages. Each of these files is appended to `index.html`, located at the root.
#### `index.html`
This is the main view in the app. It contains the sidebar with navigation and uses [HTML imports](http://www.html5rocks.com/en/tutorials/webcomponents/imports/) to append each section HTML page to the `body`.
#### `main.js`
This file contains the lifecycle instructions for the app like how to start and quit, it is the app's main process. It grabs every `.js` file in the `main-process` directory and executes.
The `package.json` sets this file as the `main` file.
#### `package.json`
This file is required when using `npm` and Electron.js. It contains details on the app: the author, dependencies, repository and points to `main.js` as the application's main process file.
#### Docs
The files: `CODE_OF_CONDUCT`, `README`, `docs` and `CONTRIBUTING` files make up the documentation for the project.
## UI Terminology

## CSS Naming Convention
Nothing too strict and used more as a guide:
- Styling elements directly should be avoided, but ok in some cases. Like `
` or ``.
- Elements that belong together are prefixed with their parent class. `.section`, `.section-header`, `.section-icon`.
- States use `is-` prefix
- Utilities use `u-` prefix
## Add a Section or Demo
Here are tips for covering the bases when adding a new section or demo. General tip—for some of these just copy the line or file of a similar existing item to get started!
### New Section
A whole new page with one or more demos.
#### index.html
This page contains the sidebar list of sections as well as each section template that is imported with HTML imports.
- Add demo to sidebar in the appropriate category in `index.html`
- update `id` i.e. `id="button-dialogs"`
- update `data-section` i.e. `data-section="dialogs"`
- Add demo template path to the import links in the `head` of `index.html`
- i.e. ``
#### Template
This template is added to the `index.html` in the app.
- In the `sections` directory, copy an existing template `html` file from the category you're adding a section to.
- Update these tags `id`
- i.e. `id="dialogs-section"`
- Update all the text in the `header` tag with text relevant to your new section.
- Remove the demos and pro-tips as needed.
### Demo
Any code that you create for your demo should be added to the 'main-process' or 'renderer-process' directories depending on where it runs.
All JavaScript files within the 'main-process' directory are run when the app starts but you'll link to the file so that it is displayed within your demo (see below).
The renderer process code you add will be read and displayed within the demo and then required on the template page so that it runs in that process (see below).
- Start by copying and pasting an existing `
` blocks from the template page.
- Update the demo button `id`
- i.e ``
- If demo includes a response written to the DOM, update that `id`, otherwise delete:
- i.e. ``
- Update the text describing your demo.
- If you are displaying main or renderer process sample code, include or remove that markup accordingly.
- Sample code is read and added to the DOM by adding the path to the code in the `data-path`
- i.e. `
`
- Require your render process code in the script tag at the bottom of the template
- i.e `require('./renderer-process/native-ui/dialogs/information')`
#### Try it out
At this point you should be able to run the app, `npm start`, and see your section and/or demo. :tada:
================================================
FILE: index.html
================================================
================================================
FILE: license.md
================================================
MIT License
Copyright (c) 2016-2018 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: main-process/communication/async-msg.js
================================================
const {ipcMain} = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
event.sender.send('asynchronous-reply', 'pong')
})
================================================
FILE: main-process/communication/sync-msg.js
================================================
const {ipcMain} = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
event.returnValue = 'pong'
})
================================================
FILE: main-process/menus/application-menu.js
================================================
const {BrowserWindow, Menu, app, shell, dialog} = require('electron')
let template = [{
label: 'Edit',
submenu: [{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
role: 'undo'
}, {
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
role: 'redo'
}, {
type: 'separator'
}, {
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut'
}, {
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
}, {
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
}, {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
}]
}, {
label: 'View',
submenu: [{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: (item, focusedWindow) => {
if (focusedWindow) {
// on reload, start fresh and close any old
// open secondary windows
if (focusedWindow.id === 1) {
BrowserWindow.getAllWindows().forEach(win => {
if (win.id > 1) win.close()
})
}
focusedWindow.reload()
}
}
}, {
label: 'Toggle Full Screen',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Ctrl+Command+F'
} else {
return 'F11'
}
})(),
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
}
}
}, {
label: 'Toggle Developer Tools',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I'
} else {
return 'Ctrl+Shift+I'
}
})(),
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.toggleDevTools()
}
}
}, {
type: 'separator'
}, {
label: 'App Menu Demo',
click: function (item, focusedWindow) {
if (focusedWindow) {
const options = {
type: 'info',
title: 'Application Menu Demo',
buttons: ['Ok'],
message: 'This demo is for the Menu section, showing how to create a clickable menu item in the application menu.'
}
dialog.showMessageBox(focusedWindow, options, function () {})
}
}
}]
}, {
label: 'Window',
role: 'window',
submenu: [{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}, {
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}, {
type: 'separator'
}, {
label: 'Reopen Window',
accelerator: 'CmdOrCtrl+Shift+T',
enabled: false,
key: 'reopenMenuItem',
click: () => {
app.emit('activate')
}
}]
}, {
label: 'Help',
role: 'help',
submenu: [{
label: 'Learn More',
click: () => {
shell.openExternal('http://electron.atom.io')
}
}]
}]
function addUpdateMenuItems (items, position) {
if (process.mas) return
const version = app.getVersion()
let updateItems = [{
label: `Version ${version}`,
enabled: false
}, {
label: 'Checking for Update',
enabled: false,
key: 'checkingForUpdate'
}, {
label: 'Check for Update',
visible: false,
key: 'checkForUpdate',
click: () => {
require('electron').autoUpdater.checkForUpdates()
}
}, {
label: 'Restart and Install Update',
enabled: true,
visible: false,
key: 'restartToUpdate',
click: () => {
require('electron').autoUpdater.quitAndInstall()
}
}]
items.splice.apply(items, [position, 0].concat(updateItems))
}
function findReopenMenuItem () {
const menu = Menu.getApplicationMenu()
if (!menu) return
let reopenMenuItem
menu.items.forEach(item => {
if (item.submenu) {
item.submenu.items.forEach(item => {
if (item.key === 'reopenMenuItem') {
reopenMenuItem = item
}
})
}
})
return reopenMenuItem
}
if (process.platform === 'darwin') {
const name = app.getName()
template.unshift({
label: name,
submenu: [{
label: `About ${name}`,
role: 'about'
}, {
type: 'separator'
}, {
label: 'Services',
role: 'services',
submenu: []
}, {
type: 'separator'
}, {
label: `Hide ${name}`,
accelerator: 'Command+H',
role: 'hide'
}, {
label: 'Hide Others',
accelerator: 'Command+Alt+H',
role: 'hideothers'
}, {
label: 'Show All',
role: 'unhide'
}, {
type: 'separator'
}, {
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit()
}
}]
})
// Window menu.
template[3].submenu.push({
type: 'separator'
}, {
label: 'Bring All to Front',
role: 'front'
})
addUpdateMenuItems(template[0].submenu, 1)
}
if (process.platform === 'win32') {
const helpMenu = template[template.length - 1].submenu
addUpdateMenuItems(helpMenu, 0)
}
app.on('ready', () => {
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
})
app.on('browser-window-created', () => {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = false
})
app.on('window-all-closed', () => {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = true
})
================================================
FILE: main-process/menus/context-menu.js
================================================
const {
BrowserWindow,
Menu,
MenuItem,
ipcMain,
app
} = require('electron')
const menu = new Menu()
menu.append(new MenuItem({ label: 'Hello' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: 'Electron', type: 'checkbox', checked: true }))
app.on('browser-window-created', (event, win) => {
win.webContents.on('context-menu', (e, params) => {
menu.popup(win, params.x, params.y)
})
})
ipcMain.on('show-context-menu', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
menu.popup(win)
})
================================================
FILE: main-process/menus/shortcuts.js
================================================
const {app, dialog, globalShortcut} = require('electron')
app.on('ready', () => {
globalShortcut.register('CommandOrControl+Alt+K', () => {
dialog.showMessageBox({
type: 'info',
message: 'Success!',
detail: 'You pressed the registered global shortcut keybinding.',
buttons: ['OK']
})
})
})
app.on('will-quit', () => {
globalShortcut.unregisterAll()
})
================================================
FILE: main-process/native-ui/dialogs/error.js
================================================
const {ipcMain, dialog} = require('electron')
ipcMain.on('open-error-dialog', (event) => {
dialog.showErrorBox('An Error Message', 'Demonstrating an error message.')
})
================================================
FILE: main-process/native-ui/dialogs/information.js
================================================
const {ipcMain, dialog} = require('electron')
ipcMain.on('open-information-dialog', (event) => {
const options = {
type: 'info',
title: 'Information',
message: "This is an information dialog. Isn't it nice?",
buttons: ['Yes', 'No']
}
dialog.showMessageBox(options, (index) => {
event.sender.send('information-dialog-selection', index)
})
})
================================================
FILE: main-process/native-ui/dialogs/open-file.js
================================================
const {ipcMain, dialog} = require('electron')
ipcMain.on('open-file-dialog', (event) => {
dialog.showOpenDialog({
properties: ['openFile', 'openDirectory']
}, (files) => {
if (files) {
event.sender.send('selected-directory', files)
}
})
})
================================================
FILE: main-process/native-ui/dialogs/save.js
================================================
const {ipcMain, dialog} = require('electron')
ipcMain.on('save-dialog', (event) => {
const options = {
title: 'Save an Image',
filters: [
{ name: 'Images', extensions: ['jpg', 'png', 'gif'] }
]
}
dialog.showSaveDialog(options, (filename) => {
event.sender.send('saved-file', filename)
})
})
================================================
FILE: main-process/native-ui/drag/drag.js
================================================
const {ipcMain} = require('electron')
const path = require('path')
ipcMain.on('ondragstart', (event, filepath) => {
const iconName = 'codeIcon.png'
event.sender.startDrag({
file: filepath,
icon: path.join(__dirname, iconName)
})
})
================================================
FILE: main-process/native-ui/tray/tray.js
================================================
const path = require('path')
const {ipcMain, app, Menu, Tray} = require('electron')
let appIcon = null
ipcMain.on('put-in-tray', (event) => {
const iconName = process.platform === 'win32' ? 'windows-icon.png' : 'iconTemplate.png'
const iconPath = path.join(__dirname, iconName)
appIcon = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([{
label: 'Remove',
click: () => {
event.sender.send('tray-removed')
}
}])
appIcon.setToolTip('Electron Demo in the tray.')
appIcon.setContextMenu(contextMenu)
})
ipcMain.on('remove-tray', () => {
appIcon.destroy()
})
app.on('window-all-closed', () => {
if (appIcon) appIcon.destroy()
})
================================================
FILE: main-process/system/app-information.js
================================================
const {app, ipcMain} = require('electron')
ipcMain.on('get-app-path', (event) => {
event.sender.send('got-app-path', app.getAppPath())
})
================================================
FILE: main-process/system/protocol-handler.js
================================================
const {app, dialog} = require('electron')
const path = require('path')
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('electron-api-demos', process.execPath, [path.resolve(process.argv[1])])
}
} else {
app.setAsDefaultProtocolClient('electron-api-demos')
}
app.on('open-url', (event, url) => {
dialog.showErrorBox('Welcome Back', `You arrived from: ${url}`)
})
================================================
FILE: main.js
================================================
require('update-electron-app')({
logger: require('electron-log')
})
const path = require('path')
const glob = require('glob')
const {app, BrowserWindow} = require('electron')
const debug = /--debug/.test(process.argv[2])
if (process.mas) app.setName('Electron APIs')
let mainWindow = null
function initialize () {
makeSingleInstance()
loadDemos()
function createWindow () {
const windowOptions = {
width: 1080,
minWidth: 680,
height: 840,
title: app.getName(),
webPreferences: {
nodeIntegration: true
}
}
if (process.platform === 'linux') {
windowOptions.icon = path.join(__dirname, '/assets/app-icon/png/512.png')
}
mainWindow = new BrowserWindow(windowOptions)
mainWindow.loadURL(path.join('file://', __dirname, '/index.html'))
// Launch fullscreen with DevTools open, usage: npm run debug
if (debug) {
mainWindow.webContents.openDevTools()
mainWindow.maximize()
require('devtron').install()
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.on('ready', () => {
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
}
// Make this app a single instance app.
//
// The main window will be restored and focused instead of a second window
// opened when a person attempts to launch a second instance.
//
// Returns true if the current version of the app should quit instead of
// launching.
function makeSingleInstance () {
if (process.mas) return
app.requestSingleInstanceLock()
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
})
}
// Require each JS file in the main-process dir
function loadDemos () {
const files = glob.sync(path.join(__dirname, 'main-process/**/*.js'))
files.forEach((file) => { require(file) })
}
initialize()
================================================
FILE: package.json
================================================
{
"name": "electron-api-demos",
"productName": "Electron API Demos",
"version": "2.0.2",
"description": "Electron interactive API demos",
"main": "main.js",
"bin": "cli.js",
"scripts": {
"start": "electron .",
"dev": "electron . --debug",
"test": "mocha && standard",
"generate-test-report": "mocha --reporter=json > report.json",
"package": "npm-run-all package:*",
"package:mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --out=out --icon=assets/app-icon/mac/app.icns --osx-sign.identity='Developer ID Application: GitHub' --extend-info=assets/mac/info.plist",
"package:win": "electron-packager . --overwrite --platform=win32 --arch=ia32 --out=out --icon=assets/app-icon/win/app.ico",
"package:linux": "electron-packager . --overwrite --platform=linux --arch=x64 --out=out",
"package:sign-exe": "signcode './out/Electron API Demos-win32-ia32/Electron API Demos.exe' --cert ~/electron-api-demos.p12 --prompt --name 'Electron API Demos' --url 'http://electron.atom.io'",
"package:installer": "node ./script/installer.js",
"package:sign-installer": "signcode './out/windows-installer/ElectronAPIDemosSetup.exe' --cert ~/electron-api-demos.p12 --prompt --name 'Electron API Demos' --url 'http://electron.atom.io'",
"xpackage:mas": "./script/mas.sh",
"windows-store": "node ./script/windows-store.js",
"release": "node ./script/release.js",
"prepack": "check-for-leaks",
"prepush": "check-for-leaks"
},
"repository": "https://github.com/electron/electron-api-demos",
"keywords": [
"Electron",
"API",
"demo"
],
"author": "GitHub",
"license": "MIT",
"devDependencies": {
"@octokit/rest": "^16.3.2",
"chai": "^3.4.1",
"chai-as-promised": "^6.0.0",
"check-for-leaks": "^1.2.1",
"devtron": "^1.3.0",
"electron-packager": "^16.0.0",
"electron-winstaller": "^2.2.0",
"husky": "^0.14.3",
"mocha": "^5.2.0",
"npm-run-all": "^4.0.2",
"request": "^2.70.0",
"rimraf": "^2.5.2",
"signcode": "^1.0.0",
"spectron": "^5.0.0",
"standard": "^8.2.0",
"tap": "^14.10.6"
},
"dependencies": {
"electron": "^15.5.5",
"electron-log": "^2.2.14",
"electron-settings": "^3.0.7",
"electron-shortcut-normalizer": "^1.0.0",
"glob": "^7.1.0",
"highlight.js": "^10.4.1",
"update-electron-app": "^1.1.1"
},
"standard": {
"env": {
"mocha": true
}
}
}
================================================
FILE: renderer-process/communication/async-msg.js
================================================
const {ipcRenderer} = require('electron')
const asyncMsgBtn = document.getElementById('async-msg')
asyncMsgBtn.addEventListener('click', () => {
ipcRenderer.send('asynchronous-message', 'ping')
})
ipcRenderer.on('asynchronous-reply', (event, arg) => {
const message = `Asynchronous message reply: ${arg}`
document.getElementById('async-reply').innerHTML = message
})
================================================
FILE: renderer-process/communication/invisible-msg.js
================================================
const {BrowserWindow} = require('electron').remote
const ipcRenderer = require('electron').ipcRenderer
const path = require('path')
const invisMsgBtn = document.getElementById('invis-msg')
const invisReply = document.getElementById('invis-reply')
invisMsgBtn.addEventListener('click', (clickEvent) => {
const windowID = BrowserWindow.getFocusedWindow().id
const invisPath = `file://${path.join(__dirname, '../../sections/communication/invisible.html')}`
let win = new BrowserWindow({
width: 400,
height: 400,
show: false,
webPreferences: {
nodeIntegration: true
}
})
win.loadURL(invisPath)
win.webContents.on('did-finish-load', () => {
const input = 100
win.webContents.send('compute-factorial', input, windowID)
})
})
ipcRenderer.on('factorial-computed', (event, input, output) => {
const message = `The factorial of ${input} is ${output}`
invisReply.textContent = message
})
================================================
FILE: renderer-process/communication/sync-msg.js
================================================
const {ipcRenderer} = require('electron')
const syncMsgBtn = document.getElementById('sync-msg')
syncMsgBtn.addEventListener('click', () => {
const reply = ipcRenderer.sendSync('synchronous-message', 'ping')
const message = `Synchronous message reply: ${reply}`
document.getElementById('sync-reply').innerHTML = message
})
================================================
FILE: renderer-process/media/desktop-capturer.js
================================================
const {desktopCapturer, shell} = require('electron')
const {screen} = require('electron').remote
const fs = require('fs')
const os = require('os')
const path = require('path')
const screenshot = document.getElementById('screen-shot')
const screenshotMsg = document.getElementById('screenshot-path')
screenshot.addEventListener('click', (event) => {
screenshotMsg.textContent = 'Gathering screens...'
const thumbSize = determineScreenShotSize()
let options = { types: ['screen'], thumbnailSize: thumbSize }
desktopCapturer.getSources(options, (error, sources) => {
if (error) return console.log(error)
sources.forEach((source) => {
if (source.name === 'Entire Screen' || source.name === 'Screen 1') {
const screenshotPath = path.join(os.tmpdir(), 'screenshot.png')
fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => {
if (error) return console.log(error)
shell.openExternal(`file://${screenshotPath}`)
const message = `Saved screenshot to: ${screenshotPath}`
screenshotMsg.textContent = message
})
}
})
})
})
function determineScreenShotSize () {
const screenSize = screen.getPrimaryDisplay().workAreaSize
const maxDimension = Math.max(screenSize.width, screenSize.height)
return {
width: maxDimension * window.devicePixelRatio,
height: maxDimension * window.devicePixelRatio
}
}
================================================
FILE: renderer-process/menus/context-menu.js
================================================
const {ipcRenderer} = require('electron')
// Tell main process to show the menu when demo button is clicked
const contextMenuBtn = document.getElementById('context-menu')
contextMenuBtn.addEventListener('click', () => {
ipcRenderer.send('show-context-menu')
})
================================================
FILE: renderer-process/native-ui/dialogs/error.js
================================================
const {ipcRenderer} = require('electron')
const errorBtn = document.getElementById('error-dialog')
errorBtn.addEventListener('click', (event) => {
ipcRenderer.send('open-error-dialog')
})
================================================
FILE: renderer-process/native-ui/dialogs/information.js
================================================
const {ipcRenderer} = require('electron')
const informationBtn = document.getElementById('information-dialog')
informationBtn.addEventListener('click', (event) => {
ipcRenderer.send('open-information-dialog')
})
ipcRenderer.on('information-dialog-selection', (event, index) => {
let message = 'You selected '
if (index === 0) message += 'yes.'
else message += 'no.'
document.getElementById('info-selection').innerHTML = message
})
================================================
FILE: renderer-process/native-ui/dialogs/open-file.js
================================================
const {ipcRenderer} = require('electron')
const selectDirBtn = document.getElementById('select-directory')
selectDirBtn.addEventListener('click', (event) => {
ipcRenderer.send('open-file-dialog')
})
ipcRenderer.on('selected-directory', (event, path) => {
document.getElementById('selected-file').innerHTML = `You selected: ${path}`
})
================================================
FILE: renderer-process/native-ui/dialogs/save.js
================================================
const {ipcRenderer} = require('electron')
const saveBtn = document.getElementById('save-dialog')
saveBtn.addEventListener('click', (event) => {
ipcRenderer.send('save-dialog')
})
ipcRenderer.on('saved-file', (event, path) => {
if (!path) path = 'No path'
document.getElementById('file-saved').innerHTML = `Path selected: ${path}`
})
================================================
FILE: renderer-process/native-ui/drag/drag.js
================================================
const {ipcRenderer} = require('electron')
const dragFileLink = document.getElementById('drag-file-link')
dragFileLink.addEventListener('dragstart', (event) => {
event.preventDefault()
ipcRenderer.send('ondragstart', __filename)
})
================================================
FILE: renderer-process/native-ui/ex-links-file-manager/ex-links.js
================================================
const {shell} = require('electron')
const exLinksBtn = document.getElementById('open-ex-links')
exLinksBtn.addEventListener('click', (event) => {
shell.openExternal('http://electron.atom.io')
})
================================================
FILE: renderer-process/native-ui/ex-links-file-manager/file-manager.js
================================================
const {shell} = require('electron')
const os = require('os')
const fileManagerBtn = document.getElementById('open-file-manager')
fileManagerBtn.addEventListener('click', (event) => {
shell.showItemInFolder(os.homedir())
})
================================================
FILE: renderer-process/native-ui/notifications/advanced-notification.js
================================================
const path = require('path')
const notification = {
title: 'Notification with image',
body: 'Short message plus a custom image',
icon: path.join(__dirname, '../../../assets/img/programming.png')
}
const notificationButton = document.getElementById('advanced-noti')
notificationButton.addEventListener('click', () => {
const myNotification = new window.Notification(notification.title, notification)
myNotification.onclick = () => {
console.log('Notification clicked')
}
})
================================================
FILE: renderer-process/native-ui/notifications/basic-notification.js
================================================
const notification = {
title: 'Basic Notification',
body: 'Short message part'
}
const notificationButton = document.getElementById('basic-noti')
notificationButton.addEventListener('click', () => {
const myNotification = new window.Notification(notification.title, notification)
myNotification.onclick = () => {
console.log('Notification clicked')
}
})
================================================
FILE: renderer-process/native-ui/tray/tray.js
================================================
const ipc = require('electron').ipcRenderer
const trayBtn = document.getElementById('put-in-tray')
let trayOn = false
trayBtn.addEventListener('click', function (event) {
if (trayOn) {
trayOn = false
document.getElementById('tray-countdown').innerHTML = ''
ipc.send('remove-tray')
} else {
trayOn = true
const message = 'Click demo again to remove.'
document.getElementById('tray-countdown').innerHTML = message
ipc.send('put-in-tray')
}
})
// Tray removed from context menu on icon
ipc.on('tray-removed', function () {
ipc.send('remove-tray')
trayOn = false
document.getElementById('tray-countdown').innerHTML = ''
})
================================================
FILE: renderer-process/system/app-information.js
================================================
const {ipcRenderer} = require('electron')
const appInfoBtn = document.getElementById('app-info')
appInfoBtn.addEventListener('click', () => {
ipcRenderer.send('get-app-path')
})
ipcRenderer.on('got-app-path', (event, path) => {
const message = `This app is located at: ${path}`
document.getElementById('got-app-info').innerHTML = message
})
================================================
FILE: renderer-process/system/copy.js
================================================
const {clipboard} = require('electron')
const copyBtn = document.getElementById('copy-to')
const copyInput = document.getElementById('copy-to-input')
copyBtn.addEventListener('click', () => {
if (copyInput.value !== '') copyInput.value = ''
copyInput.placeholder = 'Copied! Paste here to see.'
clipboard.writeText('Electron Demo!')
})
================================================
FILE: renderer-process/system/paste.js
================================================
const {clipboard} = require('electron')
const pasteBtn = document.getElementById('paste-to')
pasteBtn.addEventListener('click', () => {
clipboard.writeText('What a demo!')
const message = `Clipboard contents: ${clipboard.readText()}`
document.getElementById('paste-from').innerHTML = message
})
================================================
FILE: renderer-process/system/protocol-handler.js
================================================
const {shell} = require('electron')
const path = require('path')
const protocolHandlerBtn = document.getElementById('protocol-handler')
protocolHandlerBtn.addEventListener('click', () => {
const pageDirectory = __dirname.replace('app.asar', 'app.asar.unpacked')
const pagePath = path.join('file://', pageDirectory, '../../sections/system/protocol-link.html')
shell.openExternal(pagePath)
})
================================================
FILE: renderer-process/system/screen-information.js
================================================
const {screen} = require('electron').remote
const screenInfoBtn = document.getElementById('screen-info')
const size = screen.getPrimaryDisplay().size
screenInfoBtn.addEventListener('click', () => {
const message = `Your screen is: ${size.width}px x ${size.height}px`
document.getElementById('got-screen-info').innerHTML = message
})
================================================
FILE: renderer-process/system/sys-information.js
================================================
const os = require('os')
const homeDir = os.homedir()
const sysInfoBtn = document.getElementById('sys-info')
sysInfoBtn.addEventListener('click', () => {
const message = `Your system home directory is: ${homeDir}`
document.getElementById('got-sys-info').innerHTML = message
})
================================================
FILE: renderer-process/system/version-information.js
================================================
const versionInfoBtn = document.getElementById('version-info')
const electronVersion = process.versions.electron
versionInfoBtn.addEventListener('click', () => {
const message = `This app is using Electron version: ${electronVersion}`
document.getElementById('got-version-info').innerHTML = message
})
================================================
FILE: renderer-process/windows/create-window.js
================================================
const {BrowserWindow} = require('electron').remote
const path = require('path')
const newWindowBtn = document.getElementById('new-window')
newWindowBtn.addEventListener('click', (event) => {
const modalPath = path.join('file://', __dirname, '../../sections/windows/modal.html')
let win = new BrowserWindow({ width: 400, height: 320 })
win.on('close', () => { win = null })
win.loadURL(modalPath)
win.show()
})
================================================
FILE: renderer-process/windows/frameless-window.js
================================================
const {BrowserWindow} = require('electron').remote
const newWindowBtn = document.getElementById('frameless-window')
const path = require('path')
newWindowBtn.addEventListener('click', (event) => {
const modalPath = path.join('file://', __dirname, '../../sections/windows/modal.html')
let win = new BrowserWindow({ frame: false })
win.on('close', () => { win = null })
win.loadURL(modalPath)
win.show()
})
================================================
FILE: renderer-process/windows/manage-window.js
================================================
const {BrowserWindow} = require('electron').remote
const path = require('path')
const manageWindowBtn = document.getElementById('manage-window')
let win
manageWindowBtn.addEventListener('click', (event) => {
const modalPath = path.join('file://', __dirname, '../../sections/windows/manage-modal.html')
win = new BrowserWindow({ width: 400, height: 275 })
win.on('resize', updateReply)
win.on('move', updateReply)
win.on('close', () => { win = null })
win.loadURL(modalPath)
win.show()
function updateReply () {
const manageWindowReply = document.getElementById('manage-window-reply')
const message = `Size: ${win.getSize()} Position: ${win.getPosition()}`
manageWindowReply.innerText = message
}
})
================================================
FILE: renderer-process/windows/process-crash.js
================================================
const {BrowserWindow, dialog} = require('electron').remote
const path = require('path')
const processCrashBtn = document.getElementById('process-crash')
processCrashBtn.addEventListener('click', (event) => {
const crashWinPath = path.join('file://', __dirname, '../../sections/windows/process-crash.html')
let win = new BrowserWindow({
width: 400,
height: 320,
webPreferences: {
nodeIntegration: true
}
});
win.webContents.on('crashed', () => {
const options = {
type: 'info',
title: 'Renderer Process Crashed',
message: 'This process has crashed.',
buttons: ['Reload', 'Close']
}
dialog.showMessageBox(options, (index) => {
if (index === 0) win.reload()
else win.close()
})
})
win.on('close', () => { win = null })
win.loadURL(crashWinPath)
win.show()
})
================================================
FILE: renderer-process/windows/process-hang.js
================================================
const {BrowserWindow, dialog} = require('electron').remote
const path = require('path')
const processHangBtn = document.getElementById('process-hang')
processHangBtn.addEventListener('click', (event) => {
const hangWinPath = path.join('file://', __dirname, '../../sections/windows/process-hang.html')
let win = new BrowserWindow({
width: 400,
height: 320,
webPreferences: {
nodeIntegration: true
}
});
win.on('unresponsive', () => {
const options = {
type: 'info',
title: 'Renderer Process Hanging',
message: 'This process is hanging.',
buttons: ['Reload', 'Close']
}
dialog.showMessageBox(options, (index) => {
if (index === 0) win.reload()
else win.close()
})
})
win.on('close', () => { win = null })
win.loadURL(hangWinPath)
win.show()
})
================================================
FILE: renderer-process/windows/using-window-events.js
================================================
const {BrowserWindow} = require('electron').remote
const path = require('path')
const manageWindowBtn = document.getElementById('listen-to-window')
const focusModalBtn = document.getElementById('focus-on-modal-window')
let win
manageWindowBtn.addEventListener('click', () => {
const modalPath = path.join('file://', __dirname, '../../sections/windows/modal-toggle-visibility.html')
win = new BrowserWindow({ width: 600, height: 400 })
const hideFocusBtn = () => {
focusModalBtn.classList.add('disappear')
focusModalBtn.classList.remove('smooth-appear')
focusModalBtn.removeEventListener('click', clickHandler)
}
const showFocusBtn = (btn) => {
if (!win) return
focusModalBtn.classList.add('smooth-appear')
focusModalBtn.classList.remove('disappear')
focusModalBtn.addEventListener('click', clickHandler)
}
win.on('focus', hideFocusBtn)
win.on('blur', showFocusBtn)
win.on('close', () => {
hideFocusBtn()
win = null
})
win.loadURL(modalPath)
win.show()
const clickHandler = () => { win.focus() }
})
================================================
FILE: script/installer.js
================================================
#!/usr/bin/env node
const createWindowsInstaller = require('electron-winstaller').createWindowsInstaller
const path = require('path')
const rimraf = require('rimraf')
deleteOutputFolder()
.then(getInstallerConfig)
.then(createWindowsInstaller)
.catch((error) => {
console.error(error.message || error)
process.exit(1)
})
function getInstallerConfig () {
const rootPath = path.join(__dirname, '..')
const outPath = path.join(rootPath, 'out')
return Promise.resolve({
appDirectory: path.join(outPath, 'Electron API Demos-win32-ia32'),
exe: 'Electron API Demos.exe',
iconUrl: 'https://raw.githubusercontent.com/electron/electron-api-demos/master/assets/app-icon/win/app.ico',
loadingGif: path.join(rootPath, 'assets', 'img', 'loading.gif'),
noMsi: true,
outputDirectory: path.join(outPath, 'windows-installer'),
setupExe: 'ElectronAPIDemosSetup.exe',
setupIcon: path.join(rootPath, 'assets', 'app-icon', 'win', 'app.ico'),
skipUpdateIcon: true
})
}
function deleteOutputFolder () {
return new Promise((resolve, reject) => {
rimraf(path.join(__dirname, '..', 'out', 'windows-installer'), (error) => {
error ? reject(error) : resolve()
})
})
}
================================================
FILE: script/mas.sh
================================================
#!/bin/bash
set -ex
# App Store does not allow the word "demos" in the app's name
APP="Electron APIs"
electron-packager . \
"$APP" \
--asar \
--asar-unpack=protocol-link.html \
--overwrite \
--platform=mas \
--app-bundle-id=com.github.electron-api-demos \
--app-version="$npm_package_version" \
--build-version="1.1.0" \
--arch=x64 \
--icon=assets/app-icon/mac/app.icns \
--prune=true \
--out=out \
--extend-info=assets/mac/info.plist
APP_PATH="./out/$APP-mas-x64/$APP.app"
RESULT_PATH="./out/$APP.pkg"
APP_KEY="3rd Party Mac Developer Application: GitHub (VEKTX9H2N7)"
INSTALLER_KEY="3rd Party Mac Developer Installer: GitHub (VEKTX9H2N7)"
FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks"
CHILD_PLIST="./assets/mac/child.plist"
PARENT_PLIST="./assets/mac/parent.plist"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Electron Framework"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework/Versions/A/Libraries/libnode.dylib"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/Electron Framework.framework"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/Contents/MacOS/$APP Helper"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper.app/"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/Contents/MacOS/$APP Helper EH"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper EH.app/"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/Contents/MacOS/$APP Helper NP"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$FRAMEWORKS_PATH/$APP Helper NP.app/"
codesign -s "$APP_KEY" -f --entitlements "$CHILD_PLIST" "$APP_PATH/Contents/MacOS/$APP"
codesign -s "$APP_KEY" -f --entitlements "$PARENT_PLIST" "$APP_PATH"
productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_KEY" "$RESULT_PATH"
================================================
FILE: script/release.js
================================================
#!/usr/bin/env node
const childProcess = require('child_process')
const fs = require('fs')
const path = require('path')
const octokit = require('@octokit/rest')
const token = process.env.ELECTRON_API_DEMO_GITHUB_TOKEN
const version = require('../package').version
const github = octokit({
timeout: 30 * 1000,
'user-agent': `node/${process.versions.node}`
})
if (!token) {
console.error('ELECTRON_API_DEMO_GITHUB_TOKEN environment variable not set\nSet it to a token with repo scope created from https://github.com/settings/tokens/new')
process.exit(1)
}
github.authenticate({
type: 'token',
token: token
})
async function doRelease () {
const release = await getOrCreateRelease()
const assets = await prepareAssets()
await uploadAssets(release, assets)
await publishRelease(release)
console.log('Done!')
}
doRelease().catch(err => {
console.error(err.message || err)
process.exit(1)
})
function prepareAssets () {
const outPath = path.join(__dirname, '..', 'out')
const zipAssets = [{
name: 'electron-api-demos-mac.zip',
path: path.join(outPath, 'Electron API Demos-darwin-x64', 'Electron API Demos.app')
}, {
name: 'electron-api-demos-windows.zip',
path: path.join(outPath, 'Electron API Demos-win32-ia32')
}, {
name: 'electron-api-demos-linux.zip',
path: path.join(outPath, 'Electron API Demos-linux-x64')
}]
return Promise.all(zipAssets.map(zipAsset)).then((zipAssets) => {
return zipAssets.concat([{
name: 'RELEASES',
path: path.join(outPath, 'windows-installer', 'RELEASES')
}, {
name: 'ElectronAPIDemosSetup.exe',
path: path.join(outPath, 'windows-installer', 'ElectronAPIDemosSetup.exe')
}, {
name: `electron-api-demos-${version}-full.nupkg`,
path: path.join(outPath, 'windows-installer', `electron-api-demos-${version}-full.nupkg`)
}])
})
}
function zipAsset (asset) {
return new Promise((resolve, reject) => {
const assetBase = path.basename(asset.path)
const assetDirectory = path.dirname(asset.path)
console.log(`Zipping ${assetBase} to ${asset.name}`)
if (!fs.existsSync(asset.path)) {
return reject(new Error(`${asset.path} does not exist`))
}
const zipCommand = `zip --recurse-paths --symlinks '${asset.name}' '${assetBase}'`
const options = {cwd: assetDirectory, maxBuffer: Infinity}
childProcess.exec(zipCommand, options, (error) => {
if (error) {
reject(error)
} else {
asset.path = path.join(assetDirectory, asset.name)
resolve(asset)
}
})
})
}
async function getOrCreateRelease () {
const { data: releases } = await github.repos.listReleases({
owner: 'electron',
repo: 'electron-api-demos',
per_page: 100,
page: 1
})
const existingRelease = releases.find(release => release.tag_name === `v${version}` && release.draft === true)
if (existingRelease) {
console.log(`Using existing draft release for v${version}`)
return existingRelease
}
console.log('Creating new draft release')
const { data: release } = await github.repos.createRelease({
owner: 'electron',
repo: 'electron-api-demos',
tag_name: `v${version}`,
target_commitish: 'master',
name: version,
body: 'An awesome new release :tada:',
draft: true,
prerelease: false
})
return release
}
async function uploadAssets (release, assets) {
for (const asset of assets) {
if (release.assets.some(ghAsset => ghAsset.name === asset.name)) {
console.log(`Skipping already uploaded asset ${asset.name}`)
} else {
process.stdout.write(`Uploading ${asset.name}... `)
try {
await uploadAsset(release, asset)
} catch (err) {
if (err.name === 'HttpError' && err.message.startsWith('network timeout')) {
console.error('\n')
console.error(` There was a network timeout while uploading ${asset.name}.`)
console.error(' This likely resulted in a bad asset; please visit the release at')
console.error(` ${release.html_url} and manually remove the bad asset,`)
console.error(` then run this script again to continue where you left off.`)
console.error('')
process.exit(2)
} else {
throw err
}
}
process.stdout.write('Success!\n')
// [mkt] Waiting a bit between uploads seems to increase success rate
await new Promise(resolve => setTimeout(resolve, 5000))
}
}
}
function uploadAsset (release, asset) {
return github.repos.uploadReleaseAsset({
headers: {
'content-type': 'application/octet-stream',
'content-length': fs.statSync(asset.path).size
},
url: release.upload_url,
file: fs.createReadStream(asset.path),
name: asset.name
})
}
function publishRelease (release) {
console.log('Publishing release')
return github.repos.updateRelease({
owner: 'electron',
repo: 'electron-api-demos',
release_id: release.id,
draft: false
})
}
================================================
FILE: script/windows-store.js
================================================
const ChildProcess = require('child_process')
const path = require('path')
const metadata = require('../package')
const command = path.join(__dirname, '..', 'node_modules', '.bin', 'electron-windows-store.cmd')
const args = [
'--input-directory',
path.join(__dirname, '..', 'out', 'ElectronAPIDemos-win32-ia32'),
'--output-directory',
path.join(__dirname, '..', 'out', 'windows-store'),
'--flatten',
true,
'--package-version',
metadata.version + '.0',
'--package-name',
metadata.name,
'--package-display-name',
metadata.productName,
'--assets',
path.join(__dirname, '..', 'assets', 'tiles'),
'--package-description',
metadata.description
]
const windowsStore = ChildProcess.spawn(command, args, {stdio: 'inherit'})
windowsStore.on('close', (code) => {
process.exit(code)
})
================================================
FILE: sections/about.html
================================================
To illustrate this we've made use of const, for unchanging declarations; let for scoped declarations; and template strings like: `Result is ${output}` in the demo snippets.
The ipc (inter-process communication) module allows you to send and receive synchronous and asynchronous messages between the main and renderer processes.
There is a version of this module available for both processes: ipcMain and ipcRenderer.
Using ipc to send messages between processes asynchronously is the preferred method since it will return when finished without blocking other operations in the same process.
This example sends a "ping" from this process (renderer) to the main process. The main process then replies with "pong".
Renderer Process
Main Process
You can use the ipc module to send synchronous messages between processes as well, but note that the synchronous nature of this method means that it will block other operations while completing its task.
This example sends a synchronous message, "ping", from this process (renderer) to the main process. The main process then replies with "pong".
Renderer Process
Main Process
It is common practice to create a new invisible browser window (renderer process) in order to run tasks without impacting performance in the main app's window.
In this example we use the remote module to create a new invisible browser window from this renderer process. When the new page is loaded we send a message with ipc that the new window is listening for.
The new window then computes the factorial and sends the result to be received by this, the original, window and added to the page above.
The desktopCapturer module in Electron can be used to access any media, such as audio, video, screen and window, that is available through the getUserMedia web API in Chromium.
The Menu and MenuItem modules allow you to customize your application menu. If you don't set any menu, Electron will generate a minimal menu for your app by default.
This app uses the code below to set the application menu. If you click the 'View' option in the application menu and then the 'App Menu Demo', you'll see an information box displayed.
Main Process
ProTip
Know operating system menu differences.
When designing an app for multiple operating systems it's important to be mindful of the ways application menu conventions differ on each operating system.
For instance, on Windows, accelerators are set with an &. Naming conventions also vary, like between "Settings" or "Preferences". Below are resources for learning operating system specific standards.
A context, or right-click, menu can be created with the Menu and MenuItem modules as well. You can right-click anywhere in this app or click the demo button to see an example context menu.
In this demo we use the ipcRenderer module to show the context menu when explicitly calling it from the renderer process.
The globalShortcut and Menu modules can be used to define keyboard shortcuts.
In Electron, keyboard shortcuts are called accelerators.
They can be assigned to actions in your application's Menu,
or they can be assigned globally so they'll be triggered even when
your app doesn't have keyboard focus.
To try this demo, press CommandOrControl+Alt+K on your keyboard.
Global shortcuts are detected even when the app doesn't have
keyboard focus, and they must be registered after the app's
`ready` event is emitted.
Main Process
ProTip
Avoid overriding system-wide keyboard shortcuts.
When registering global shortcuts, it's important to be aware of
existing defaults in the target operating system, so as not to
override any existing behaviors. For an overview of each
operating system's keyboard shortcuts, view these documents:
The dialog module in Electron allows you to use native system dialogs for opening files or directories, saving a file or displaying informational messages.
This is a main process module because this process is more efficient with native utilities and it allows the call to happen without interrupting the visible elements in your page's renderer process.
In this demo, the ipc module is used to send a message from the renderer process instructing the main process to launch the open file (or directory) dialog. If a file is selected, the main process can send that information back to the renderer process.
Renderer Process
Main Process
ProTip
The sheet-style dialog on macOS.
On macOS you can choose between a "sheet" dialog or a default dialog. The sheet version descends from the top of the window. To use sheet version, pass the window as the first argument in the dialog method.
In this demo, the ipc module is used to send a message from the renderer process instructing the main process to launch the error dialog.
You can use an error dialog before the app's ready event, which is useful for showing errors upon startup.
Renderer Process
Main Process
In this demo, the ipc module is used to send a message from the renderer process instructing the main process to launch the information dialog. Options may be provided for responses which can then be relayed back to the renderer process.
Note: The title property is not displayed in macOS.
An information dialog can contain an icon, your choice of buttons, title and message.
Renderer Process
Main Process
In this demo, the ipc module is used to send a message from the renderer process instructing the main process to launch the save dialog. It returns the path selected by the user which can be relayed back to the renderer process.
This demonstrates using the shell module to open the system file manager at a particular location.
Clicking the demo button will open your file manager at the root.
Renderer Process
If you do not want your app to open website links within the app, you can use the shell module to open them externally. When clicked, the links will open outside of your app and in the user's default web browser.
When the demo button is clicked, the electron website will open in your browser.
Renderer Process
ProTip
Open all outbound links externally.
You may want to open all http and https links outside of your app. To do this, query the document and loop through each link and add a listener. This app uses the code below which is located in assets/ex-links.js.
The notification module in Electron allows you to add basic desktop notifications.
Electron conveniently allows developers to send notifications with the HTML5 Notification API, using the currently running operating system’s native notification APIs to display it.
Note: Since this is an HTML5 API it is only available in the renderer process.
The demo button sends a message to the main process using the ipc module. In the main process the app is told to place an icon, with a context menu, in the tray.
In this example the tray icon can be removed by clicking 'Remove' in the context menu or selecting the demo button again.
Main Process
Renderer Process
ProTip
Tray support in Linux.
On Linux distributions that only have app indicator support, users will need to install libappindicator1 to make the tray icon work. See the full API documentation(opens in new window) for more details about using Tray on Linux.
With a few Node.js and Electron modules you can gather information about the user's system, app or screen.
Links to the relevant documentation are in the demos below.
The main process app module can be used to get the path at which your app is located on the user's computer.
In this example, to get that information from the renderer process, we use the ipc module to send a message to the main process requesting the app's path.
The process module is built into Node.js (therefore you can use this in both the main and renderer processes) and in Electron apps this object has a few more useful properties on it.
The example below gets the version of Electron in use by the app.
Electron also includes versions of Chromium, Node.js and V8 within the process.versions object. You can get there quickly by opening up developer tools in an Electron app and typing process.versions.
// Returns version of Chromium in use
process.versions.chrome
// Returns version of V8 in use
process.versions.v8
// Returns version of Node in use
process.versions.node
The Node.js os module provides useful information about the user's operating system. It's built into Node.js and can be used in both the main and renderer proesses.
In the example below we require the module and then return the location of the home directory.
The Electron screen module retrieves information about screen size, displays, cursor position, etc. In the example below we retrieve the dimensions of the monitor in use.
The .size method in the example returns the raw dimensions of the screen but because of system menu bars this may not be the actual space available for your app.
To get the dimensions of the available screen space use the .workAreaSize method. Using .workArea will return the coordinates as well as the dimensions of the available screen space.
In this example we copy a phrase to the clipboard. After clicking 'Copy' use the text area to paste (CMD + V or CTRL + V) the phrase from the clipboard.
Renderer Process
In this example we copy a string to the clipboard and then paste the results into a message above.
The app module provides methods for handling protocols.
These methods allow you to set and unset the protocols your app should be the default app for. Similar to when a browser asks to be your default for viewing web pages.
You can set your app as the default app to open for a specific protocol. For instance, in this demo we set this app as the default for electron-api-demos://. The demo button above will launch a page in your default browser with a link. Click that link and it will re-launch this app.
Packaging
This feature will only work on macOS when your app is packaged. It will not work when you're launching it in development from the command-line. When you package your app you'll need to make sure the macOS plist for the app is updated to include the new protocol handler. If you're using electron-packager then you can add the flag --extend-info with a path to the plist you've created. The one for this app is below.
The BrowserWindow module will emit events when the renderer process crashes or hangs. You can listen for these events and give users the chance to reload, wait or close that window.
In this demo we create a new window (via the remote module) and provide a link that will force a crash using process.crash().
The window is listening for the crash event and when the event occurs it prompts the user with two options: reload or close.
Renderer Process
In this demo we create a new window (via the remote module) and provide a link that will force the process to hang using process.hang().
The window is listening for the process to become officially unresponsive (this can take up to 30 seconds). When this event occurs it prompts the user with two options: reload or close.
Renderer Process
ProTip
Wait for the process to become responsive again.
A third option in the case of a process that is hanging is to wait and see if the problem resolves, allowing the process to become responsive again. To do this, use the BrowserWindow event 'responsive' as shown below.
win.on('responsive', function () {
// Do something when the window is responsive again
})
Resize or move the window. See the size and position in the main window.
Close this Window
================================================
FILE: sections/windows/modal-toggle-visibility.html
================================================
Click on the parent window to see how the "focus on demo" button appears.
Close this Window
================================================
FILE: sections/windows/modal.html
================================================
Hello World!
Close this Window
================================================
FILE: sections/windows/process-crash.html
================================================
Click the text below to crash and then reload this process.
Crash this process
================================================
FILE: sections/windows/process-hang.html
================================================
Click the text below to hang and then reload this process.
(This will take up to 30 seconds.)
Hang this process
================================================
FILE: sections/windows/windows.html
================================================
Create and Manage Windows
The BrowserWindow module in Electron allows you to create a new browser window or manage an existing one.
Each browser window is a separate process, known as the renderer process. This process, like the main process that controls the life cycle of the app, has full access to the Node.js APIs.
The BrowserWindow module gives you the ability to create new windows in your app. This main process module can be used from the renderer process with the remote module, as is shown in this demo.
There are a lot of options when creating a new window. A few are in this demo, but visit the documentation(opens in new window) for the full list.
Renderer Process
ProTip
Use an invisible browser window to run background tasks.
You can set a new browser window to not be shown (be invisible) in order to use that additional renderer process as a kind of new thread in which to run JavaScript in the background of your app. You do this by setting the show property to false when defining the new window.
var win = new BrowserWindow({
width: 400, height: 225, show: false
})
In this demo we create a new window and listen for move and resize events on it. Click the demo button, change the new window and see the dimensions and position update here, above.
There are a lot of methods for controlling the state of the window such as the size, location, and focus status as well as events to listen to for window changes. Visit the documentation(opens in new window) for the full list.
Renderer Process
In this demo, we create a new window and listen for blur event on it. Click the demo button to create a new modal window, and switch focus back to the parent window by clicking on it. You can click the Focus on Demo button to switch focus to the modal window again.
Renderer Process
A frameless window is a window that has no "chrome",
such as toolbars, title bars, status bars, borders, etc. You can make a browser window frameless by setting
frame to false when creating the window.
Renderer Process
Windows can have a transparent background, too. By setting the transparent option to true, you can also make your frameless window transparent:
var win = new BrowserWindow({
transparent: true,
frame: false
})