JavaScript Virtual Keyboard

Virtual keyboard bound to a textarea field

Introduction

Imagine that you are sitting in a London internet cafe wishing to write an e-mail to your family living in Athens. It's good if someone in your family speaks English; if not, where would you find a keyboard with a Greek layout? I'm sure you can recall a dozen of situations when you thought, "I wish I had another keyboard".

This article presents the Virtual Keyboard that solves this usability problem. The design task for it can be specified as follows:

  • allow text input from computers without the user's native language layout installed, therefore allowing the creation of national/multilingual interfaces that can be used worldwide;
  • allow text input from computers without keyboard, or with sensor screens (e.g., hand-held PCs), or with remote controls (mouse, e-pen, etc.) being the only input devices;
  • protect users from keylogger-type spyware.

Installation of the Virtual Keyboard requires a casual knowledge of HTML and JavaScript; to be able to fine-tune the script, you must be familiar with the W3C DOM Level 1, CSS Level 1, and DOM/MS IE event model.

Virtual Keyboard is an open-source script distributed under the zlib/libpng license.

Setup

Five easy steps:

    • vkboard.js (vkboard folder in the archive) is the primary script. Full keyboard is simulated:

      full keyboard

    • vkboardp.js (vkboard-popup folder) is the the same as previous, but tuned to be placed in a modal popup window. Language menu (can be accessed by clicking on the red rectangle in the left-bottom corner of the keyboard) has a special two-row configuration, not a simple menu as in the previous variant:

      vkeyboard in a popup window, with language menu opened

      The most precious thing here is not the script itself, but it's surroundings: main.html and popup.html.

    • numpad.js (numpad_full folder) - numpad part of the keyboard:

      numpad variant

    • atm.js (numpad_atm folder) - stripped numpad - contains only Enter and the number keys:

      numpad_atm variant

  • Include the chosen script file:
    <HTML>
    <HEAD>

    <SCRIPT type="text/javascript" src="vkboard.js"></SCRIPT>

    ...

    </HEAD>

    ...

    Note: for each installation, two script files are available:

    • vkboard.js/vkboardp.js/numpad.js/atm.js - original script. If you wish to change the script, or just want to learn how it works, this is the file for you to look at;
    • vkboardc.js/vkboardpc.js/numpadc.js/atmc.js - compressed version of the script, 30%/30%/39%/37% (respectively) smaller than the original. This is the file you should use on the web.
  • Define a callback function:

    Collapse
    <HTML>
    <HEAD>

    <SCRIPT type="text/javascript" src="vkboard.js"></SCRIPT>
    <SCRIPT>

    // Minimal callback function:
    function keyb_callback(char)
    {
    // Let's bind vkeyboard to the <TEXTAREA>
    // with id="textfield":
    var text = document.getElementById("textfield"), val = text.value;
    switch(ch)
    {
    case "BackSpace":
    var min = (val.charCodeAt(val.length - 1) == 10) ? 2 : 1;
    text.value = val.substr(0, val.length - min);
    break;

    case "Enter":
    text.value += "\n";
    break;

    default:
    text.value += ch;
    }

    text.focus();

    // The following piece of code is intended to
    // correctly position the insertion marker for
    // both left-to-right and right-to-left languages:
    //
    var l = text.value.length;
    if(text.setSelectionRange)
    {
    text.setSelectionRange(l, l);
    }
    else if(document.selection)
    {
    var Sel = document.selection.createRange();

    if(Sel.moveToElementText)
    {
    Sel.moveToElementText(text);
    Sel.collapse(true);
    Sel.move('character', l);
    Sel.select();
    }
    }
    }
    </SCRIPT>
    </HEAD>

    ...

    The callback function must have a single parameter that accepts a string value returned by the virtual keyboard script.

    Note: this is the most basic callback function; an example of a more advanced code snippet is given later.

  • Define a container for the keyboard, which must be an empty DIV or SPAN:
     <HTML>
    ...

    <BODY>

    ...

    <TEXTAREA id="textfield" rows="10" cols="50"></TEXTAREA>

    <DIV id="keyboard"></DIV>

    </BODY></HTML>
  • Finally, show the keyboard:
     <BODY onload="ShowVKeyboard("keyboard", "keyb_callback");">

    <!-- VKeyboard is shown on load, bound to container with
    id="keyboard" and with callback function "keyb_callback" -->

    Of course, the call to ShowVKeyboard(...) (numpad: ShowVNumpad(...), ATM-style numpad: ShowVATM(...)) can be used anywhere a JavaScript function can be.

ShowVKeyboard has the following parameters (and defaults):

