Use virtual keyboard in node-red-dashboard

Where do you want to use it?
In the Node-RED editor?
Or within webpages you are creating in Node-RED?

Within webpages im creating. Its an offline system so everything must be self contained

Hi @stfndr! Sorry for not replying sooner, I totally missed that post. Basically the On/Off button doesn't open the keyboard, it merely enables or disables it. Clicking in the text fields while the Keyboard button is enabled (ON) will pop-up the virtual keyboard.

Hi @hugobox,
I like your keyboard flow !

For it to display, from the beginning, a French keyboard, I modified:

  • the layout of the azerty keys
  • the 3 places where there is English that I replaced with French on lines : 19, 125, 579

It works like that, but I wanted to know if I did well?

1 Like

Thanks @SuperNinja! If it works for you than you most likely did well! I'll look into making it easier to change the default layout!

It works fine for me :slight_smile: the only issue I have is I dont use the dashboard nodes how can i convert this to work on a webpage hosted on node red?

I thought I did not have to replace all English, but as you say, if it works it's cool.

@blackwellj You could try to implement it from the original author's code which uses jquery: https://github.com/javidan/jkeyboard

but that is permantly visible

Pretty sure its meant to pop-up whenever you give a text field focus and goes away when you're done.

Is there anyway to have this towards the bottom of the screen instead of directly in the middle?

Hi @eli99k !
Sorry for the late reply! You have to play with the CSS values. Here's the full code to have it at the bottom:

<script> 
    
