Electron Kiosk
Learn how I created an input interface for electronjs. It is useful for e.g. interactive exhibition setups.
The Goal
We are going to create a very simple Electron App displaying a keyboard and an input field. An application like this can be very useful for live installations like exhibitions, polls etc. As part of the exhibition for the project Credit Criminals, we attached the applications to a small invoice printer so that we can offer the visitor tailor-made souvenirs.
Simple Input Interface
Setup
First of all, this project is tailored to run on a RapsberryPi. But since its electron. It can of course also run on every plattform. After setting up your RaspberryPi with a GUI operating system, you’ll need to install node and npm. After installing node and npm, we will init our electron-kiosk
project.
$ npm init
$ cd electron-kiosk
Let’s install electron.
# globally
$ sudo npm install -g electron --unsafe-perm=true --allow-root
# dev dependency
npm install electron --save-dev
You want to set your entry point to src/main.js
. And add "start": "electron ."
to your scripts. Your package.json
should look something like this.
{
"name": "electron-kisok",
"version": "1.0.0",
"author": "leo-muehlfeld",
"license": "MIT",
"main": "src/main.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^9.1.0"
}
}
Create the files.
$ mkdir src
$ cd src
$ touch index.html main.js
$ mkdir keyboard
$ cd keyboard
$ touch keyboard.js keyboard.css
Electron App
We have created four files. Lets fill these files with the neccessary code. We will start with main.js
which is basically something like a launch instruction for electron.
const { app, BrowserWindow } = require('electron')
function createWindow () {
let win = new BrowserWindow({
// Size of app window
width: 1024,
height: 600,
// Kiosk "true" runs app on fullscreen without obstructions.
kiosk: true,
webPreferences: {
nodeIntegration: true
}
})
// Load file
win.loadFile('src/index.html')
// Line below enables DevTools
win.webContents.openDevTools()
}
app.on('ready', createWindow)
Okay, electron will now know what to do on startup. Lets continue by adding our html structure to the index.html.
We will import the Google Material Icons, create an input field, add some styles to it and write a little function that disables our user to deselect the input.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="keyboard/keyboard.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"rel="stylesheet">
<meta charset="UTF-8">
<title>Electron Kiosk</title>
<style media="screen">
body{
background-color: black;
}
.use-keyboard-input {
font-family: sans-serif;
background-color: white;
color: black;
font-size: 80px;
width: 80%;
padding: 20px 27px 12px;
border: 2px solid white;
box-sizing: border-box;
border-radius: 4px;
outline: none;
display: block;
margin: 70px auto 0;
-webkit-user-select: none;
}
</style>
</head>
<body>
<input
id="disable-select"
class="use-keyboard-input"
type="text"
placeholder="Your Name"
onblur="this.focus()"
autofocus
/>
<!-- Disable deselection of input -->
<script>
var inp = document.getElementById('disable-select');
inp.addEventListener('select', function() {
this.selectionStart = this.selectionEnd;
}, false);
</script>
</body>
</html>
Add the Keyboard
Neat! Basically, when we run this app on a machine with a keyboard, we are set. But we don’t want to achieve that here. The kiosk app should be able to run by itself with just a touchscreen as an input option. So we’re going to follow the dcode tutorial and create a custom keyboard using vanilla javascript and some CSS. Let’s fill the keyboard/keyboard.css
file with content so we don’t have to worry about that for the time being.
.keyboard{
position: fixed;
left:0;
bottom: 0;
width: 100%;
padding: 5px 0;
background-color: #e2e2e2;
user-select: none;
transition: bottom 0.4s;
}
.keyboard__keys{
text-align: center;
}
.keyboard__key {
font-family: sans-serif;
height: 70px;
width: 7%;
margin: 3px;
border-radius: 4px;
border: none;
background: white;
color: black;
font-size: 1.2rem;
outline: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: top;
padding: 0;
-webkit-tap-highlight-color: transparent;
position: relative;
}
.keyboard__key--wide{
width: 23%;
max-width: 120px;
}
.keyboard__key--extrawide{
width: 50%;
max-width: 500px;
}
.keyboard__key--activatable::after{
content: '';
top: 10px;
right: 10px;
position: absolute;
width: 8px;
height: 8px;
background: rgba(0,0,0,0.4);
border-radius: 50%;
}
.keyboard__key--active::after{
background: #08ff00;
}
.keyboard__key--dark{
background: black;
color: white;
}
.keyboard__key:active{
background-color: rgba(255, 255, 255, 0.12);
}
You can of course adjust the styles later to suit your needs. We are now going to create the actual keyboard elements. In a nutshell, we will create an array with all the keys needed in order to subsequently render them into individual buttons. For special keys such as caps-lock, spacebar, backspace and so on, we will assign special actions and properties with the help of a switch.
const Keyboard = {
elements: {
main: null,
keysContainer: null,
keys: []
},
eventHandlers: {
oninput: null,
ondone: null
},
properties: {
value: "",
capslock: false,
},
init() {
this.elements.main = document.createElement("div");
this.elements.keysContainer = document.createElement("div");
this.elements.main.classList.add("keyboard");
this.elements.keysContainer.classList.add("keyboard__keys");
this.elements.keysContainer.appendChild(this._createKeys());
this.elements.keys = this.elements.keysContainer.querySelectorAll(".keyboard__key");
this.elements.main.appendChild(this.elements.keysContainer);
document.body.appendChild(this.elements.main);
document.querySelectorAll(".use-keyboard-input").forEach(element => {
element.addEventListener("focus", () => {
this.open(element.value, currentValue => {
element.value = currentValue;
})
});
});
},
_createKeys(){
const fragment = document.createDocumentFragment();
// This array will determine your keyboard layout (e.g. qwerty, abcdef, etc.)
const keyLayout = [
"q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü", "backspace",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä",
"caps", "y", "x", "c", "v", "b", "n", "m", "done",
"space"
];
// Simplify icon implementation
const createIconHTML = (icon_name) => {
return `<i class="material-icons">${icon_name}</i>`;
};
// Create keys from array
keyLayout.forEach(key => {
const keyElement = document.createElement("button");
const insertLineBreak = ["backspace", "ä", "done"].indexOf(key) !== -1;
keyElement.setAttribute("type", "button");
keyElement.classList.add("keyboard__key");
// We will treat special keys with special properties via a switch
switch(key) {
case "backspace":
keyElement.classList.add("keyboard__key--wide");
keyElement.innerHTML = createIconHTML("backspace");
keyElement.addEventListener("click", () => {
this.properties.value = this.properties.value.substring(0, this.properties.value.length - 1);
this._triggerEvent("oninput");
});
break;
case "caps":
keyElement.classList.add("keyboard__key--wide", "keyboard__key--activatable");
keyElement.innerHTML = createIconHTML("keyboard_capslock");
keyElement.addEventListener("click", () => {
this.toggleCapsLock();
keyElement.classList.toggle("keyboard__key--active", this.properties.capsLock);
});
break;
case "enter":
keyElement.classList.add("keyboard__key--wide");
keyElement.innerHTML = createIconHTML("keyboard_return");
keyElement.addEventListener("click", () => {
this.properties.value += "\n";
this._triggerEvent("oninput");
});
break;
case "space":
keyElement.classList.add("keyboard__key--extrawide");
keyElement.innerHTML = createIconHTML("space_bar");
keyElement.addEventListener("click", () => {
this.properties.value += " ";
this._triggerEvent("oninput");
});
break;
case "done":
keyElement.classList.add("keyboard__key--wide", "keyboard__key--dark");
// Set an appropriate icon for your "done" action
keyElement.innerHTML = createIconHTML("print");
keyElement.addEventListener("click", () => {
this.close();
this._triggerEvent("onclose");
});
break;
default:
keyElement.textContent = key.toLowerCase();
keyElement.addEventListener("click", () => {
this.properties.value += this.properties.capsLock ? key.toUpperCase() : key.toLowerCase();
this._triggerEvent("oninput");
});
break;
}
fragment.appendChild(keyElement);
if (insertLineBreak) {
fragment.appendChild(document.createElement("br"));
}
});
return fragment;
},
_triggerEvent(handlerName) {
if (typeof this.eventHandlers[handlerName] == "function") {
this.eventHandlers[handlerName](this.properties.value);
}
},
toggleCapsLock(){
this.properties.capsLock = !this.properties.capsLock;
for (const key of this.elements.keys) {
if (key.childElementCount === 0) {
key.textContent = this.properties.capsLock ? key.textContent.toUpperCase() : key.textContent.toLowerCase();
}
}
},
open(initialValue, oninput, onclose) {
this.properties.value = initialValue || "";
this.eventHandlers.oninput = oninput;
this.eventHandlers.onclose = onclose;
this.elements.main.classList.remove("keyboard--hidden");
},
close() {
this.properties.value = "";
this.eventHandlers.oninput = oninput;
this.eventHandlers.onclose = onclose;
document.querySelectorAll(".use-keyboard-input").forEach(element => {
element.value = "";
});
}
};
// Load Keyboard when ready
window.addEventListener("DOMContentLoaded", function() {
Keyboard.init();
});
Almost done. As a last step, we will have to import the keyboard.js
script into our index.html. We will do this by adding the following line before the </body>
tag.
<script src="keyboard/keyboard.js"></script>
Done!
Lets run it.
# Only on RPI
export DISPLAY=:0
# start app
npm start
The finished interface
Credits
Scripts and Graphics by Leo Mühlfeld. Keyboard idea by Dcode. Using Google Material Icons.