ShowVKeyboard("container_id",  // container's id, mandatory;
"keyb_callback", // name of the callback function, mandatory;

// the following params are optional:

true, // change show/hide state
// when changing the callback?
"14px", // font size, in pixels;
"#000", // font color;
"#F00", // font color for the dead keys;
"#FFF", // background color;
"#777", // border color;
"#CCC", // border color of "inactive"
// (no value/disabled) key;
"#F77", // border color of the language selector's cell;
"#DDD", // background color of switched/selected item;
true); // create numpad or not?

ShowVNumpad and ShowVATM have a limited set of parameters:

ShowVNumpad("container_id",  // container's id, mandatory;
"keyb_callback", // name of the callback function, mandatory;

// the following params are optional:

true, // change show/hide state
// when changing the callback?
"14px", // font size, in pixels;
"#000", // font color;
"#FFF", // background color;
"#777"); // border color;

Examples of usage:

Collapse
<SCRIPT>

ShowVKeyboard("keyboard1", "keyb_callback");

// creates 2nd keyboard object with
// the same callback function.
ShowVKeyboard("keyboard2", "keyb_callback");

</SCRIPT>

<SCRIPT>

// Binds an element with ID = "keyboard" to a vkeyboard
// with a callback of "keyb_callback_1" and shows it.
ShowVKeyboard("keyboard", "keyb_callback_1");

// Changes the callback from
// "keyb_callback_1" to "keyb_callback_2" and hides the keyboard.
ShowVKeyboard("keyboard", "keyb_callback_2");

</SCRIPT>

<SCRIPT>

// Binds an element with ID = "keyboard" to a vkeyboard
// with a callback of "keyb_callback_1" and shows it.
ShowVKeyboard("keyboard", "keyb_callback_1");

// Changes the callback from
// "keyb_callback_1" to "keyb_callback_2"; keyboard stays on-screen.
ShowVKeyboard("keyboard", "keyb_callback_2", false);

</SCRIPT>

Creating your own language layout