// the semi-colon before function invocation is a safety net against concatenated
// scripts and/or other plugins which may not be closed properly.
; (function ($, window, document, undefined) {

    // undefined is used here as the undefined global variable in ECMAScript 3 is
    // mutable (ie. it can be changed by someone else). undefined isn't really being
    // passed in so we can ensure the value of it is truly undefined. In ES5, undefined
    // can no longer be modified.

    // window and document are passed through as local variable rather than global
    // as this (slightly) quickens the resolution process and can be more efficiently
    // minified (especially when both are regularly referenced in your plugin).

    // Create the defaults once
    var pluginName = "jkeyboard",
        defaults = {
            layout: "english",
            input: $('#input'),
            customLayouts: {
                selectable: []
            },
        };


    var function_keys = {
        backspace: {
            text: 'DEL',
        },
        return: {
            text: 'Enter'
        },
        shift: {
            text: 'Shift'
        },
        space: {
            text: 'Space'
        },
        numeric_switch: {
            text: '123',
            command: function () {
                this.createKeyboard('numeric');
                this.events();
            }
        },
        layout_switch: {
            text: '<i class="fa fa-keyboard-o" aria-hidden="true"></i>',
            command: function () {
                var l = this.toggleLayout();
                this.createKeyboard(l);
                this.events();
            }
        },
        character_switch: {
            text: 'ABC',
            command: function () {
                this.createKeyboard(layout);
                this.events();
            }
        },
        symbol_switch: {
            text: '#+=',
            command: function () {
                this.createKeyboard('symbolic');
                this.events();
            }
        }
    };


    var layouts = {
        selectable: ['azeri', 'english', 'russian','french', 'emoji'],
        azeri: [
            ['q', 'ü', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'ö', 'ğ'],
            ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ı', 'ə'],
            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'ç', 'ş', 'backspace'],
            ['numeric_switch', 'layout_switch', 'space', 'return']
        ],
        english: [
            ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',],
            ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l',],
            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],
            ['numeric_switch', 'layout_switch', 'space', 'return']
        ],
        russian: [
            ['й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х'],
            ['ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'],
            ['shift', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', 'backspace'],
            ['numeric_switch', 'layout_switch', 'space', 'return']
        ],
        french: [
            ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',],
            ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l','à','ç'],
            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm','é','è', 'backspace'],
            ['numeric_switch', 'layout_switch', 'space', 'return']
        ],
        emoji: [
            ['😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',],
            ['😋', '😎', '😍', '😘', 'g', 'h', 'j', 'k', 'l','à','ç'],
            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm','é','è', 'backspace'],
            ['numeric_switch', 'layout_switch', 'space', 'return']
        ],            
        numeric: [
            ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
            ['-', '/', ':', ';', '(', ')', '$', '&', '@', '"'],
            ['symbol_switch', '.', ',', '?', '!', "'", 'backspace'],
            ['character_switch', 'layout_switch', 'space', 'return'],
        ],
        numbers_only: [
            ['1', '2', '3',],
            ['4', '5', '6',],
            ['7', '8', '9',],
            ['0', 'return', 'backspace'],
        ],
        symbolic: [
            ['[', ']', '{', '}', '#', '%', '^', '*', '+', '='],
            ['_', '\\', '|', '~', '<', '>'],
            ['numeric_switch', '.', ',', '?', '!', "'", 'backspace'],
            ['character_switch', 'layout_switch', 'space', 'return'],

        ]
    }

    var shift = false, capslock = false, layout = 'english', layout_id = 0;

    // The actual plugin constructor
    function Plugin(element, options) {
        this.element = element;
        // jQuery has an extend method which merges the contents of two or
        // more objects, storing the result in the first object. The first object
        // is generally empty as we don't want to alter the default options for
        // future instances of the plugin
        this.settings = $.extend({}, defaults, options);
        // Extend & Merge the cusom layouts
        layouts = $.extend(true, {}, this.settings.customLayouts, layouts);
        if (Array.isArray(this.settings.customLayouts.selectable)) {
            $.merge(layouts.selectable, this.settings.customLayouts.selectable);
        }
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }

    Plugin.prototype = {
        init: function () {
            layout = this.settings.layout;
            this.createKeyboard(layout);
            this.events();
        },

        setInput: function (newInputField) {
            this.settings.input = newInputField;
        },

        createKeyboard: function (layout) {
            shift = false;
            capslock = false;

            var keyboard_container = $('<ul/>').addClass('jkeyboard'),
                me = this;

            layouts[layout].forEach(function (line, index) {
                var line_container = $('<li/>').addClass('jline');
                line_container.append(me.createLine(line));
                keyboard_container.append(line_container);
            });

            $(this.element).html('').append(keyboard_container);
        },

        createLine: function (line) {
            var line_container = $('<ul/>');

            line.forEach(function (key, index) {
                var key_container = $('<li/>').addClass('jkey').data('command', key);

                if (function_keys[key]) {
                    key_container.addClass(key).html(function_keys[key].text);
                }
                else {
                    key_container.addClass('letter').html(key);
                }

                line_container.append(key_container);
            })

            return line_container;
        },

        events: function () {
            var letters = $(this.element).find('.letter'),
                shift_key = $(this.element).find('.shift'),
                space_key = $(this.element).find('.space'),
                backspace_key = $(this.element).find('.backspace'),
                return_key = $(this.element).find('.return'),

                me = this,
                fkeys = Object.keys(function_keys).map(function (k) {
                    return '.' + k;
                }).join(',');

            letters.on('click', function () {
                me.type((shift || capslock) ? $(this).text().toUpperCase() : $(this).text());
            });

            space_key.on('click', function () {
                me.type(' ');
            });

            return_key.on('click', function () {
                me.type("\n");
                me.settings.input.parents('form').submit();
            });

            backspace_key.on('click', function () {
                me.backspace();
            });

            shift_key.on('click', function () {
                if (capslock) {
                    me.toggleShiftOff();
                    capslock = false;
                } else {
                    me.toggleShiftOn();
                }
            }).on('dblclick', function () {
                capslock = true;
            });


            $(fkeys).on('click', function () {
                var command = function_keys[$(this).data('command')].command;
                if (!command) return;

                command.call(me);
            });
        },

        type: function (key) {
            var input = this.settings.input,
                val = input.val(),
                input_node = input.get(0),
                start = input_node.selectionStart,
                end = input_node.selectionEnd;

            var max_length = $(input).attr("maxlength");
            if (start == end && end == val.length) {
                if (!max_length || val.length < max_length) {
                    input.val(val + key);
                    input.change()
                    $('#vkeyname').text(val + key)
                }
            } else {
                console.log(input_node)
                if (input_node.type != "number"){
                    var new_string = this.insertToString(start, end, val, key);
                    input.val(new_string);
                    start++;
                    end = start;
                    input_node.setSelectionRange(start, end);
                    input.change()
                    $('#vkeyname').text(val + key)
                }else{
                    console.log("Not supposed to go there as number types are changed to text type and back")
                    input.val(key + val);
                    input.change()
                }
                
            }
            input.trigger('focus');

            if (shift && !capslock) {
                this.toggleShiftOff();
            }
        },

        backspace: function () {
            var input = this.settings.input,
                val = input.val();
                input_node = input.get(0),
                start = input_node.selectionStart,
                end = input_node.selectionEnd;
            
            input.val(val.slice(0, start-1) + val.slice(start))
            input.change()
            $('#vkeyname').text(input.val())
            input.focus()
            input_node.setSelectionRange(start-1, start-1);
        },

        toggleShiftOn: function () {
            var letters = $(this.element).find('.letter'),
                shift_key = $(this.element).find('.shift');

            letters.addClass('uppercase');
            shift_key.addClass('active')
            shift = true;
        },

        toggleShiftOff: function () {
            var letters = $(this.element).find('.letter'),
                shift_key = $(this.element).find('.shift');

            letters.removeClass('uppercase');
            shift_key.removeClass('active');
            shift = false;
        },

        toggleLayout: function () {
            layout_id = layout_id || 0;
            var plain_layouts = layouts.selectable;
            layout_id++;

            var current_id = layout_id % plain_layouts.length;
            var SelectedLayoutName = plain_layouts[current_id];
            $('#vkeyname').text('V-Keyboard ' + SelectedLayoutName )
            return plain_layouts[current_id];
        },

        insertToString: function (start, end, string, insert_string) {
            return string.substring(0, start) + insert_string + string.substring(end, string.length);
        }
    };

        /*
		// A really lightweight plugin wrapper around the constructor,
		// preventing against multiple instantiations
		$.fn[ pluginName ] = function ( options ) {
				return this.each(function() {
						if ( !$.data( this, "plugin_" + pluginName ) ) {
								$.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
						}
				});
		};
        */
        var methods = {
            init: function(options) {
                if (!this.data("plugin_" + pluginName)) {
                    this.data("plugin_" + pluginName, new Plugin(this, options));
                }
            },
			setInput: function(content) {
				this.data("plugin_" + pluginName).setInput($(content));
            },
            setLayout: function(layoutname) {
                // change layout if it is not match current
                object = this.data("plugin_" + pluginName);
                if (typeof(layouts[layoutname]) !== 'undefined' && object.settings.layout != layoutname) {
                    object.settings.layout = layoutname;
                    object.createKeyboard(layoutname);
                    object.events();
                };
            },
        };

		$.fn[pluginName] = function (methodOrOptions) {
            if (methods[methodOrOptions]) {
                return methods[methodOrOptions].apply(this.first(), Array.prototype.slice.call( arguments, 1));
            } else if (typeof methodOrOptions === 'object' || ! methodOrOptions) {
                // Default to "init"
                return methods.init.apply(this.first(), arguments);
            } else {
                $.error('Method ' +  methodOrOptions + ' does not exist on jQuery.tooltip');
            }
        };

})(jQuery, window, document);
</script>
<style>
    .jkeyboard {
  display: inline-block;
}
.jkeyboard, .jkeyboard .jline, .jkeyboard .jline ul {
  display: block;
  margin: 0;
  padding: 0;
}
.jkeyboard .jline {
  text-align: center;
  margin-left: -14px;
}
.jkeyboard .jline ul li {
  font-family: arial, sans-serif;
  font-size: 20px;
  display: inline-block;
  border: 1px solid #468db3;
  -webkit-box-shadow: 0 0 3px #468db3;
  -webkit-box-shadow: inset 0 0 3px #468db3;
  margin: 5px 0 1px 6px;
  color: #000000;
  border-radius: 5px;
  width: 52px;
  height: 52px;
  box-sizing: border-box;
  text-align: center;
  line-height: 52px;
  overflow: hidden;
  cursor: pointer;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: -moz-none;
  -ms-user-select: none;
  user-select: none;
}
.jkeyboard .jline ul li.uppercase {
  text-transform: uppercase;
}
.jkeyboard .jline ul li:hover, .jkeyboard .jline ul li:active {
  background-color: #185a82;
}
.jkeyboard .jline .return {
  width: 80px;
}
.jkeyboard .jline .space {
  width: 366px;
}
.jkeyboard .jline .numeric_switch {
  width: 65px;
}
.jkeyboard .jline .layout_switch {
}
.jkeyboard .jline .shift {
  width: 60px;
}
.jkeyboard .jline .backspace {
  width: 69px;
}
</style>




