├── .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