Two easy steps:

  • Append the avail_langs array with a two-member array consisting of the language abbreviation and the layout name (written in that language, using Unicode hex values where required):
    var avail_langs = 
    new Array(Array("Us", "English (US)"),
    Array("Ca", "Canadian"),
    Array("Ru", "Ðóññêèé"),
    Array("De", "Deutsch"),
    Array("Fr", "Français"),
    Array("Es", "Español"),
    Array("It", "Italiano"),
    Array("Cz", "Česky"),
    Array("El", "Έλλ" +
    "ηνας"),
    Array("He", "עברית"));
  • Define "normal", and, optionally, the "Caps Lock"ed, "Shift"ed, "AltGr"ed, and "AltGr+Shift"ed arrays.

    The following rules apply:

    • Each array's name must begin with the language abbreviation and the underscore symbol.
    • Names of arrays with values representing symbols for a keyboard with "Caps Lock" pressed must end with "caps":
      // Czech layout:
      var Cz_caps = new Array(";", ... , "-");
    • Names of arrays with values representing symbols for a keyboard with "Shift" pressed must end with "shift":
      var Cz_shift = new Array("º", ... , "_");
    • Names of arrays with values representing symbols for a keyboard with "AltGr" pressed must end with "alt_gr":
      var Cz_alt_gr = new Array(..., "!", "/");
    • Names of arrays with values representing symbols for a keyboard with "AltGr" and "Shift" keys pressed must end with "alt_gr_shift":
      var Cz_alt_gr_shift = new Array("~", ... , "?");
    • Names of arrays with values representing symbols for a keyboard's normal condition (with no modifier keys pressed) must end with "normal":
      var Cz_normal = new Array(";", ... , "-");
    • Each array must have exactly 48 entries, each containing either the hexadecimal value of the appropriate symbol, or, in the case of a dead key, the array consisting of the hex value for this dead symbol and the name of one of the eight predefined arrays:

      • Acute
      • Caron
      • Cedilla
      • Circumflex
      • Grave
      • Ring
      • Tilde
      • Umlaut
      var Cz_alt_gr = new Array(Array("`", "Grave"), // dead key
      "!", ... // simple key
    • Arrays are mapped to the layout according to the following illustration (numbers within cells are the array indices):

      array-to-key mapping

    • The "normal" array is mandatory; others are optional.

    The following layouts have been defined:

    • English (US International),
    • Canadian (multilingual standard),
    • Russian,
    • German,
    • French,
    • Spanish,
    • Italian,
    • Czech,
    • Greek,
    • Hebrew.

    language menu for the full keyboard (vkboard.js)

Note 1: as a source of the layout info, I've used the IBM Globalization database and the Wikipedia article on keyboard layouts.

Note 2: Czech layout differs from one presented at the IBM Globalization database; I've used the layout definition from Bohemica.com, which seems a bit more comprehensive.

Creating your own keyboard layout

You may wish to create a custom key layout. There are two ways to achieve this:

  1. Use atm.js (the most simple script of four; see numpad_atm folder in the archive) as a template. In short, the script flow is as follows:

    • create the outer box:

      Collapse
        var initX = 0, initY = 0;

      ...

      var kb = document.createElement("DIV");
      ct.appendChild(kb);

      ct.style.display = "block";
      ct.style.position = "absolute";
      ct.style.top = initY + "px", ct.style.left = initX +"px";

      kb.style.position = "relative";
      kb.style.top = "0px", kb.style.left = "0px";
      kb.style.border = "1px solid " + bc;

      var kb_main = document.createElement("DIV");
      kb.appendChild(kb_main);

      kb_main.style.position = "relative";
      kb_main.style.width = "1px";
      kb_main.style.cursor = "default";
      kb_main.style.backgroundColor = bkc;

      ...

      kb.style.width = kb_main.style.width = String(findOffX(kb_pad_3)
      + findOffW(kb_pad_3) + 1) + "px";

      kb_main.style.height = (findOffY(kb_pad_0) +
      findOffH(kb_pad_0) + 1) + "px";
    • create the keys with setup_key function:

        var kb_pad_7 = setup_key(kb_main, "1px", "1px", cp, cp, bc, c, lh, fs);
      kb_pad_7.sub.innerHTML = "7";
      kb_pad_7.sub.id = container_id + "___pad_7";
    • route all output to the generic_callback_proc function:

        for(var k in vkboard.ctrl)
      setup_event(vkboard.ctrl[k], 'mousedown', generic_callback_proc);
    • invoke the callback function:

      function generic_callback_proc(event)
      {
      ...

      if(val && vkboard.Callback)
      eval(vkboard.Callback + "(\"" + val + "\")");
      }

      Sample layout

  2. Hire me. As a creator of the Virtual Keyboard, I can quickly reprogram vkboard to any keyboard configuration you wish.

Scalability issue

Currently, all element dimensions within the script are specified in pixels. This causes problems with the Mozilla/Firefox browser, where the "text size" changes only the font-size parameter, independent of any other size-related attributes. MS IE (in which font sizes are fixed once specified) and Opera (which zooms the entire page) do not have this problem. The issue is under investigation.

(There are rumors that Firefox 3 browser will feature an Opera-like page zoom behaviour.)

Call from beyond

One natural feature that all users expect from a text field is the ability to edit the text at any position within a field. However, it is impossible to do so with a function described earlier, which only appends symbols to (or removes from) the end of the text.

The following script is the attempt to write a compatible callback function to fulfill the above task. It is largely based on the discussion on the thescripts.com forum.

Collapse
<HEAD>
<SCRIPT type="text/javascript"><!--

var opened = false,
insertionS = -1, // selection start
insertionE = 0; // selection end

...

// Callback function:
function keyb_callback(ch)
{
var text = document.getElementById("textfield"), val = text.value;

if(document.selection && text.caretPos)
{
switch(ch)
{
case "BackSpace":
if(val.length)
{
var dup = document.selection.createRange().duplicate();

dup.moveToElementText(text);
dup.collapse(true);
dup.moveEnd('character', 1);

while(dup.compareEndPoints("EndToStart", text.caretPos) < 0)
{
dup.moveEnd('character', 1);
dup.moveStart('character', 1);
}

dup.text = "";
}
break;

case "Enter":
insertAtCaret(text, "\n");
break;

default:
insertAtCaret(text, ch);
}
}
else
{
var valP = val.substring(0, insertionS == -1 ? insertionE : insertionS),
valN = val.substring(insertionE, val.length);

switch(ch)
{
case "BackSpace":
var min = (valP.charCodeAt(valP.length - 1) == 10) ? 2 : 1;
text.value = valP.substr(0, valP.length - min) + valN;
insertionE -= min;
break;

case "Enter":
text.value = valP + "\n" + valN;
insertionE += window.opera ? 2 : 1; // Opera has a strange bug(?)
break;

default:
text.value = valP + ch + valN;
insertionE += insertionS == -1 ? 1 : insertionS - insertionE + 1;
}

insertionS = -1;
}

if(text.setSelectionRange)
{
text.setSelectionRange(insertionE, insertionE);
}
}

function getCaretPositions(ctrl)
{
var CaretPosS = -1, CaretPosE = 0;
ctrl.focus();

// Mozilla way - store range start/end position:
if(ctrl.selectionStart || ctrl.selectionStart == '0')
{
CaretPosS = ctrl.selectionStart;
CaretPosE = ctrl.selectionEnd;

insertionS = CaretPosS == -1 ? CaretPosE : CaretPosS;
insertionE = CaretPosE;
}
// IE way - just storing the text range, not the position:
else if(document.selection && ctrl.createTextRange)
{
ctrl.caretPos = document.selection.createRange().duplicate();
}
}

// MS IE-only insertion:
function insertAtCaret(ctrl, text)
{
if(ctrl.createTextRange && ctrl.caretPos)
{
var caretPos = ctrl.caretPos;
caretPos.text +=
caretPos.text.charAt(caretPos.text.length - 1) ==
' ' ? text + ' ' : text;

caretPos.select();
}
else
ctrl.value += text;
}
//--></SCRIPT></HEAD>

<BODY>

...

<-- Don't forget this 'onclick' and 'onkeyup': -->
<TEXTAREA onkeyup="getCaretPositions(this);"
onclick="getCaretPositions(this);"
id="textfield" rows="12" cols="50">
</TEXTAREA>

...

</BODY>

You can test the above script by running the edit_full.html file, vkboard folder, from the attached archive. Basic callback is demonstrated in edit_simple.html.

Tips and tricks

Script flow is quite straightforward, so, I hope, it won't be hard to dive into it. Here is a couple of words on some tricky places within.

  • Event setup. We need to handle both MS IE and W3C DOM event models:

    function setup_event(elem, eventType, handler)
    {
    // MS IE event setup:
    if(elem.attachEvent)
    {
    elem.attachEvent('on' + eventType, handler);
    }
    // DOM event setup:
    else if(elem.addEventListener)
    {
    elem.addEventListener(eventType, handler, false);
    }
    }
  • Key container setup. Each key consists of the "outer" DIV, where we set the top, left, width and height parameters only, and the "inner" DIV, which accepts padding, border color and other parameters. We use such a complex construction to circumvent the box model problem of modern browsers.

    Note: here is the JavaScript solution. If you wish to avoid the box model problem using CSS, you may wish to see the article by Trenton Moss (see item #6).

    function setup_key(parent, id, top, left, width, height,
    border_color, text_align, line_height,
    font_size, font_weight, padding_left,
    padding_right)
    {
    // Outer DIV:
    var key = document.createElement("DIV");
    setup_style(key, top, left, width, height, "absolute");

    // Inner DIV:
    var key_sub = document.createElement("DIV");
    key.appendChild(key_sub);

    setup_style(key_sub, "", "", "", line_height, "relative", border_color,
    text_align, line_height, font_size, font_weight,
    padding_left, padding_right, font_color, back_color);

    return key_sub;
    }
  • Key style setup. The last two calls of setup_event is the only interesting part here; these calls deny the selection of the container's content. Perfectly usable (and compatible) to use it instead of UNSELECTABLE (MS IE) and -moz-user-select (Gecko-based browsers) properties.

    Collapse
    function setup_style(obj, top, left, width, height, position,
    border_color, text_align, line_height,
    font_size, font_weight, padding_left,
    padding_right)
    {
    if(top)
    obj.style.top = top;
    if(left)
    obj.style.left = left;
    if(width)
    obj.style.width = width;
    if(height)
    obj.style.height = height;
    if(position)
    obj.style.position = position;
    if(border_color)
    obj.style.border = "1px solid " + border_color;

    if(text_align)
    obj.style.textAlign = text_align;
    if(line_height)
    obj.style.lineHeight = line_height;
    if(font_size)
    obj.style.fontSize = font_size;
    obj.style.fontWeight = (font_weight ? font_weight : "bold");

    if(padding_left)
    obj.style.paddingLeft = padding_left;
    if(padding_right)
    obj.style.paddingRight = padding_right;

    setup_event(obj, "selectstart", new Function("event", "return false;"));
    setup_event(obj, "mousedown", new Function("event",
    "if(event.preventDefault) event.preventDefault();
    return false;"));
    }
  • Invoking the callback. Nothing complex here, except the fact that we must safeguard the callback function from recieving the improper parameter:

    // Full keyboard:
    function generic_callback_proc(event)
    {
    ...

    if(val && vkboard.Callback)
    eval(vkboard.Callback + (val == "\"" ? "('" + val + "')" :
    (val == "\\" ? "(\"\\\\\")" :
    "(\"" + val + "\")")));
    }

    // Numpad and ATM-style numpad - much easier generic_callback_proc:
    function generic_callback_proc(event)
    {
    ...

    if(val && vkboard.Callback)
    eval(vkboard.Callback + "(\"" + val + "\")");
    }
  • Saving space with language arrays. A little tip. If one of the language arrays is a sparse array (i.e. contains very few number of entries - 6 or less), it maybe useful not to initialize the array in an ordinary way:

    var He_alt_gr = new Array(..., ";", "1",
    ...empty entries go on...,
    "ץ", ".", ...);

    but to do:

    var He_alt_gr = new Array(48);

    He_alt_gr[4] = "₪";
    He_alt_gr[11] = "־"; He_alt_gr[19] = "װ";
    He_alt_gr[31] = "ײ"; He_alt_gr[32] = "ױ";

Coda

Pros:

  • A complete JavaScript toolkit suitable for simulating every single aspect of a real keyboard device;
  • A self-contained, compact (compressed version: 38.9 Kb) script that doesn't require any images - for faster download;
  • Very simple setup procedure;
  • Works and looks the same way on all mainstream browsers (tested on Mozilla Firefox 1.5, Opera 7.5/9, Netscape 8.1, MS IE 6);
  • Customizable size and colors - perfect for skinable environments;
  • Several variants are available (full keyboard, popup-tuned full keyboard, numpad, ATM-style numpad);
  • Ten most used keyboard layouts are bundled with the full script;
  • Allows the use of as much keyboard objects per page as you wish;
  • Allows changing the callback procedure for every keyboard object on-the-fly;
  • Open-source script, distributed under the zlib/libpng license (=can be used free of charge even on commercial sites).

Cons:

  • Unscalable (on some browsers) layout presents a serious accessibility issue (under investigation).

Script requirements:

Any browser that is aware of:

  • JavaScript (implementation compliant with ECMAScript Edition 3);
  • W3C DOM Level 1;
  • CSS Level 1 (at least, most of the elements);
  • W3C or MS IE event model.

Links:

History

  • April 10th, 2006:
    • first version, English layout only.
  • April 14th, 2006:
    • code refined;
    • Russian, German, French, and Czech layouts added.
  • April 22nd, 2006:
    • second code revision, much more compact and robust code;
    • Spanish, Italian, and Greek layouts added.
  • May 14th, 2006:
    • third code revision, even more robust and error-proof code;
    • Canadian (multilingual standard) and Hebrew layouts added;
    • "AltGr" key and appropriate layout variants added;
    • German, French, Spanish, and Greek layouts fixed;
    • "Caps Lock" + "Shift" case switching fixed;
    • language menu positioning issue fixed;
    • moved from manual to on-focus switching in the "test2" example.
  • May 16th, 2006:
    • all "box model"-related issues solved.
  • June 12th, 2006:
    • most layouts revised and fixed;
    • minor language menu issue fixed;
    • pilot implementation of the dead keys subsystem.
  • August 28th, 2006:
    • keyboard now pops over the page rather than embedding into it;
    • keyboard font size can now be specified, keyboard scales uniformly with font size;
    • keyboard colors can now be customized;
    • Shift and AltGr keys now deactivate after alphanumeric key has been pressed;
    • fixed an obscure language menu bug sometimes occurring in MS IE;
    • compressed version of script is now bundled in the archive;
    • minor code changes and cleanups, too numerous to be listed.
  • September 14th, 2006:
  • October 4th, 2006:
    • numpad-only - 2 variants - scripts are now bundled in the archive;
    • [full keyboard] fixed problem with an improper cursor positioning when used with Hebrew in edit_simple sample;
    • [full keyboard] many corrections to Greek layout; letters with acute, umlaut and Dialytika Tonos accents are now available;
    • [full keyboard] added optional switch to ShowVKeyboard, that allows you not to create the numpad.
  • October 16th, 2006:
    • [full keyboard] dead-keys subsystem reworked;
    • [all variants] slightly more compact and clean code.
  • October 26th, 2006:
    • popup-tuned variant of the vkeyboard created;
    • [all variants] key parts of the script were rewritten, resulting in smaller and faster code;
    • [all variants] fixed the annoying issue with key text selection when "typing" fast (due to eventual drag-n-drop);
    • [all variants] got rid of all browser detection code;
    • [full keyboard] advanced callback function was rewritten; works ok with MS IE 6, Firefox 1.5, Opera 9 and Netscape 8.1.
  • November 16th, 2006:
    • [full keyboard] test_fly_anonym sample added;
    • [full/popup keyboard] 'new shekel' symbol added to the Hebrew layout;
    • [full/popup keyboard] language names in the language menu were rewritten in the languages they represent.

posted on 2006-12-21 11:23  心悦  阅读(2447)  评论(0编辑  收藏  举报