<style>
body {font-family: Arial, Helvetica, sans-serif;}

.nr-dashboard-theme .nr-dashboard-template .md-button:not(:first-of-type) {
    margin-top: 0px;
}

/* The Modal (background) */
.modal {
    display: none; /* Hidden by default */
    position: fixed; /* Stay in place */
    opacity:0.99;
    z-index: 100; /* Sit on top */
    left: 0;
    top: 0;
    width: 100%; /* Full width */
    height: 100%; /* Full height */
    overflow: auto; /* Enable scroll if needed */
    background-color: rgb(0,0,0); /* Fallback color */
    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}

/* Modal Content */
.modal-content {
    position: fixed;
    background-color: #fefefe;
    margin: auto;
    padding: 0;
    bottom: 0%;
    left: 50%;
    transform: translate(-50%, 0%);
    border: 1px solid #888;
    width: 720px;
    max-width: 100%;
    max-height: 100%;
    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
    -webkit-animation-name: animate;
    -webkit-animation-duration: 0.4s;
    animation-name: animate;
    animation-duration: 0.4s
}

/* Add Animation */
@-webkit-keyframes animate {
    from {bottom:100%; opacity:0} 
    to {bottom:0%; opacity:1}
}

@keyframes animate {
    from {bottom:100%; opacity:0}
    to {bottom:0%; opacity:1}
}

