Virtual Keyboard a little help please

Hi everyone,
I am using the Virtual Keyboard as modified by @hugobox and shared on Node-Red flows.
I have sent @hugobox a message but as yet no reply.
link to node

I'm trying to modify it to display the text that has been entered as in this example which I believe is where the Virtual Keyboard originated. I just don't understand JQuery.
Example

I can see where the header line is displayed (line 524) but I can't see what the value to display is called anywhere.

Any pointers or help greatly appreciated.

Cheers.
Paul.

Hi Paul!
Here's a modification of the code that enables what you need. Let me know if it works as intended.

<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.type)
                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()
                }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;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    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 {top:100%; opacity:0} 
    to {top:50%; opacity:1}
}

@keyframes animate {
    from {top:100%; opacity:0}
    to {top:50%; 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() {
    $('#vkeyname').text("V-KeyBoard")
    inputType = event.target.type
    inputTarget = event.target
    var layoutName;
    if (inputType == "number"){
        //console.log(event.target)
        inputTarget.type = "text" //hack because chrome doesn't allow setselection in number inputs
        layoutName = "numbers_only"
    }else{
        layoutName = "english"
    }
    $('#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> 



Hi @hugobox,
Thank you for your reply and code.
If the field being edited text or numeric, has a value prior to being selected, that value is not being shown until I click a key. If that value could be shown prior to any key press, to make it obvious that a value was being edited, well that would be the icing on the cake!

Many Thanks
Paul.

Hi again @hugobox ,
I just noticed that the functionality is not in the numeric keypad.

Cheers.
Paul.

Yeah I now have the prior values shown, but I see that the numeric keypad isn't working as intended...

I have not found a clever way to make it work with numeric inputs, so instead of having a buggy behavior, now with numeric, it replaces the prior value instead of trying to modify it.

<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;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    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 {top:100%; opacity:0} 
    to {top:50%; opacity:1}
}

@keyframes animate {
    from {top:100%; opacity:0}
    to {top:50%; 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> 
1 Like

Hi @hugobox,
That looks good to go!
Thank you very much, If I have any more questions or feature requests may I get back to you?
Where in the world are you?

Cheers.
Paul.

Sure feel free to reach out, I usually reply within reasonable delays! I'm in snowy Montreal, how bout you?

Thanks.
Not sunny, Not snowy but very grey, Norwich, Norfolk UK :wink:

Cheers.
Paul.

1 Like

Hi @hugobox,
Hope all's well with you.
There's still a problem with the virtual keyboard.

I was editing a form email field.
I clicked on the field.
Pressed delete.
It deleted the end character and then added the entire field contents.

Probably best to not try and edit existing data but clear the field and start from fresh.

Cheers.
Paul.

Hi @hugobox, I changed the field type from email to text and it's ok.
I think you should make this in to a node. :wink:

Cheers.
Paul.

I'll test with email type and let you know if I find a good solution.

Hi @hugobox, I have found a couple of minor niggles.
for example changing the selection from keyboard on/off and back again, the numeric keypad wouldn't come back it would only display the text keyboard for a numeric field. Having a smaller layout for the numeric keypad.

Let me work through it and I'll come back to you, perhaps get it sorted in one swoop rather than keep coming back to you while you are busy.

Having a virtual keyboard is such an important feature, I'm surprised there isn't one as part of the default node set.

Cheers.
Paul.

Hello @hugobox

I'm very new to this ecosystem, so appologize in advance for my ignorance. I got my first linux box this week (a pi) and loaded it up with node and node-red. I am going to be installing some wall mounted tablets to conrol my home automation (smartthings) with a sharptools dashboard.

My first Node project is integrating my alarm system (done--installed node.js integration package). Now I want to build a simple alarm keypad in node-red that will pass my pin and an arming/disarming command to my alarm system. I have the following questions after analyzing your sample flow and playing around with it.

  1. what happens when i access a text input node from a tablet (will the tablet natively pop-up a keyboard in addition to your virtual keyboard?

  2. This might be a misunderstanding on my part... But why did you create a custom template node for the Quantity Field; Whereas you seem to have hooked into the standard text input node for User ID and Password? (I did play around with using the text input field with a number type, and the behavior was a bit wonky in terms of moving my cursor and deleting when I was inputting numbers. Not sure why...)

  3. How can I get the virtual keyboard to pop up on a button press (versus entering a text field)? I'm still thinking through the best UI design.... One idea is that I would click on an action button('arm away,' 'arm stay', 'disarm') and have it pop up the numeric keypad, upon hitting enter I would run a function to concantenate my pin with the action code and pass it to the alarm system (without ever showing the PIN on the screen)

  4. Is there an easy way to get the numeric keypad to always show (instead of on button press)? I think i prefer the above option (3), but I'd like to know if this is possible, for other use cases.

  5. I'd like to not pass characters to my function until i hit enter on the numeric keypad. Upon hitting enter, I would need to close the keypad automatically and run the concantenation that builds the string for the alarm interface. Can I make that happen easily (close keypad upon hitting enter and not pass any of the numbers to downstream nodes until i hit enter?

  6. Is there anyway to put a charachter mask to not allow more than 4 characters? This is not that big of a deal, I can always figure out away to trim the left 4 charachters or something like that.

Kind Regards,

Doug

Hi @DaringDougler,
In my opinion and it is only my opinion, your items 5 & 6 are not the way a normal keyboard would function. In terms of functionality It should simply be an on screen replacement virtual keyboard behaving exactly the way a physical keyboard would. If the keyboard is modified to your functionality then what happens to everyone else who doesn't want/require that functionality when they apply an update?

The validation and masking of any input should be down to your code, and what you're asking is relatively easy to implement in your code.

If you need a little help just ask.

Just sayin :wink:

All the best.

Hi @hugobox,
Ok I've had some time with the Virtual Keyboard, and here are a couple of things I've noticed.

  1. On the numeric keypad the top row is initially indented to the left.
  2. On the numeric keypad referring to item 1, as a result when the first numeric key is touched the whole keyboard moves to re centre and allow the value to be displayed.
  3. On the numeric keypad should the white space be reduced to be similar to the size of the numeric keypad.

Finally I can't get this behaviour to replicate - changing the selection from keyboard on/off and back again, the numeric keypad wouldn't come back it would only display the text keyboard for a numeric field.

As someone famous with long ears said "That's All Folks!".
Again thanks for all your help.
Cheers.
Paul.

Hi @DaringDougler !

  1. I'm not sure, haven't tried on a tablet yet, let me know if you find out!

  2. I don't remember the reason, but it works with the node-red-dashboard text input set to number Mode. I also fixed the wonky behavior in the next patch.

3 & 4 & 5 & 6. If you want to go that route, you'll need to search the forum for a keypad template.

Thanks for your inputs guys!

I've worked on some of the wonky behaviors you reported, here's the latest keyboard code, let me know if it works better or introduces new problems:

<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 {
                if (input_node.type == "text"){
                    var new_string = this.insertToString(start, end, val, key);
                    input.val(new_string);
                    start++;
                    end = start;
                    input_node.setSelectionRange(start, end);
                }else{
                    input.val(val + key);
                }
                input.change()
                $('#vkeyname').text(val + key)
                
            }
            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;
            if (input.type == "text"){
                input.val(val.slice(0, start-1) + val.slice(start))
                input_node.setSelectionRange(start-1, start-1);
                console.log(val)
                $('#vkeyname').text(val)
            }else{
                input.val(val.slice(0,-1))
                $('#vkeyname').text(val.slice(0,-1))
            }
            input.change()
            input.focus()
        },

        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
    var layoutName;
    if (inputType == "number"){
        //inputTarget.type = "number" //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> 

Hi @hugobox,
That seems to work OK.
And existing numeric values are now being edited.

Just aesthetically, with no value the numeric keypad top row is initially indented to the left.
Is it easy to correct? It is a minor issue so not ever so important.
Again aesthetically the size of the numeric keypad whitespace.

apart from that all is good.

Cheers.
Paul.

Great! I'll look into these, shouldn't be too hard to fix hopefullly!