├── .gitignore ├── README.md ├── assets ├── electron-virtual-keyboard-demo.gif ├── electron-virtual-keyboard-us-en-mobile-with-numpad.gif ├── electron-virtual-keyboard-us-en-mobile.gif ├── electron-virtual-keyboard-us-en-with-numpad.gif └── electron-virtual-keyboard-us-en.gif ├── client.js ├── demo ├── demo.html └── demo.js ├── index.js ├── package.json ├── src ├── client.js └── main.js ├── virtual-keyboard.css └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # vuepress build output 64 | .vuepress/dist 65 | 66 | # Serverless directories 67 | .serverless -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron Virtual Keyboard 2 | 3 | 4 | 5 | A themable JQuery virtual keyboard built to use Electron's webContent.sendInputEvent() api which minimizes input field event fighting with other libraries that might require modifying the input fields on the fly. 6 | 7 | This project takes inspiration from https://github.com/Mottie/Keyboard 8 | 9 | # Installation 10 | 11 | Through npm 12 | 13 | ```bash 14 | npm install electron-virtual-keyboard 15 | ``` 16 | Through yarn 17 | 18 | ```bash 19 | yarn add electron-virtual-keyboard 20 | ``` 21 | 22 | # Run the demo 23 | 24 | Through npm: 25 | ```bash 26 | npm run demo 27 | ``` 28 | 29 | or through yarn: 30 | ```bash 31 | yarn demo 32 | ``` 33 | 34 | # Usage 35 | 36 | The keyboard requires passing keys to the main process to mimic key input events. Therefore, you must set your main process to handle these requests 37 | 38 | ## Main Process 39 | 40 | Somewhere in you main electron process after you have created your window, pass the webContent object to the VirtualKeyboard class 41 | 42 | ```javascript 43 | const VirtualKeyboard = require('electron-virtual-keyboard'); 44 | 45 | let vkb; // keep virtual keyboard reference around to reuse. 46 | function createWindow() { 47 | /* Your setup code here */ 48 | 49 | vkb = new VirtualKeyboard(window.webContents); 50 | } 51 | 52 | ``` 53 | 54 | ## Render Process 55 | 56 | Then on your renderer process you can setup any supported element to use the virtual keyboard as follows: 57 | 58 | ```html 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | ``` 78 | 79 | 80 | 81 | # API 82 | 83 | The api entry point: 84 | 85 | ```javascript 86 | var keyboard = $('input:text').keyboard(); 87 | ``` 88 | 89 | You can pass an object to further customize the keyboard behaviour. See the next section. 90 | 91 | The keyboard plugin returns a VirtualKeyboard instance which you can use to trigger your own behaviours on the keyboard. Including sending key press events. 92 | 93 | ## Configuration 94 | 95 | ```javascript 96 | var keyboard = $('input:text').keyboard({ 97 | // Your config object // 98 | }); 99 | ``` 100 | 101 | | key | default | type | description | 102 | |:------:|:-------:|:------:|:------------| 103 | | theme | null | string | A theme class to apply to a keyboard. Available themes are "theme-black", "theme-mac" | 104 | | layout | "us-en"| string | The predefined layout id to use | 105 | | container | null | DomElement, JQueryElement or function($el) | A container to embed the virtual keyboard | 106 | | show | false | bool | When true, displays keyboard after setup | 107 | | displayOnFocus | true | bool | When true, auto displays/hides keyboard when the input field is in or out of focus. | 108 | | autoPosition | true | bool or function($el, $kb) | When true, snaps the keyboard below the input field. If a function is passed, this function will be called to calculate the snap position instead. | 109 | | keyTemplate | `````` | string | The default keyboard key container template to use. | 110 | | customKeys | null | Object | An object defining custom keys to use in your keyouts or redefine existing ones | 111 | 112 | ## show() 113 | 114 | Displays the keyboard 115 | 116 | ## hide() 117 | 118 | Hides the keyboard 119 | 120 | ## toggleLayout() 121 | 122 | Displays the next layout state 123 | 124 | ## showLayout(name) 125 | 126 | | arg | type | description | 127 | |:---:|:----:|:------------| 128 | | name | string | The name identifier of the state to display | 129 | 130 | Displays a layout state by name 131 | 132 | ## keyPress(key) 133 | 134 | | arg | type | description | 135 | |:---:|:----:|:------------| 136 | | key | string | The group of character keys to simulate | 137 | 138 | Sends a keypress to the electron main process to simulate a key input event. 139 | 140 | # Customizations 141 | 142 | ## Custom Keys 143 | 144 | There are two ways to add custom keys: 145 | 146 | 1) By adding a new key/value entry in ```$.fn.keyboard_custom_keys``` 147 | 2) by adding a custom_keys object to the keyboard config setup. 148 | 149 | For either option the setup is identical: 150 | 151 | ```javascript 152 | $.fn.keyboard_custom_keys['^mykey$'] = { 153 | render: function(kb, $key, modifier) { 154 | // You can override the key dom element to display anything you 155 | // want on the key. On this case, we just replace the key text. 156 | $key.text('Special Key'); 157 | }, 158 | handler: function(kb, $key) { 159 | // This key simply switche the keyboard keyout to a custom one 160 | // called 'special'. 161 | kb.showLayout('special'); 162 | } 163 | } 164 | ``` 165 | 166 | Custom keys are thus tied to keyboard layouts. Notice that the keys on ```$.fn.keyboard_custom_keys``` are regular expression patterns. 167 | 168 | ## Keyboard Layouts 169 | 170 | There are 4 built in keyboard layouts to use, plus you can setup your own custom layouts. 171 | 172 | ### us-en 173 | 174 | 175 | ### us-en:with-numpad 176 | 177 | 178 | ### us-en:mobile 179 | 180 | 181 | ### us-en:mobile-with-numpad 182 | 183 | 184 | ### Custom Layouts 185 | 186 | Defining layouts is straight forward, see the following example: 187 | 188 | Below is a copy/paste of the us-en keyboard layout defined as a one use layout: 189 | ```javascript 190 | var keyboard = $('input:text').keyboard({ 191 | layout: { 192 | 'normal': [ 193 | '` 1 2 3 4 5 6 7 8 9 0 - = {backspace:*}', 194 | ['{tab} q w e r t y u i o p [ ] \\', '7 8 9'], 195 | ['{sp:2} a s d f g h j k l ; \' {enter}', '4 5 6'], 196 | ['{shift:*} z x c v b n m , . / {shift:*}', '1 2 3'], 197 | ['{space}', '0'] 198 | ], 199 | 'shift': [ 200 | '~ ! @ # $ % ^ & * ( ) _ + {backspace:*}', 201 | ['{tab} Q W E R T Y U I O P { } |', '7 8 9'], 202 | ['{sp:2} A S D F G H J K L : " {enter}', '4 5 6'], 203 | ['{shift:*} Z X C V B N M < > ? {shift:*}', '1 2 3'], 204 | ['{space}', '0'] 205 | } 206 | }) 207 | ``` 208 | 209 | You can also define reusable layouts this way: 210 | ```javascript 211 | $.fn.keyboard_layouts['en-us:with-numpad'] = { 212 | 'normal': [ 213 | '` 1 2 3 4 5 6 7 8 9 0 - = {backspace:*}', 214 | ['{tab} q w e r t y u i o p [ ] \\', '7 8 9'], 215 | ['{sp:2} a s d f g h j k l ; \' {enter}', '4 5 6'], 216 | ['{shift:*} z x c v b n m , . / {shift:*}', '1 2 3'], 217 | ['{space}', '0'] 218 | ], 219 | 'shift': [ 220 | '~ ! @ # $ % ^ & * ( ) _ + {backspace:*}', 221 | ['{tab} Q W E R T Y U I O P { } |', '7 8 9'], 222 | ['{sp:2} A S D F G H J K L : " {enter}', '4 5 6'], 223 | ['{shift:*} Z X C V B N M < > ? {shift:*}', '1 2 3'], 224 | ['{space}', '0'] 225 | } 226 | 227 | var keyboard = $('input:text') 228 | .keyboard({ layout: 'en-us:with-numpad'}); 229 | ``` 230 | 231 | Here is how layouts work: 232 | 233 | 1) A layout object can contain multiple key/value pairs to define keyboard layouts used to swap display states. 234 | 2) Layout objects require at least one layout key "normal" which is the default layout displayed. 235 | 3) Custom key behaviours can be setup with squigly identifiers {custom-key} 236 | 4) Each key row may be a string or an array of strings. If using the array version, the keyboard turn them into columns to group keys horizontally. 237 | 5) Custom keys can be defined in ```$.fn.keyboard_custom_keys``` -------------------------------------------------------------------------------- /assets/electron-virtual-keyboard-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiThinkIT/electron-virtual-keyboard/524d160ea699744aa41bff157acb1acade5be725/assets/electron-virtual-keyboard-demo.gif -------------------------------------------------------------------------------- /assets/electron-virtual-keyboard-us-en-mobile-with-numpad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiThinkIT/electron-virtual-keyboard/524d160ea699744aa41bff157acb1acade5be725/assets/electron-virtual-keyboard-us-en-mobile-with-numpad.gif -------------------------------------------------------------------------------- /assets/electron-virtual-keyboard-us-en-mobile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiThinkIT/electron-virtual-keyboard/524d160ea699744aa41bff157acb1acade5be725/assets/electron-virtual-keyboard-us-en-mobile.gif -------------------------------------------------------------------------------- /assets/electron-virtual-keyboard-us-en-with-numpad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiThinkIT/electron-virtual-keyboard/524d160ea699744aa41bff157acb1acade5be725/assets/electron-virtual-keyboard-us-en-with-numpad.gif -------------------------------------------------------------------------------- /assets/electron-virtual-keyboard-us-en.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiThinkIT/electron-virtual-keyboard/524d160ea699744aa41bff157acb1acade5be725/assets/electron-virtual-keyboard-us-en.gif -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/client.js') -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |

Input:text test

20 | 21 | 26 |
27 |
28 |
29 |

Input:text test wide keyboard

30 | 31 | 36 |
37 |
38 |
39 |

Input:text test mobile keyboard

40 | 41 | 46 |
47 |
48 |
49 |

Input:password test

50 | 51 | 56 |
57 |
58 |
59 |

TextArea test

60 | 61 | 66 |
67 |
68 | 116 | 117 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const app = electron.app 3 | const BrowserWindow = electron.BrowserWindow 4 | const path = require('path') 5 | const url = require('url') 6 | const VirtualKeyboard = require('../index'); 7 | 8 | app.commandLine.appendSwitch('ignore-gpu-blacklist') 9 | 10 | let mainWindow 11 | let vkb 12 | 13 | function createWindow() { 14 | mainWindow = new BrowserWindow({ 15 | width: 800, 16 | height: 600, 17 | show: false, 18 | backgroundColor: '#000000', 19 | }) 20 | 21 | mainWindow.loadURL(url.format({ 22 | pathname: path.join(__dirname, 'demo.html'), 23 | protocol: 'file:', 24 | slashes: true 25 | })) 26 | mainWindow.webContents.setFrameRate(30) 27 | 28 | mainWindow.show() 29 | mainWindow.maximize() 30 | mainWindow.webContents.openDevTools() 31 | mainWindow.on('closed', function () { 32 | mainWindow = null 33 | }) 34 | 35 | vkb = new VirtualKeyboard(mainWindow.webContents) 36 | } 37 | 38 | app.on('ready', createWindow) 39 | app.on('window-all-closed', function () { 40 | if (process.platform !== 'darwin') { 41 | app.quit() 42 | } 43 | }) 44 | app.on('activate', function () { 45 | if (mainWindow === null) { 46 | createWindow() 47 | } 48 | }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/main') -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-virtual-keyboard", 3 | "version": "1.0.7", 4 | "description": "An electron based virtual keyboard. Uses electron's sendInputEvent api to implement a simple customizable soft keyboard.", 5 | "main": "index.js", 6 | "author": "forellana@digithinkit.com", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/DigiThinkIT/electron-virtual-keyboard.git" 11 | }, 12 | "scripts": { 13 | "demo": "electron ./demo/demo.js" 14 | }, 15 | "devDependencies": { 16 | "electron": "^2.0.2" 17 | }, 18 | "dependencies": { 19 | "jquery": "^3.3.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['jquery'], factory); 5 | } else if (typeof module === 'object' && module.exports) { 6 | // Node/CommonJS 7 | module.exports = function (root, jQuery) { 8 | if (jQuery === undefined) { 9 | // require('jQuery') returns a factory that requires window to 10 | // build a jQuery instance, we normalize how we use modules 11 | // that require this pattern but the window provided is a noop 12 | // if it's defined (how jquery works) 13 | if (typeof window !== 'undefined') { 14 | jQuery = require('jquery'); 15 | } 16 | else { 17 | jQuery = require('jquery')(root); 18 | } 19 | } 20 | factory(jQuery); 21 | return jQuery; 22 | }; 23 | } else { 24 | // Browser globals 25 | factory(jQuery); 26 | } 27 | }(function ($) { 28 | const ipcRenderer = require('electron').ipcRenderer; 29 | const EventEmitter = require('events'); 30 | 31 | /** 32 | * A wrapper over setTimeout to ease clearing and early trigger of the function. 33 | * @param {function} fn 34 | * @param {int} timeout 35 | * @returns {object} Returns an object { clear: , trigger: } 36 | */ 37 | function delayFn(fn, timeout) { 38 | var timeoutId = setTimeout(fn, timeout); 39 | return { 40 | clear: function() { 41 | clearTimeout(timeoutId); 42 | }, 43 | trigger: function() { 44 | clearTimeout(timeoutId); 45 | fn(); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * A wrapper over setInterval to ease clearing and early trigger of the function. 52 | * @param {function} fn 53 | * @param {int} interval 54 | * @returns {object} Returns an object { clear: , trigger: } 55 | */ 56 | function repeatFn(fn, interval) { 57 | var repeatId = setInterval(fn, interval); 58 | return { 59 | clear: function() { 60 | clearInterval(repeatId); 61 | }, 62 | trigger: function() { 63 | clearInterval(repeatId); 64 | fn(); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Allows calling fn first at one timeout then repeadeatly at a second interval. 71 | * Used, to mimic keyboard button held down effect. 72 | * @param {function} fn 73 | * @param {int} delay 74 | * @param {int} interval 75 | * @returns {object} Returns an object { clear: , trigger: } 76 | */ 77 | function delayThenRepeat(fn, delay, interval) { 78 | var secondInt = null 79 | var firstDelay = null; 80 | 81 | firstDelay = delayFn(() => { 82 | fn(); 83 | secondInt = repeatFn(fn, interval); 84 | firstDelay = null; 85 | }, delay); 86 | 87 | return { 88 | clear: function() { 89 | if ( firstDelay ) { 90 | firstDelay.clear(); 91 | } 92 | 93 | if ( secondInt ) { 94 | secondInt.clear(); 95 | } 96 | }, 97 | trigger: function() { 98 | if ( firstDelay ) { 99 | firstDelay.trigger(); 100 | firstDelay = null; 101 | } 102 | 103 | if (secondInt) { 104 | secondInt.clear(); 105 | secondInt = null; 106 | } 107 | } 108 | } 109 | 110 | } 111 | 112 | /** 113 | * Helper class dedicated to create a keyboard layout(single state) 114 | */ 115 | class KeyboardLayout extends EventEmitter { 116 | constructor($container, name, layout, config) { 117 | super(); 118 | 119 | this.layout = layout; 120 | this.$container = $container; 121 | this.name = name; 122 | this.config = config; 123 | this.init(); 124 | } 125 | 126 | init() { 127 | this.$layoutContainer = $('
'); 128 | this.$layoutContainer.addClass(this.name); 129 | this.$container.append(this.$layoutContainer); 130 | if ( this.name == 'normal' ) { 131 | this.$layoutContainer.addClass('active'); 132 | } 133 | 134 | // lets loop over layout once first to check if we have column layout 135 | // this is defined as an array of arrays. Each row containing more than one 136 | // string defines a new column 137 | var columnCount = 1; 138 | for (var i in this.layout) { 139 | var layout = this.layout[i]; 140 | if ( layout.constructor == Array ) { 141 | if ( columnCount < layout.length ) { 142 | columnCount = layout.length; 143 | } 144 | } 145 | } 146 | 147 | // build column containers 148 | for (var i=0; i < columnCount; i++) { 149 | this.$layoutContainer.append('
'); 150 | } 151 | 152 | // lets parse through layout lines and build keys 153 | for(var i in this.layout) { 154 | 155 | var layout = this.layout[i]; 156 | if ( layout.constructor != Array ) { 157 | layout = [layout]; 158 | } 159 | 160 | for(var col in layout) { 161 | var $row = $('
'); 162 | this.$layoutContainer.find('.kb-column').eq(col).append($row); 163 | var keys = layout[col].split(/\s+/m); 164 | for(var ki in keys) { 165 | var key = keys[ki]; 166 | if ( typeof ki != 'function' ) { 167 | var custom = null; 168 | var $key = $(this.config.keyTemplate); 169 | var text = key.length > 1?key.replace(/[\{\}]/gm, ''):key; 170 | var parts = (text==":")?[":"]:text.split(':'); 171 | var modifier = { mod: null, applied: [] }; 172 | if ( parts.length > 1 ) { 173 | text = parts[0]; 174 | modifier.mod = parts[1]; 175 | } 176 | $key.text(text); 177 | $row.append($key); 178 | // test modifiers 179 | if ($.fn.keyboard_custom_modifiers && modifier.mod) { 180 | for (var pattern in $.fn.keyboard_custom_modifiers) { 181 | var patternRx = new RegExp(pattern, 'ig'); 182 | 183 | if ( modifier.mod.search(patternRx) > -1 ) { 184 | $.fn.keyboard_custom_modifiers[pattern](this.keyboard, $key, modifier); 185 | } 186 | } 187 | } 188 | 189 | // test config.customKeys to apply customizations 190 | if ( this.config.customKeys ) { 191 | for(var pattern in this.config.customKeys) { 192 | var patternRx = new RegExp(pattern, 'ig'); 193 | 194 | if (text.search(patternRx) > -1 ) { 195 | custom = this.config.customKeys[pattern]; 196 | if (custom.render ) { 197 | custom.render(this.keyboard, $key, modifier); 198 | } 199 | } 200 | } 201 | } 202 | 203 | if (custom && custom.handler ) { 204 | $key.data('kb-key-handler', custom.handler); 205 | } 206 | $key.data('kb-key', text); 207 | } 208 | } 209 | } 210 | 211 | } 212 | } 213 | } 214 | 215 | /** 216 | * The Virtual Keyboard class holds all behaviour and rendering for our keyboard. 217 | */ 218 | class VirtualKeyboard extends EventEmitter { 219 | constructor($el, config) { 220 | super(); 221 | 222 | this.$el = $el; 223 | this.config = Object.assign({ 224 | individual: false, 225 | theme: null, 226 | show: false, 227 | displayOnFocus: true, 228 | container: null, 229 | autoPosition: true, 230 | layout: 'us-en', 231 | keyTemplate: '', 232 | customKeys: Object.assign({}, $.fn.keyboard_custom_keys) 233 | }, config); 234 | this.inited = false; 235 | 236 | // replace layout key for layout definition lookup on $.fn.keyboard_layouts 237 | if (typeof this.config.layout === 'string' || 238 | this.config.layout instanceof String) { 239 | this.config.layout = $.fn.keyboard_layouts[this.config.layout]; 240 | } 241 | this._onMouseDown = false; 242 | 243 | this.init(); 244 | } 245 | 246 | /** 247 | * Initializes our keyboard rendering and event handing. 248 | */ 249 | init() { 250 | if ( this.inited ) { 251 | console.warn("Keyboard already initialized..."); 252 | return; 253 | } 254 | var base = this; 255 | 256 | // build a defaut container if we don't get one from client 257 | // by default we'll just float under the input element 258 | // otherwise we let the client implement positioning 259 | if ( !this.config.container ) { 260 | this.$container = $('
'); 261 | $('body').append(this.$container); 262 | } else if ( typeof this.config.container == 'function' ) { 263 | this.$container = this.config.container(this.$el, this); 264 | this.$container.addClass('virtual-keyboard'); 265 | } 266 | 267 | if ( this.config.theme ) { 268 | this.$container.addClass(this.config.theme); 269 | } 270 | 271 | if ( this.config.show ) { 272 | this.$container.show(); 273 | } else { 274 | this.$container.hide(); 275 | } 276 | 277 | // hook up element focus events 278 | this.$el 279 | .focus(function(e) { 280 | if (base._onMouseDown) { 281 | return; 282 | } 283 | base.inputFocus(e.target); 284 | }) 285 | .blur(function(e) { 286 | if (base._onMouseDown ) { 287 | e.stopImmediatePropagation(); 288 | e.preventDefault(); 289 | return false; 290 | } 291 | 292 | base.inputUnFocus(e.target); 293 | }); 294 | 295 | // hook up mouse press down/up keyboard sims 296 | this.$container 297 | .on("mousedown touchstart", function(e) { 298 | if (!base._onMouseDown && $(e.target).data('kb-key') ) { 299 | base._onMouseDown = true; 300 | base.simKeyDown(e.target); 301 | 302 | e.stopImmediatePropagation(); 303 | return false; 304 | } 305 | }); 306 | $('body') 307 | .on("mouseup touchend", function(e) { 308 | if ( base._onMouseDown) { 309 | base._onMouseDown = false; 310 | base.simKeyUp(e.target); 311 | } 312 | }); 313 | 314 | // init layout renderer 315 | // break layouts into separate keyboards, we'll display them according to their 316 | // define behaviours later. 317 | this.layout = {} 318 | for(var k in this.config.layout) { 319 | if ( typeof this.config.layout[k] != 'function' ) { 320 | this.layout[k] = new KeyboardLayout(this.$container, k, this.config.layout[k], this.config); 321 | } 322 | } 323 | 324 | this.inited = true; 325 | } 326 | 327 | /** 328 | * Displays the next layout or wraps back to the first one in the layout list. 329 | */ 330 | toggleLayout() { 331 | var $next = this.$container.find('.layout.active').next(); 332 | if ( $next.length == 0 ) { 333 | $next = this.$container.find('.layout:first'); 334 | } 335 | 336 | this.$container 337 | .find('.layout') 338 | .removeClass('active'); 339 | 340 | $next.addClass('active'); 341 | } 342 | 343 | /** 344 | * Displays a layout by name 345 | * @param {string} name 346 | */ 347 | showLayout(name) { 348 | this.$container 349 | .find('.layout') 350 | .removeClass('active'); 351 | 352 | this.$container 353 | .find('.layout.'+name) 354 | .addClass('active'); 355 | } 356 | 357 | /** 358 | * Handles sending keyboard key press requests to the main electron process. 359 | * From there we'll simulate real keyboard key presses(as far as chromium is concerned) 360 | * @param {string} key 361 | */ 362 | pressKey(key) { 363 | ipcRenderer.send("virtual-keyboard-keypress", key); 364 | } 365 | 366 | /** 367 | * Handles displaying the keyboard for a certain input element 368 | * @param {DomElement} el 369 | */ 370 | show(el) { 371 | this.$container.show(); 372 | 373 | if ( this.config.autoPosition && typeof this.config.autoPosition != 'function' ) { 374 | var offset = $('body').offset(); 375 | // figure out bottom center position of the element 376 | var bounds = el.getBoundingClientRect(); 377 | var position = { 378 | x: bounds.left + offset.left, 379 | y: bounds.top + offset.top, 380 | width: bounds.width, 381 | height: bounds.height 382 | } 383 | 384 | var x = position.x + ((position.width - this.$container.width()) / 2);`` 385 | // keep container away from spilling outside window width 386 | if ((x + this.$container.width()) > $(window).width()) { 387 | x = $(window).width() - this.$container.width(); 388 | } 389 | // but also make sure we don't spil out to the left window edge either(priority) 390 | if ( x < 0 ) { 391 | x = 0; 392 | } 393 | this.$container.css({ 394 | position: 'absolute', 395 | top: position.y + position.height, 396 | left: x 397 | }); 398 | } else if (typeof this.config.autoPosition == 'function') { 399 | var position = this.config.autoPosition(el, this.$container); 400 | this.$container.css({ 401 | position: 'absolute', 402 | top: position.top, 403 | left: position.left 404 | }); 405 | } 406 | } 407 | 408 | /** 409 | * Handles hiding the keyboard. 410 | * @param {DomElement} el 411 | */ 412 | hide(el) { 413 | this.$container.hide(); 414 | } 415 | 416 | /** 417 | * Event handler for input focus event behaviour 418 | * @param {DomElement} el 419 | */ 420 | inputFocus(el) { 421 | 422 | // If we had an unfocus timeout function setup 423 | // and we are now focused back on an input, lets 424 | // cancel it and just move the keyboard into position. 425 | this.currentElement = el; 426 | if (this.unfocusTimeout) { 427 | this.unfocusTimeout.clear(); 428 | this.unfocusTimeout = null; 429 | } 430 | 431 | if (this.config.displayOnFocus) { 432 | this.show(el); 433 | } 434 | } 435 | 436 | /** 437 | * Event handler for input blur event behaviour 438 | * @param {DomElement} el 439 | */ 440 | inputUnFocus(el) { 441 | // setup a timeout to hide keyboard. 442 | // if the input was unfocused due to clicking on the keyboard, 443 | // we'll be able to cancel the delayed function. 444 | this.unfocusTimeout = delayFn(() => { 445 | if (this.config.displayOnFocus) { 446 | this.hide(el); 447 | } 448 | this.unfocusTimeout = null; 449 | }, 500); 450 | } 451 | 452 | simKeyDown(el) { 453 | // handle key clicks by letting them bubble to the parent container 454 | // from here we'll call our key presses for normal and custom keys 455 | // to mimic key held down effect we first trigger our key then wait 456 | // to call the same key on an interval. Mouse Up stops this loop. 457 | 458 | if (this.unfocusTimeout) { 459 | this.unfocusTimeout.clear(); 460 | this.unfocusTimeout = null; 461 | } 462 | 463 | // reset focus on next loop 464 | setTimeout(() => { 465 | $(this.currentElement).focus(); 466 | }, 1); 467 | 468 | // if we pressed on key, setup interval to mimic repeated key presses 469 | if ($(el).data('kb-key')) { 470 | this.keydown = delayThenRepeat(() => { 471 | //$(this.currentElement).focus(); 472 | var handler = $(el).data('kb-key-handler'); 473 | var key = $(el).data('kb-key'); 474 | if (handler) { 475 | key = handler(this, $(el)); 476 | } 477 | 478 | if ( key !== null && key !== undefined ) { 479 | this.pressKey(key); 480 | } 481 | }, 500, 100); 482 | } 483 | } 484 | 485 | simKeyUp(el) { 486 | // Mouse up stops key down effect. Since mousedown always presses the key at 487 | // least once, this event handler takes care of stoping the rest of the loop. 488 | 489 | if (this.keydown) { 490 | this.keydown.trigger(); 491 | this.keydown = null; 492 | } 493 | } 494 | } 495 | 496 | /** 497 | * Simple test for $.is() method to test compatible elements against. 498 | * @param {int} i 499 | * @param {DomElement} el 500 | */ 501 | function testSupportedElements(i, el) { 502 | return $(el).is('input:text') || $(el).is('input:password') || $(el).is('textarea'); 503 | } 504 | 505 | /** 506 | * Creates a virtual keyboard instance on the provided elements. 507 | * @param {object} config 508 | */ 509 | $.fn.keyboard = function (config) { 510 | 511 | var config = Object.assign({}, { 512 | individual: false 513 | }, config); 514 | 515 | if (!config && $(this).data('virtual-keyboard')) { 516 | return $(this).data('virtual-keyboard'); 517 | } 518 | 519 | $(this).each(function() { 520 | if ( !$(this).is(testSupportedElements) ) { 521 | throw Error("Virtual Keyboard does not support element of type: " + $(this).prop('name') ); 522 | } 523 | }); 524 | 525 | if ( !config.individual ) { 526 | var kb = new VirtualKeyboard($(this), config); 527 | $(this).data('virtual-keyboard', kb); 528 | 529 | return kb; 530 | } else { 531 | return $(this).each(function() { 532 | var kb = new VirtualKeyboard($(this), config); 533 | $(this).data('virtual-keyboard', kb); 534 | }); 535 | } 536 | }; 537 | 538 | $.fn.keyboard_custom_modifiers = { 539 | '(\\d+|\\*)(%|cm|em|ex|in|mm|pc|pt|px|vh|vw|vmin)?$': function(kb, $key, modifier) { 540 | var size = modifier.mod; 541 | if (size == '*') { 542 | $key.addClass('fill'); 543 | } else { 544 | if (size && size.search('[a-z]') == -1) { 545 | size += 'rem'; 546 | } 547 | $key.width(size); 548 | $key.addClass('sizer'); 549 | } 550 | 551 | modifier.applied.push('size'); 552 | } 553 | } 554 | 555 | $.fn.keyboard_custom_keys = { 556 | '^[`0-9~!@#$%^&*()_+\-=]$': { 557 | render: function(kb, $key) { 558 | $key.addClass('digit'); 559 | } 560 | }, 561 | '^enter$': { 562 | render: function(kb, $key) { 563 | $key.text('\u23ce ' + $key.text()); 564 | $key.addClass('action enter'); 565 | }, 566 | handler: function(kb, $key) { 567 | return '\r'; 568 | } 569 | }, 570 | '^shift$': { 571 | render: function(kb, $key) { 572 | $key.text('\u21e7 ' + $key.text()); 573 | $key.addClass('action shift'); 574 | }, 575 | handler: function(kb, $key) { 576 | kb.toggleLayout(); 577 | return null; 578 | } 579 | }, 580 | '^numeric$': { 581 | render: function (kb, $key) { 582 | $key.text('123'); 583 | }, 584 | handler: function(kb, $key) { 585 | kb.showLayout('numeric'); 586 | } 587 | }, 588 | '^abc$': { 589 | handler: function (kb, $key) { 590 | kb.showLayout('normal'); 591 | } 592 | }, 593 | '^symbols$': { 594 | render: function(kb, $key) { 595 | $key.text('#+='); 596 | }, 597 | handler: function (kb, $key) { 598 | kb.showLayout('symbols'); 599 | } 600 | }, 601 | '^caps$': { 602 | render: function (kb, $key) { 603 | $key.text('\u21e7'); 604 | $key.addClass('action shift'); 605 | }, 606 | handler: function (kb, $key) { 607 | kb.showLayout('shift'); 608 | return null; 609 | } 610 | }, 611 | '^lower$': { 612 | render: function (kb, $key) { 613 | $key.text('\u21e7'); 614 | $key.addClass('action shift'); 615 | }, 616 | handler: function (kb, $key) { 617 | kb.showLayout('normal'); 618 | return null; 619 | } 620 | }, 621 | '^space$': { 622 | render: function(kb, $key) { 623 | $key.addClass('space'); 624 | }, 625 | handler: function(kb, $key) { 626 | return ' '; 627 | } 628 | }, 629 | '^tab$': { 630 | render: function (kb, $key) { 631 | $key.addClass('action tab'); 632 | }, 633 | handler: function (kb, $key) { 634 | return '\t'; 635 | } 636 | }, 637 | '^backspace$': { 638 | render: function(kb, $key) { 639 | $key.text(' \u21e6 '); 640 | $key.addClass('action backspace'); 641 | }, 642 | handler: function(kb, $key) { 643 | return '\b'; 644 | } 645 | }, 646 | '^del(ete)?$': { 647 | render: function (kb, $key) { 648 | $key.addClass('action delete'); 649 | }, 650 | handler: function (kb, $key) { 651 | return String.fromCharCode(127); 652 | } 653 | }, 654 | '^sp$': { 655 | render: function(kb, $key, modifier) { 656 | $key.empty(); 657 | $key.addClass('spacer'); 658 | if ( modifier.applied.indexOf('size') < 0) { 659 | $key.addClass('fill'); 660 | } 661 | }, 662 | handler: function(kb, $key) { 663 | return null; 664 | } 665 | } 666 | } 667 | 668 | $.fn.keyboard_layouts = { 669 | 'us-en': { 670 | 'normal': [ 671 | '{`:*} 1 2 3 4 5 6 7 8 9 0 - = {backspace:*}', 672 | '{tab} q w e r t y u i o p [ ] \\', 673 | '{sp:2} a s d f g h j k l ; \' {enter}', 674 | '{shift:*} z x c v b n m , . / {shift:*}', 675 | '{space}' 676 | ], 677 | 'shift': [ 678 | '{~:*} ! @ # $ % ^ & * ( ) _ + {backspace:*}', 679 | '{tab} Q W E R T Y U I O P { } |', 680 | '{sp:2} A S D F G H J K L : " {enter}', 681 | '{shift:*} Z X C V B N M < > ? {shift:*}', 682 | '{space}' 683 | ] 684 | }, 685 | 'us-en:with-numpad': { 686 | 'normal': [ 687 | '` 1 2 3 4 5 6 7 8 9 0 - = {backspace:*}', 688 | ['{tab} q w e r t y u i o p [ ] \\', '7 8 9'], 689 | ['{sp:2} a s d f g h j k l ; \' {enter}', '4 5 6'], 690 | ['{shift:*} z x c v b n m , . / {shift:*}', '1 2 3'], 691 | ['{space}', '0'] 692 | ], 693 | 'shift': [ 694 | '~ ! @ # $ % ^ & * ( ) _ + {backspace:*}', 695 | ['{tab} Q W E R T Y U I O P { } |', '7 8 9'], 696 | ['{sp:2} A S D F G H J K L : " {enter}', '4 5 6'], 697 | ['{shift:*} Z X C V B N M < > ? {shift:*}', '1 2 3'], 698 | ['{space}', '0'] 699 | ] 700 | }, 701 | 'us-en:mobile': { 702 | 'normal': [ 703 | 'q w e r t y u i o p', 704 | 'a s d f g h j k l', 705 | '{caps:*} z x c v b n m {backspace:*}', 706 | '{numeric} , {space:*} . {enter}' 707 | ], 708 | 'shift': [ 709 | 'Q W E R T Y U I O P', 710 | 'A S D F G H J K L', 711 | '{lower:*} Z X C V B N M {backspace:*}', 712 | '{numeric} , {space:*} . {enter}' 713 | ], 714 | 'numeric': [ 715 | '1 2 3 4 5 6 7 8 9 0', 716 | '- / : ; ( ) $ & @ "', 717 | '{symbols:*} {sp} . , ? ! \' {sp} {backspace:*}', 718 | '{abc} , {space:*} . {enter}' 719 | ], 720 | 'symbols': [ 721 | '[ ] { } # % ^ * + =', 722 | '_ \ | ~ < >', 723 | '{numeric:*} {sp} . , ? ! \' {Sp} {backspace:*}', 724 | '{abc} , {space:*} . {enter}' 725 | ], 726 | }, 727 | 'us-en:mobile-with-numpad': { 728 | 'normal': [ 729 | ['q w e r t y u i o p', '7 8 9'], 730 | ['a s d f g h j k l', '4 5 6'], 731 | ['{caps:*} z x c v b n m {backspace:*}', '1 2 3'], 732 | ['{numeric} , {space:*} . {enter}', '0:2'] 733 | ], 734 | 'shift': [ 735 | ['Q W E R T Y U I O P', '& * ('], 736 | ['A S D F G H J K L', '$ % ^'], 737 | ['{lower:*} Z X C V B N M {backspace:*}', '! @ #'], 738 | ['{numeric} , {space:*} . {enter}', '):2'] 739 | ], 740 | 'numeric': [ 741 | ['* + = - / : ; $ & @', '7 8 9'], 742 | ['[ ] { } ( ) # % ^ "', '4 5 6'], 743 | ['{lower:*} _ \\ | ~ ? ! \' {backspace:*}', '1 2 3'], 744 | ['{abc} < {space:*} > {enter}', '0:2'] 745 | ] 746 | } 747 | }; 748 | 749 | })); 750 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const { ipcMain } = require('electron') 2 | const EventEmitter = require('events') 3 | 4 | class VirtualKeyboard extends EventEmitter { 5 | constructor(webContent) { 6 | super(); 7 | this.webContent = webContent; 8 | this.keyBuffer = []; 9 | this.keyPressWait = 30; 10 | this.init(); 11 | } 12 | 13 | init() { 14 | // renderer to main process message api handlers 15 | ipcMain.on('virtual-keyboard-keypress', (e, value) => this.receiveKeyPress(e, value)); 16 | ipcMain.on('virtual-keyboard-config', this.config.bind(this)); 17 | 18 | // redirect select events back to renderer process 19 | this.on('buffer-empty', () => { 20 | this.webContent.send('keyboard-buffer-empty') 21 | }) 22 | } 23 | 24 | config(e, key, value) { 25 | if ( key == 'keyPressWait' ) { 26 | this.keyPressWait = parseInt(value); 27 | } 28 | } 29 | 30 | receiveKeyPress(e, value) { 31 | // continues adding keys to the key buffer without stopping a flush 32 | var chars = String(value).split(''); 33 | for(var i=0; i