/* The Close Button */
.close {
    color: black;
    float: right;
    font-size: 28px;
    font-weight: bold;
}

.close:hover,
.close:focus {
    color: #000;
    text-decoration: none;
    cursor: pointer;
}

.modal-header {
    padding: 2px 16px;
    background-color: aliceblue;
    color: white;
}

.modal-body {padding: 2px 16px;}

.modal-footer {
    padding: 2px 16px;
    background-color: #5cb85c;
    color: white;
}
</style>

<!-- The Modal -->
<div id="myModal" class="modal">

  <!-- Modal content -->
  <div class="modal-content">
      <div class="modal-header">
      <span class="close" onclick="closeModal()">&times;</span>
      <h2 id="vkeyname" style="background-color: aliceblue !important; color: black !important; text-align: center;">V-Keyboard</h2>
    </div>
    <div class="modal-body">
        <div id="keyboard"></div>
        <div>
        </div>
    </div>
  </div>
</div>


<script>
    // Get the modal
var modal = document.getElementById('myModal');

/*
$('input[type=text]').click(function () {
    $('#keyboard').unbind().removeData();
        $('#keyboard').jkeyboard({
            layout: "english",
            input: $('#'+$(this).attr('id'))
    });
});

$('input[type=number]').click(function () {
    $('#keyboard').unbind().removeData();
        $('#keyboard').jkeyboard({
            layout: "numbers_only",
            input: $('#'+$(this).attr('id'))
    });
});
*/

var inputTags;
var inputType;

