Click here to Skip to main content
15,886,110 members
Articles / Web Development / HTML

JavaScript Virtual Keyboard

Rate me:
Please Sign up or sign in to vote.
4.91/5 (181 votes)
30 Mar 2008CPOL20 min read 1.2M   23.6K   323  
This article presents a Virtual Keyboard - an important addendum to the library of usability tools.
<HTML>
 <HEAD>
  <SCRIPT type="text/javascript" src="vkboard.js"></SCRIPT>
  <SCRIPT><!--

   // This example shows the (still VERY experimental) way
   // of specifying the vkeyboard font size with not only "px",
   // but with any CSS length unit, i.e. %, em, ex, pt, pc,
   // in, cm and mm.

   // Parts of the following code are taken from the DocumentSelection
   // library (http://debugger.ru/projects/browserextensions/documentselection)
   // by Ilya Lebedev. DocumentSelection is distributed under LGPL license
   // (http://www.gnu.org/licenses/lgpl.html).

   // Look at the 'keyb_change' call in the very bottom of this
   // page to see how it works and try to change it.

   var opened = false, vkb = null, text = null, insertionS = 0, insertionE = 0;

   var userstr = navigator.userAgent.toLowerCase();
   var safari = (userstr.indexOf('applewebkit') != -1);
   var gecko  = (userstr.indexOf('gecko') != -1) && !safari;
   var standr = gecko || window.opera || safari;

   // This function is the "connector" from arbitrary length
   // units to "px" world. Parameters are:
   //
   // 'obj' - object, in which context we should determine
   //            the px value from the size given in 'value';
   //
   // 'value' - string, consisting of the numeric value and
   //                the length unit designator.
   //
   function convert_to_px(obj, value)
   {
     // Inner function - retrieves the (default) font size
     // for the given object.
     function get_font_size(obj)
     {
       // Standard way:
       if(window.getComputedStyle)
         return window.getComputedStyle(obj, "").getPropertyValue("font-size");

       // MS IE way:
       if(obj.currentStyle)
         return obj.currentStyle.fontSize;

       return "16px";
     }

     var unit, valu, last = value.length ? value.substr(value.length - 1, 1) : "";

     if(isNaN(last))
     {
       var def = get_font_size(document.body);
       unit = def.substr(def.length - 2, 2), valu = def.substr(0, def.indexOf(unit));

       switch(value)
       {
         case "xx-small": valu -= 3; break;
         case "x-small":  valu -= 2; break;
         case "small":    valu -= 1; break;
         case "medium":              break; // already set
         case "large":    valu += 1; break;
         case "x-large":  valu += 2; break;
         case "xx-large": valu += 3; break;
         default:
           if(value.length)
           {
             unit = (last == "%") ? "%" : value.substr(value.length - 2, 2);
             valu = value.substr(0, value.indexOf(unit));
           }
       }
     }
     else
       return value;

     if(unit == "px")
       return valu;

     // Below we have the main problem with the code. The problem is -
     // only MS IE has the facility ('window.screen.logicalXDPI' property)
     // to determine current screen DPI, which we need to convert values
     // from absolute length units (pt, pc, in, cm, mm) to pixels.
     //
     // Currently, no other browser has the way to retrieve the DPI,
     // so we have to use the "usual" value of 96 DPI (== 1.3333 pixels
     // per point), which is quite common for Windows machines.
     //
     // With browsers other than MS IE, only relative length units
     // (em, ex, % or px) can be used safely.

     // Pixels per point:
     var px_per_pt = window.screen.logicalXDPI ? (window.screen.logicalXDPI / 72.0) : 1.3333;

     // 'base_u' - base unit for current hierarchy level;
     // 'base_v' - base value for current hierarchy level;
     //
     var base_u, base_v = 1, obj_ = obj;

     do
     {
       var base = get_font_size(obj_);
       if(String(base).length)
       {
         var tmp = base.substr(base.length - 1, 1);

         if(isNaN(tmp))
         {
           base_u  = (tmp == "%") ? "%" : base.substr(base.length - 2, 2);
           base_v *= base.substr(0, base.indexOf(base_u));

                if(base_u == "%")  { base_v /= 100.0; }
           else if(base_u == "ex") { base_v /= 2.0; }
           else if(base_u == "em") { }
           else break;
         }
         else  
         {
           base_u  = "px";
           base_v *= base;
           break;
         }
       }

       obj_ = obj_.parentNode;
     }
     while(obj_ != document.documentElement);

     if(!base_v) { base_v = 16; base_u = "px"; }

     switch(unit)
     {
       case "%":  valu /= 50.0;
       case "ex": valu /= 2.0;
       case "em": valu *= base_v, unit = base_u;
     }

     switch(unit)
     {
       case "mm": valu *= 0.1;
       case "cm": valu *= 0.3937007874015748031496062992126;
       case "in": valu *= 6
       case "pc": valu *= 12;
       case "pt": valu *= px_per_pt;
     }

     return valu;
   }

   function keyb_change(size)
   {
     document.getElementById("switch").innerHTML = (opened ? "Show keyboard" : "Hide keyboard");
     opened = !opened;

     if(opened && !vkb)
     {
       var obj = document.getElementById("keyboard");

       vkb = new VKeyboard("keyboard",    // container's id
                           keyb_callback, // reference to the callback function
                           true,          // create the arrow keys or not? (this and the following params are optional)
                           true,          // create up and down arrow keys? 
                           false,         // reserved
                           true,          // create the numpad or not?
                           "",            // font name ("" == system default)
   convert_to_px(obj,size) + "px",        // font size in px
                           "#000",        // font color
                           "#F00",        // font color for the dead keys
                           "#FFF",        // keyboard base background color
                           "#FFF",        // keys' background color
                           "#DDD",        // background color of switched/selected item
                           "#777",        // border color
                           "#CCC",        // border/font color of "inactive" key (key with no value/disabled)
                           "#FFF",        // background color of "inactive" key (key with no value/disabled)
                           "#F77",        // border color of the language selector's cell
                           true,          // show key flash on click? (false by default)
                           "#CC3300",     // font color during flash
                           "#FF9966",     // key background color during flash
                           "#CC3300",     // key border color during flash
                           false,         // embed VKeyboard into the page?
                           true,          // use 1-pixel gap between the keys?
                           0);            // index(0-based) of the initial layout
     }
     else
       vkb.Show(opened);

     text = document.getElementById("textfield");
     text.focus();

     if(document.attachEvent)
       text.attachEvent("onblur", backFocus);
   }

   function backFocus()
   {
     if(opened)
     {
       var l = text.value.length;

       setRange(text, insertionS, insertionE);

       text.focus();
     }
   }

   // Advanced callback function:
   //
   function keyb_callback(ch)
   {
     var val = text.value;

     switch(ch)
     {
       case "BackSpace":
         if(val.length)
         {
           var span = null;

           if(document.selection)
             span = document.selection.createRange().duplicate();

           if(span && span.text.length > 0)
           {
             span.text = "";
             getCaretPositions(text);
           }
           else
             deleteAtCaret(text);
         }

         break;

       case "<":
         if(insertionS > 0)
           setRange(text, insertionS - 1, insertionE - 1);

         break;

       case ">":
         if(insertionE < val.length)
           setRange(text, insertionS + 1, insertionE + 1);

         break;

       case "/\\":
         if(!standr) break;

         var prev  = val.lastIndexOf("\n", insertionS) + 1;
         var pprev = val.lastIndexOf("\n", prev - 2);
         var next  = val.indexOf("\n", insertionS);

         if(next == -1) next = val.length;
         var nnext = next - insertionS;

         if(prev > next)
         {
           prev  = val.lastIndexOf("\n", insertionS - 1) + 1;
           pprev = val.lastIndexOf("\n", prev - 2);
         }

         // number of chars in current line to the left of the caret:
         var left = insertionS - prev;

         // length of the prev. line:
         var plen = prev - pprev - 1;

         // number of chars in the prev. line to the right of the caret:
         var right = (plen <= left) ? 1 : (plen - left);

         var change = left + right;
         setRange(text, insertionS - change, insertionE - change);

         break;

       case "\\/":
         if(!standr) break;

         var prev  = val.lastIndexOf("\n", insertionS) + 1;
         var next  = val.indexOf("\n", insertionS);
         var pnext = val.indexOf("\n", next + 1);

         if( next == -1)  next = val.length;
         if(pnext == -1) pnext = val.length;

         if(pnext < next) pnext = next;

         if(prev > next)
            prev  = val.lastIndexOf("\n", insertionS - 1) + 1;

         // number of chars in current line to the left of the caret:
         var left = insertionS - prev;

         // length of the next line:
         var nlen = pnext - next;

         // number of chars in the next line to the left of the caret:
         var right = (nlen <= left) ? 0 : (nlen - left - 1);

         var change = (next - insertionS) + nlen - right;
         setRange(text, insertionS + change, insertionE + change);

         break;

       default:
         insertAtCaret(text, (ch == "Enter" ? (window.opera ? '\r\n' : '\n') : ch));
     }
   }

   // This function retrieves the position (in chars, relative to
   // the start of the text) of the edit cursor (caret), or, if
   // text is selected in the TEXTAREA, the start and end positions
   // of the selection.
   //
   function getCaretPositions(ctrl)
   {
     var CaretPosS = -1, CaretPosE = 0;

     // Mozilla way:
     if(ctrl.selectionStart || (ctrl.selectionStart == '0'))
     {
       CaretPosS = ctrl.selectionStart;
       CaretPosE = ctrl.selectionEnd;

       insertionS = CaretPosS == -1 ? CaretPosE : CaretPosS;
       insertionE = CaretPosE;
     }
     // IE way:
     else if(document.selection && ctrl.createTextRange)
     {
       var start = end = 0;
       try
       {
         start = Math.abs(document.selection.createRange().moveStart("character", -10000000)); // start

         if (start > 0)
         {
           try
           {
             var endReal = Math.abs(ctrl.createTextRange().moveEnd("character", -10000000));

             var r = document.body.createTextRange();
             r.moveToElementText(ctrl);
             var sTest = Math.abs(r.moveStart("character", -10000000));
             var eTest = Math.abs(r.moveEnd("character", -10000000));

             if ((ctrl.tagName.toLowerCase() != 'input') && (eTest - endReal == sTest))
               start -= sTest;
           }
           catch(err) {}
         }
       }
       catch (e) {}

       try
       {
         end = Math.abs(document.selection.createRange().moveEnd("character", -10000000)); // end
         if(end > 0)
         {
           try
           {
             var endReal = Math.abs(ctrl.createTextRange().moveEnd("character", -10000000));

             var r = document.body.createTextRange();
             r.moveToElementText(ctrl);
             var sTest = Math.abs(r.moveStart("character", -10000000));
             var eTest = Math.abs(r.moveEnd("character", -10000000));

             if ((ctrl.tagName.toLowerCase() != 'input') && (eTest - endReal == sTest))
              end -= sTest;
           }
           catch(err) {}
         }
       }
       catch (e) {}

       insertionS = start;
       insertionE = end
     }
   }

   function setRange(ctrl, start, end)
   {
     if(ctrl.setSelectionRange) // Standard way (Mozilla, Opera, Safari ...)
     {
       ctrl.setSelectionRange(start, end);
     }
     else // MS IE
     {
       var range;

       try
       {
         range = ctrl.createTextRange();
       }
       catch(e)
       {
         try
         {
           range = document.body.createTextRange();
           range.moveToElementText(ctrl);
         }
         catch(e)
         {
           range = null;
         }
       }

       if(!range) return;

       range.collapse(true);
       range.moveStart("character", start);
       range.moveEnd("character", end - start);
       range.select();
     }

     insertionS = start;
     insertionE = end;
   }

   function deleteSelection(ctrl)
   {
     if(insertionS == insertionE) return;

     var tmp = (document.selection && !window.opera) ? ctrl.value.replace(/\r/g,"") : ctrl.value;
     ctrl.value = tmp.substring(0, insertionS) + tmp.substring(insertionE, tmp.length);

     setRange(ctrl, insertionS, insertionS);
   }

   function deleteAtCaret(ctrl)
   {
     // if(insertionE < insertionS) insertionE = insertionS;
     if(insertionS != insertionE)
     {
       deleteSelection(ctrl);
       return;
     }

     if(insertionS == insertionE)
       insertionS = insertionS - 1;

     var tmp = (document.selection && !window.opera) ? ctrl.value.replace(/\r/g,"") : ctrl.value;
     ctrl.value = tmp.substring(0, insertionS) + tmp.substring(insertionE, tmp.length);

     setRange(ctrl, insertionS, insertionS);
   }

   // This function inserts text at the caret position:
   //
   function insertAtCaret(ctrl, val)
   {
     if(insertionS != insertionE) deleteSelection(ctrl);

     if(gecko && document.createEvent && !window.opera)
     {
       var e = document.createEvent("KeyboardEvent");

       if(e.initKeyEvent && ctrl.dispatchEvent)
       {
         e.initKeyEvent("keypress",        // in DOMString typeArg,
                        false,             // in boolean canBubbleArg,
                        true,              // in boolean cancelableArg,
                        null,              // in nsIDOMAbstractView viewArg, specifies UIEvent.view. This value may be null;
                        false,             // in boolean ctrlKeyArg,
                        false,             // in boolean altKeyArg,
                        false,             // in boolean shiftKeyArg,
                        false,             // in boolean metaKeyArg,
                        null,              // key code;
                        val.charCodeAt(0));// char code.

         ctrl.dispatchEvent(e);
       }
     }
     else
     {
       var tmp = (document.selection && !window.opera) ? ctrl.value.replace(/\r/g,"") : ctrl.value;
       ctrl.value = tmp.substring(0, insertionS) + val + tmp.substring(insertionS, tmp.length);
     }

     setRange(ctrl, insertionS + val.length, insertionS + val.length);
   }

 //--></SCRIPT></HEAD>

 <BODY>
  <P style="font-family:Tahoma;font-size:14px">Virtual keyboard test #7: specifying keyboard size with any CSS length unit.</P>

  <TABLE border="0" width="60%">
   <TR>
     <TD width="100px"><TEXTAREA id="textfield" rows="12" cols="50" onkeyup="getCaretPositions(this);" onclick="getCaretPositions(this);"></TEXTAREA></TD>

     <TD width="10px"></TD><TD><DIV width="50px" align="justify">VKeyboard font size is set to '1em'. View page source to see how it works.</DIV></TD>
   </TR>
  </TABLE>

  <!-- VKeyboard font size is set to 1em. Change to see the effect. -->

  <P><A href="javascript:keyb_change('1em')" onclick="javascript:blur()" id="switch" style="font-family:Tahoma;font-size:14px;text-decoration:none;border-bottom: 1px dashed #0000F0;color:#0000F0">Show keyboard</A></P>

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

