Electron Kiosk

Learn how I created an input interface for electronjs. It is useful for e.g. interactive exhibition setups.

7 min read

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.

electron app 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

electron app The finished interface

Credits

Scripts and Graphics by Leo Mühlfeld. Keyboard idea by Dcode. Using Google Material Icons.