var getinputs = function() {
    inputTags = document.getElementsByTagName("input");
    for (var i = 0; i < inputTags.length; i++) {
        inputTags[i].addEventListener('click', openModal, false)
    }
}

setTimeout(function(){ getinputs(); }, 1000);

var inputTarget;

var openModal = function() {
    inputType = event.target.type
    inputTarget = event.target
    //console.log(event.target.value)
    var layoutName;
    if (inputType == "number"){
        //console.log(event.target)
        inputTarget.type = "text" //hack because chrome doesn't allow setselection in number inputs
        inputTarget.value = ""
        layoutName = "numbers_only"
    }else{
        layoutName = "english"
    }
    $('#vkeyname').text(event.target.value)
    $('#keyboard').unbind().removeData();
    modal.style.display = "block";
    $('#keyboard').jkeyboard({
        layout: layoutName,
        input: $('#'+$(this).attr('id'))
    });
}


// Get the <span> element that closes the modal
var span = document.getElementsByClassName("close")[0];

// When the user clicks on <span> (x), close the modal
//span.onclick = function(event) {
  //closeModal()
//}

// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
    var source = event.target;
    if (source == modal || source == span) {
        closeModal(source)
    }
};

var closeModal = function(source){
    //console.log("closing")
    modal.style.display = "none";
   
    if (inputType == "number"){
        inputTarget.type = "number" //hack because chrome doesn't allow selectionstart on number inputs
    }
}

</script>
<script>

var clickState = 1;
var btn = document.querySelector('.VK');

btn.addEventListener('click', function(){

  if (clickState == 0) {
    this.textContent = 'V-KeyBoard On';
    modal = document.getElementById('myModal');
    clickState = 1;
  } else {
    this.textContent = 'V-KeyBoard Off';
    modal = document.getElementById('empty');
    clickState = 0;
  }

});
</script>

<style>
.VK{
    position: fixed;
    top: 60px;
    right: 20px;
    height: 30px;
}
</style>

<div id="empty"></div>
<button class="VK">V-KeyBoard On</button> 

Hello @hugobox,

I would like to thank you for your code the keyboard works great.
The only problem I have is the "enter" (return_key) does not work. When I click on it nothing happens.
I assume that on pressing enter the keyboard would close or it would move to the next input widget.

Is there a solution for this?

Hi @mc3153 !
Indeed "enter" does not close the keyboard, you can either click on the top-right "x" or click anywhere outside the keyboard to close it. For the "enter" key to do anything, it has to be coded in the webpage itself, for example in a form with a "submit" button at the end. Not sure I'd change that behavior now.

1 Like

Thank you for your reply.

Is it not possible to make a simple adjustment to a code just to close the keyboard?
I was hoping it would work similar to this lines:
window.onclick = function(event) {
var source = event.target;
if (source == modal || source == span) {
closeModal(source)
}
I tried to replace "window" with "return_key" but it does not do the trick.

I would like that the keyboard would close when you click on enter.

Yes, you could modify the code to do that.
Around line 211, you could change what the enter key does by adding a call to the function to close the keyboard:

            return_key.on('click', function () {
                me.type("\n");
                closeModal();
                me.settings.input.parents('form').submit();
            });

Is it possible to completely disable/hide the keyboard by setting a property?

I would like to dynamically enable or disable the keyboard on a certain tab.

Hi @mc3153 !

You could add a scope.watch script at the end that listens to incoming messages, something like:

<script>
(function(scope) {
  scope.$watch('msg', function(msg) {
    if (msg) {
      if (msg.payload === "enable"){
          modal = document.getElementById('myModal');
      }else{
          modal = document.getElementById('empty');
      }
    }
  });
})(scope);
</script>
1 Like

Thank you @hugobox

This works perfectly!

Dear Hugo,

Can you send me the code for centering your virtual keypa.
I'm using it, and I'm very happy with it.
The only thing is, my touchscreen has thick borders, so when the keypad is to close to the border, its hard to touch the bottum keys.
Thanks