</BODY></HTML>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Freelance software engineer
Russian Federation Russian Federation
Dmitry Khudorozhkov began programming (and gaming) with his ZX Spectrum in 1989. Having seen and used all IBM PCs from early XT to the latest x64 machines, now Dmitry is a freelance programmer, living in Moscow, Russia. He is a graduate of the Moscow State Institute of Electronics and Mathematics (Applied Mathematics).

He is proficient in:

- C/C++ - more that 9 years of experience. Pure Win32 API/MFC desktop programming, networking (BSD/Win sockets), databases (primarily SQLite), OpenGL;

- JavaScript - more that 6 years of experience. Client-side components, AJAX, jQuery installation and customization;

- Firefox extensions (immediatelly ready for addons.mozilla.org reviewing) and Greasemonkey scripts. As an example of extensions Dmitry made you can search for FoxyPrices or WhatBird Winged Toolbar;

- XML and it's applications (last 2 years): XSLT (+ XPath), XSD, SVG, VML;

- ASP.NET/C# (webservices mostly);

Also familiar with (= entry level):

- PHP;

- HTML/CSS slicing.

Trying to learn:

- Ruby/Ruby-on-Rails;

- Czech language.

If you wish to express your opinion, ask a question or report a bug, feel free to e-mail:dmitrykhudorozhkov@yahoo.com. Job offers are warmly welcome.

If you wish to donate - and, by doing so, support further development - you can send Dmitry a bonus through the Rentacoder.com service (registration is free, Paypal is supported). Russian users can donate to the Yandex.Money account 41001132298694.

-

Comments and Discussions