1/**
2 * $Id: virtualkeyboard.js 175 2007-01-13 01:47:56Z wingedfox $
3 * $HeadURL: https://svn.debugger.ru/repos/jslibs/Virtual%20Keyboard/tags/VirtualKeyboard.v3.0b2/virtualkeyboard.js $
4 *
5 * Virtual Keyboard.
6 * (C) 2006 Vladislav SHCHapov, phprus@gmail.com
7 * (C) 2006 Ilya Lebedev <ilya@lebedev.net>
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
13 * See http://www.gnu.org/copyleft/lesser.html
14 *
15 * Do not remove this comment if you want to use script!
16 * �� �������� ������ �����������, ���� �� ������ ������������ ������!
17 *
18 * @author Vladislav SHCHapov <phprus@gmail.com>
19 * @author Ilya Lebedev <ilya@lebedev.net>
20 * @version $Rev: 175 $
21 * @modified Yuriy Nasretdinov
22 * @lastchange $Author: wingedfox $ $Date: 2007-01-13 04:47:56 +0300 (Сбт, 13 Янв 2007) $
23 */
24/*
25*  The Virtual Keyboard
26*
27*  @class VirtualKeyboard
28*  @constructor
29*/
30var VirtualKeyboard = new function () {
31  var self = this;
32  this.$VERSION$ = " $HeadURL: https://svn.debugger.ru/repos/jslibs/Virtual%20Keyboard/tags/VirtualKeyboard.v3.0b2/virtualkeyboard.js $ ".match(/\/[^\.]*[\.\/]([^\/]+)\/[\w\.\s$]+$/)[1]+"."+(" $Rev: 175 $ ".replace(/\D/g,""));
33  /*
34  *  ID prefix
35  *
36  *  @type String
37  *  @access private
38  */
39  var idPrefix = 'kb_b';
40  /**
41   *  Keyboard keys mapping, as on the keyboard
42   *
43   *  @type Array
44   *  @scope private
45   */
46  var keymap = [192,49,50,51,52,53,54,55,56,57,48,189,187,220,8, // ~ to BS
47                9,81,87,69,82,84,89,85,73,79,80,219,221,13,      // TAB to ENTER
48                20,65,83,68,70,71,72,74,75,76,186,222,           // CAPS to '
49                16,90,88,67,86,66,78,77,188,190,191,16,          // SHIFT to SHIFT
50                46,18,32,18];                                    // Delete, Alt, SPACE, Alt
51//                17,18,32,18,17,                                 // CTRL to CTRL
52//                46];                                            // Delete
53  if (navigator.product && 'gecko' == navigator.product.toLowerCase()) {
54    keymap[11] = 109;
55    keymap[12] = 61;
56    keymap[39] = 59;
57  }
58  /**
59   *  Keyboard mode, bitmap
60   *
61   *
62   *
63   *
64   *  @type Number
65   *  @scope private
66   */
67  var mode = 0
68     ,VK_NORMAL = 0
69     ,VK_SHIFT = 1
70     ,VK_ALT = 2
71     ,VK_CTRL = 4
72     ,VK_CAPS = 8;
73  /**
74   *  Deadkeys, original and mofified characters
75   *
76   *  @see http://en.wikipedia.org/wiki/Dead_key
77   *  @see http://en.wikipedia.org/wiki/Combining_character
78   *  @type Array
79   *  @access private
80   */
81  var deadkeys = [
82    // greek tonos
83    ["\u0384", "\u03b1\u03ac \u03b5\u03ad \u03b9\u03af \u03bf\u03cc \u03b7\u03ae \u03c5\u03cd \u03c9\u03ce "+
84               "\u0391\u0386 \u0395\u0388 \u0399\u038a \u039f\u038c \u0397\u0389 \u03a5\u038e \u03a9\u038f"
85    ],
86    // greek dialytika tonos
87    ["\u0385", "\u03c5\u03b0 \u03b9\u0390"],
88    // acute accent
89    ["\xb4", "a\xe1 A\xc1 e\xe9 E\xc9 i\xed I\xcd o\xf3 O\xd3 u\xfa U\xda y\xfd Y\xdd "+
90             "c\u0107 C\u0106 l\u013a L\u0139 n\u0144 N\u0143 r\u0155 R\u0154 s\u015b S\u015a w\u1e83 W\u1e82 z\u017a Z\u0179"
91    ],
92    // diaeresis
93    ["\xa8", "a\xe4 A\xc4 e\xeb E\xcb i\xef I\xcf j\u0135 J\u0134 "+
94             "o\xf6 O\xd6 u\xfc U\xdc y\xff Y\u0178 w\u1e85 W\1e84 "+ //latin
95             "\u03c5\u03cb \u03b9\u03ca \u03a5\u03ab \u0399\u03aa"    //greek
96    ],
97    // circumflex
98    ["\x5e", "a\xe2 A\xc2 e\xea E\xca i\xee I\xce o\xf4 O\xd4 u\xfb U\xdb y\u0176 Y\u0177 "+
99             "c\u0109 C\u0108 h\u0125 H\u0124 g\u011d G\u011c s\u015d S\u015c w\0175 W\0174 "+ //latin
100             "\u0131\xee \u0130\xce " // dotless small i, capital I with dot above
101    ],
102    // grave
103    ["\x60", "a\xe0 A\xc0 e\xe8 E\xc8 i\xec I\xcc o\xf2 O\xd2 u\xf9 U\xd9 y\u1ef3 Y\u1ef2 w\u1e81 W\u1e80"],
104    // tilde
105    ["\x7e", "a\xe3 A\xc3 o\xf5 O\xd5 u\u0169 U\\u0168 n\xf1 N\xd1 y\u1ef8 Y\1ef7"],
106    // ring above
107    ["\xb0", "a\xe5 A\xc5 u\u016f U\u016e"],
108    // caron
109    ["\u02c7", "e\u011b E\u011a "+
110               "c\u010d C\u010c d\u010f D\u010e l\u013e L\u013d n\u0148 N\u0147 "+
111               "r\u0158 R\u0158 s\u0161 S\u0160 t\u0165 T\u0164 z\u017e Z\u017d"
112    ],
113    // ogonek
114    ["\u02db", "a\u0105 A\u0104 e\u0119 E\u0118 i\u012f I\u012e c\u010b C\u010a g\u0121 G\u0120 u\u0173 U\u0172"],
115    // dot above
116    ["\u02d9", "e\u0117 E\u0116 u0131i I\u0130 z\u017c Z\u017b"],
117    // breve
118    ["\u02d8", "a\u0103 A\u0102 e\u0115 E\u0114 o\0u14f O\0u14e G\u011f g\u011e"],
119    // double acute
120    ["\u02dd", "o\u0151 O\u0150 U\u0170 u\u0171"],
121    // cedilla
122    ["\xb8", "c\xe7 C\xc7 g\u0123 G\u0122 k\u0137 K\u0136 l\u013c L\u013b "+
123             "n\u0146 N\u0145 r\u0157 R\u0156 S\u015e s\u015f T\u0162 t\u0163"
124    ]
125  ]
126  /*
127  *  CSS classes will be used to style buttons
128  *
129  *  @type Object
130  *  @access private
131  */
132  var cssClasses = {
133    'buttonUp'      : 'kbButton',
134    'buttonDown'    : 'kbButtonDown',
135    'buttonHover'   : 'kbButtonHover',
136    'buttonNormal'  : 'normal',
137    'buttonShifted' : 'shifted',
138    'buttonAlted'   : 'alted',
139    'capslock'      : 'capsLock',
140    'deadkey'       : 'deadKey'
141  }
142  /*
143  *  current layout
144  *
145  *  @type Object
146  *  @access public
147  */
148  var lang = null;
149  /*
150  *  Available layouts
151  *
152  *  Array contains layout, it's 'shifted' difference and name
153  *  Structure:
154  *   [
155  *    ['alpha' : Array, // key codes
156  *     'diff' : Object { <start1> : Array, // array of symbols, could not be taken with toUpperCase
157  *                       <start2> : Array,
158  *                     }
159  *    ].name=<layout_code>,
160  *    {...}
161  *   ].name = <lang_code>
162  *
163  *  @type Object
164  *  @access private
165  */
166  var layout = {}
167  /*
168  *  Shortcuts to the nodes
169  *
170  *  @type Object
171  *  @access private
172  */
173  var nodes = {
174      keyboard : null     // Keyboard container @type HTMLDivElement
175     ,desk : null         // Keyboard desk @type HTMLDivElement
176     ,langbox : null      // Layout selector @type HTMLSelectElement
177     ,attachedInput : null// Field, keyboard attached to
178  }
179  /*
180  *  Key code to be inserted on the keypress
181  *
182  *  @type Number
183  *  @access private
184  */
185  var newKeyCode = null;
186
187  /**************************************************************************
188  **  KEYBOARD LAYOUT
189  **************************************************************************/
190  /*
191  *  Remove layout from the list
192  *
193  *  @param {String} layout code
194  *  @return {Boolean} removal state
195  *  @access public
196  */
197  this.removeLayout = function (code) {
198    if (!isString(code)) return false;
199    var pos = 0;
200    for (var i in layout) {
201      if (!layout.hasOwnProperty(i) || !layout[i].hasOwnProperty(code)) continue;
202      /*
203      *  if we have only 1 layout available don't do that;
204      */
205      if (1==nodes.lytbox.getOptionsCount() && 1==nodes.langbox.getOptionsCount()) return false;
206
207      if (nodes.lytbox.getValue() == code) {
208          self.setNextLayout();
209          nodes.lytbox.removeSelectedOptions(code,'exact');
210      }
211      if (!nodes.lytbox.getOptionsCount()) {
212          self.setNextLang();
213          nodes.langbox.removeSelectedOptions(i,'exact');
214      }
215      delete (layout[pos]);
216      return true;
217    }
218    return false;
219  }
220  /**
221   *  Add layout to the list
222   *
223   *  @see layout
224   *  @param {String} layout code
225   *  @param {String} layout name
226   *  @param {Array} keycodes
227   *  @param {Object} differences for shift
228   *  @param {Object} differences for alt
229   *  @param {Array} list of the present deadkeys
230   *  @return {Boolean}
231   *  @scope public
232   */
233  this.addLayout = function(code, name, alpha, diff, alt, deadkeys) {
234      if (!isString(code)) throw new Error ('VirtualKeyboard.addLayout requires first parameter to be a string.');
235      if (!isString(name)) throw new Error ('VirtualKeyboard.addLayout requires second parameter to be a string.')
236      if (isEmpty(alt)) alt = {};
237      if (isEmpty(diff)) diff = {};
238      if (isUndefined(deadkeys)) deadkeys = [];
239
240      /*
241      *  trick to decode possible HTML entities
242      */
243      var span = document.createElement('span');
244      span.innerHTML = code;
245      code = span.firstChild.nodeValue.toUpperCase();
246      span.innerHTML = name;
247      name = span.firstChild.nodeValue;
248      if (!isArray(alpha) || 47!=alpha.length) throw new Error ('VirtualKeyboard.addLayout requires 3rd parameter to be an array with 47 items. Layout code: '+code+', layout title: '+name);
249
250      /*
251      *  add language, if it does not exists
252      */
253      if (!layout.hasOwnProperty(code)) {
254        layout[code] = {};
255        nodes.langbox.addOption(code, code, false, false, true);
256      }
257
258      /*
259      *  convert layout in machine-aware form
260      */
261      var ca = null
262         ,cac = -1
263         ,cs = null
264         ,csc = -1
265         ,lt = []
266
267      for (var i=0, aL = alpha.length; i<aL; i++) {
268         if (diff.hasOwnProperty(i)) {
269           cs = diff[i];
270           csc = i;
271         }
272         if (alt.hasOwnProperty(i)) {
273           ca = alt[i];
274           cac = i;
275         }
276         lt[i] = [alpha[i],                                          // normal chars
277                  (csc>-1&&cs.hasOwnProperty(i-csc)?cs[i-csc]:null), // shift chars
278                  (cac>-1&&ca.hasOwnProperty(i-cac)?ca[i-cac]:null)  // alt chars
279                 ];
280      }
281      /*
282      *  add control keys
283      */
284      lt.splice(14,0,'backspace');
285      lt.splice(15,0,'tab');
286      lt.splice(28,0,'enter');
287      lt.splice(29,0,'caps');
288      lt.splice(41,0,'shift_left');
289      lt.splice(52,0,'shift_right');
290      lt.splice(53,0,'del');
291//      lt.splice(54,0,'ctrl_left');
292      lt.splice(54,0,'alt_left');
293      lt.splice(55,0,'space');
294      lt.splice(56,0,'alt_right');
295//      lt.splice(57,0,'ctrl_right');
296
297      lt.dk = deadkeys;
298
299      layout[code][name] = lt;
300
301      return true;
302  }
303  /**
304   *  Set current layout
305   *
306   *  @param {String} language code
307   *  @param {String} layout code
308   *  @return {Boolean} change state
309   *  @access public
310   */
311  this.switchLayout = function (code, name) {
312    if (null == code) code = nodes.langbox.getValue();
313    if (!layout.hasOwnProperty(code) || (name && lang==layout[code][name])) return false;
314    /*
315    *  select another language, if current is not the same as new
316    */
317    if (nodes.langbox.getValue() != code) nodes.langbox.selectOnlyMatchingOptions(code,'exact');
318    /*
319    *  force layouts removal, becase switchLayout could be called outside keyboard
320    */
321    nodes.lytbox.removeAllOptions();
322    for (var i in layout[code]) {
323        if (layout[code].hasOwnProperty(i)) nodes.lytbox.addOption(i,i,false,false,true);
324    }
325    if (!name || !nodes.lytbox.selectOnlyMatchingOptions(name,'exact')) {
326        nodes.lytbox.selectOption(0);
327        name = nodes.lytbox.getValue();
328    }
329
330    if (!layout[code].hasOwnProperty(name)) return false;
331    /*
332    *  we will use old but quick innerHTML
333    */
334    var btns = ""
335       ,i
336       ,zcnt = 0;
337       lang = layout[code][name];
338    for (i=0, aL = lang.length; i<aL; i++) {
339      var chr = lang[i];
340      btns +=  "<div id=\""+idPrefix+(isArray(chr)?zcnt++:chr)
341              +"\" class=\""+cssClasses['buttonUp']
342              +"\"><a href=\"#"+i+"\""
343              +">"+(isArray(chr)?(__getCharHtmlForKey(lang,chr[0],cssClasses['buttonNormal'])
344                                 +__getCharHtmlForKey(lang,chr[1],cssClasses['buttonShifted'])
345                                 +__getCharHtmlForKey(lang,chr[2],cssClasses['buttonAlted']))
346                                :"")
347              +"</a></div>";
348    }
349    nodes.desk.innerHTML = btns;
350    /*
351    *  restore capslock state
352    */
353    var caps = document.getElementById(idPrefix+'caps');
354    if (caps && mode&VK_CAPS) {
355      caps.className += ' '+cssClasses['buttonDown'];
356    }
357    /*
358    *  restore shift state
359    */
360    var shift = document.getElementById(idPrefix+'shift_left');
361    if (shift && mode&VK_SHIFT) {
362      shift.className += ' '+cssClasses['buttonDown'];
363      shift = document.getElementById(idPrefix+'shift_right');
364      shift.className += ' '+cssClasses['buttonDown'];
365      this.toggleLayoutMode();
366   }
367  }
368  /**
369   *  Toggles layout mode (switch alternative key bindings)
370   *
371   *  @param {String} a1 key suffix to be checked
372   *  @param {Number} a2 keyboard mode
373   *  @access private
374   */
375  this.toggleLayoutMode = function (a1,a2) {
376    if (a1 && a2) {
377        /*
378        *  toggle keys, it's needed, really
379        */
380        var s1 = document.getElementById(idPrefix+a1+'_left')
381           ,s2 = document.getElementById(idPrefix+a1+'_right')
382        if (mode&a2) {
383            mode = mode ^ a2;
384            s1.className = s2.className = s1.className.replace (new RegExp("\\s*\\b"+cssClasses['buttonDown']+"\\b","g"),'');
385        } else {
386            mode = mode | a2;
387            s1.className = s2.className = s1.className+" "+cssClasses['buttonDown'];
388        }
389    }
390    /*
391    *  now, process to layout toggle
392    */
393    var bi = -1
394       /*
395       *  0 - normal keys
396       *  1 - shift keys
397       *  2 - alt keys (has priority, when it pressed together with shift)
398       */
399       ,sh = Math.min(mode&(VK_ALT|VK_SHIFT),2);
400    for (var i=0, lL=lang.length; i<lL; i++) {
401        if (isString(lang[i])) continue;
402        bi++;
403        var btn = document.getElementById(idPrefix+bi).firstChild;
404        /*
405        *  swap symbols and its CSS classes
406        */
407        if (btn.childNodes.length>1) {
408            btn.childNodes.item(0).className = !sh||isEmpty(lang[i][sh])?cssClasses['buttonNormal']       // put in the 'active' position
409                                                                        :sh&1?cssClasses['buttonShifted'] // swap with shift
410                                                                             :cssClasses['buttonAlted']   // swap with alt
411            btn.childNodes.item(1).className = !sh?cssClasses['buttonShifted']      // put in the 'home' position
412                                                  :sh&1?cssClasses['buttonNormal']  // put in the 'active' position
413                                                       :cssClasses['buttonShifted'] // put in the 'home' position
414            btn.childNodes.item(2).className = !sh?cssClasses['buttonAlted']        // put in the 'home' position
415                                                  :sh&1?cssClasses['buttonAlted']   // put in the 'home' position
416                                                       :cssClasses['buttonNormal']  // put in the 'active' position
417        }
418    }
419  }
420  /*
421  *  Used to rotate langs (or set prefferred one, if legal code is specified)
422  *
423  *  @access private
424  */
425  this.setNextLang = function () {
426      nodes.langbox.selectNext(true);
427      self.switchLayout(nodes.langbox.getValue(),null);
428  }
429  /*
430  *  Used to rotate lang layouts
431  *
432  *  @access private
433  */
434  this.setNextLayout = function () {
435      nodes.lytbox.selectNext(true);
436      self.switchLayout(nodes.langbox.getValue(),nodes.lytbox.getValue());
437  }
438  /**
439   *  Return the list of the available layouts
440   *
441   *  @return {Array}
442   *  @scope public
443   */
444  this.getLayouts = function () {
445      var lts = [];
446      for (var i in layout) {
447        if (!layout.hasOwnProperty(i)) continue;
448        for (var z in layout[i]) {
449          if (!layout[i].hasOwnProperty(z)) continue;
450          lts[lts.length] = i+"\xa0-\xa0"+z;
451        }
452      }
453      return lts.sort();
454  }
455
456  //---------------------------------------------------------------------------
457  // GLOBAL EVENT HANDLERS
458  //---------------------------------------------------------------------------
459  /**
460   *  Do the key clicks, caught from both virtual and real keyboards
461   *
462   *  @param {HTMLInputElement} key on the virtual keyboard
463   *  @param {EventTarget} evt optional event object, to be used to re-map the keyCode
464   *  @access private
465   */
466  var _keyClicker_ = function (key, evt) {
467      var chr = ""
468         ,ret = false;
469      key = key.replace(idPrefix, "");
470
471      switch (key) {
472          case "caps" :
473          case "shift" :
474          case "shift_left" :
475          case "shift_right" :
476          case "alt" :
477          case "alt_left" :
478          case "alt_right" :
479              return;
480          case 'backspace':
481              /*
482              *  is char is in the buffer, or selection made, made decision at __charProcessor
483              */
484              if (DocumentSelection.getSelection(nodes.attachedInput))
485                  chr = "\x08";
486              else
487                  DocumentSelection.deleteAtCursor(nodes.attachedInput, false);
488              break;
489          case 'del':
490              DocumentSelection.deleteAtCursor(nodes.attachedInput, true);
491              break;
492          case 'space':
493              chr = " ";
494              break;
495          case 'tab':
496              chr = "\t";
497              break;
498          case 'enter':
499              chr = "\n";
500              break;
501          default:
502                  var el = document.getElementById(idPrefix+key);
503                  chr = (el.firstChild.childNodes[Math.min(mode&(VK_ALT|VK_SHIFT),2)].firstChild||
504                         el.firstChild.firstChild.firstChild).nodeValue;
505                  /*
506                  *  do uppercase if either caps or shift clicked, not both
507                  *  and only 'normal' key state is active
508                  */
509                  if (((mode & VK_SHIFT || mode & VK_CAPS) && (mode ^ (VK_SHIFT | VK_CAPS)))) chr = chr.toUpperCase();
510                  /*
511                  *  reset shift state, if clicked on the letter button
512                  */
513                  if (!(evt && evt.shiftKey) && mode&VK_SHIFT) {
514                      /*
515                      *  we need firstChild here and on other places to be sure that we point to 'a' node
516                      */
517                      document.getElementById(idPrefix+'shift_left').firstChild.fireEvent('onmousedown');
518                  }
519              break;
520      }
521      if (chr) {
522          /*
523          *  use behavior of real keyboard - replace selected text with new input
524          */
525          chr = __charProcessor(chr, DocumentSelection.getSelection(nodes.attachedInput));
526          if (chr[0].length < 2 && !chr[1]) {
527
528              try {
529                  /*
530                  *  IE allows to rewrite the key code
531                  */
532                  evt.keyCode = chr[0].charCodeAt(0);
533                  ret = true;
534              } catch (err) {
535                  try {
536                      /*
537                      *  Mozilla implements events interface mostly complete
538                      *  also, this code helps to keep input text in the view
539                      */
540                      var e = document.createEvent("KeyboardEvent");
541                      e.initKeyEvent(
542                                     "keypress",       //  in DOMString typeArg,
543                                     false,            //  in boolean canBubbleArg,
544                                     true,             //  in boolean cancelableArg,
545                                     null,             //  in nsIDOMAbstractView viewArg,  Specifies UIEvent.view. This value may be null.
546                                     false,            //  in boolean ctrlKeyArg,
547                                     false,            //  in boolean altKeyArg,
548                                     false,            //  in boolean shiftKeyArg,
549                                     false,            //  in boolean metaKeyArg,
550                                     chr[0].charCodeAt(0),chr[0].charCodeAt(0)
551                      );
552                      e.__bypass = true;
553                      nodes.attachedInput.dispatchEvent(e);
554                  } catch (err) {
555                      /*
556                      *  this is used at least by Opera9, because it neither support overwriting keyCode value
557                      *  nor KeyboardEvent creation
558                      */
559                      if (DocumentSelection.getStart(nodes.attachedInput) != DocumentSelection.getEnd(nodes.attachedInput))
560                          DocumentSelection.deleteAtCursor(nodes.attachedInput);
561                      DocumentSelection.insertAtCursor(nodes.attachedInput,chr[0]);
562                  }
563              }
564          } else {
565              /*
566              *  __charProcessor might return the char sequence
567              *  it could not be processed with the standard events, thus insert it manually
568              */
569              if (DocumentSelection.getStart(nodes.attachedInput) != DocumentSelection.getEnd(nodes.attachedInput))
570                  DocumentSelection.deleteAtCursor(nodes.attachedInput);
571              DocumentSelection.insertAtCursor(nodes.attachedInput,chr[0]);
572              /*
573              *  select as much, as __charProcessor callback requested
574              */
575              if (chr[1]) {
576                  /*
577                  *  settimeout is used to select text right after event handlers will insert new contents
578                  */
579                  DocumentSelection.setRange(nodes.attachedInput,-chr[1],0,true);
580              }
581          }
582      }
583      return ret;
584  }
585  /**
586   *  Captures some keyboard events
587   *
588   *  @param {Event} keydown
589   *  @access protected
590   */
591  var _keydownHandler_ = function(e) {
592    /*
593    *  it's global event handler. do not process event, if keyboard is closed
594    */
595    if (!self.isOpen()) return;
596    e = e || window.event;
597    /*
598    *  differently process different events
599    */
600    switch (e.type) {
601      case 'keydown' :
602        if (e.ctrlKey) mode = mode | VK_CTRL;
603
604        switch (e.keyCode) {
605          case 16://shift
606              if (!(mode&VK_SHIFT)) {
607                  self.toggleLayoutMode('shift', VK_SHIFT);
608              }
609              break;
610          case 18: //alt
611              if (e.altKey && !(mode&VK_ALT)) {
612                  self.toggleLayoutMode('alt', VK_ALT);
613              }
614              break;
615          case 20: //caps lock
616              mode = mode | VK_CAPS;
617              var cp = document.getElementById(idPrefix+'caps');
618              cp.className += ' '+cssClasses['buttonDown'];
619              break;
620          case 27:
621              VirtualKeyboard.close();
622              return false;
623          default:
624              /*
625              *  skip keypress if ctrl pressed
626              */
627              if (keymap.hasOwnProperty(e.keyCode) && !e.ctrlKey) {
628                  var el = nodes.desk.childNodes[keymap[e.keyCode]];
629                  el.className += " "+cssClasses['buttonDown'];
630                  /*
631                  *  assign the key code to be inserted on the keypress
632                  */
633                  newKeyCode = nodes.desk.childNodes[keymap[e.keyCode]].id.replace(idPrefix, "");
634              }
635              break;
636        }
637        break;
638      case 'keyup' :
639        /*
640        *  switch languages
641        */
642        if (!(mode ^ (VK_SHIFT | VK_CTRL))) {
643            self.setNextLang();
644        }
645        /*
646        *  switch layouts
647        */
648        if (!(mode ^ (VK_SHIFT | VK_ALT))) {
649            self.setNextLayout();
650        }
651        if (!e.ctrlKey && (mode & VK_CTRL)) mode = mode ^ VK_CTRL;
652        switch (e.keyCode) {
653            case 18:
654                self.toggleLayoutMode('alt', VK_ALT);
655                break;
656            case 16:
657                self.toggleLayoutMode('shift', VK_SHIFT);
658                break;
659            case 20:
660                mode = mode ^ VK_CAPS;
661                var cp = document.getElementById(idPrefix+'caps');
662                cp.className = cp.className.replace (new RegExp("\\s*\\b"+cssClasses['buttonDown']+"\\b","g"),'');
663                break;
664            default:
665                if (keymap.hasOwnProperty(e.keyCode)) {
666                    var el = nodes.desk.childNodes[keymap[e.keyCode]];
667                    el.className = el.className.replace(new RegExp("\\s*\\b"+cssClasses['buttonDown']+"\\b","g"),"");
668                }
669        }
670        break;
671      case 'keypress' :
672        /*
673        *  flag is set only when virtual key passed to input target
674        */
675        if (newKeyCode && !e.__bypass) {
676            if (!_keyClicker_(newKeyCode, e)) {
677                e.returnValue = false;
678                if (e.preventDefault) e.preventDefault();
679            }
680            /*
681            *  reset flag
682            */
683            newKeyCode = null;
684        }
685        return;
686    }
687    /*
688    *  do uppercase transformation
689    */
690    if ((mode & VK_SHIFT || mode & VK_CAPS) && (mode ^ (VK_SHIFT | VK_CAPS)))
691      nodes.desk.className += ' '+cssClasses['capslock'];
692    else
693      nodes.desk.className = nodes.desk.className.replace(new RegExp("\\s*\\b"+cssClasses['capslock']+"\\b","g"),"");
694  }
695  /*
696  *  Handle clicks on the buttons, actually used with mouseup event
697  *
698  *  @param {Event} mouseup event
699  *  @access protected
700  */
701  var _btnClick_ = function (e) {
702    /*
703    *  either a pressed key or something new
704    */
705    var el = DOM.getParent(e.srcElement||e.target,'a');
706    /*
707    *  skip invalid nodes
708    */
709    if (!el || el.parentNode.id.indexOf(idPrefix)<0) return;
710    el = el.parentNode;
711
712    switch (el.id.substring(idPrefix.length)) {
713      case "caps":
714      case "shift_left":
715      case "shift_right":
716      case "alt_left":
717      case "alt_right":
718          return;
719    }
720    el.className = el.className.replace(new RegExp("\\s*\\b"+cssClasses['buttonDown']+"\\b","g"),"");
721    _keyClicker_(el.id);
722  }
723  /*
724  *  Handle mousedown event
725  *
726  *  Method is used to set 'pressed' button state and toggle shift, if needed
727  *  Additionally, it is used by keyboard wrapper to forward keyboard events to the virtual keyboard
728  *
729  *  @param {Event} mousedown event
730  *  @access protected
731  */
732  var _btnMousedown_ = function (e) {
733    /*
734    *  either pressed key or something new
735    */
736    var el = DOM.getParent(e.srcElement||e.target, 'a');
737    /*
738    *  skip invalid nodes
739    */
740    if (!el || el.parentNode.id.indexOf(idPrefix)<0) return;
741    el = el.parentNode;
742    var key = el.id.substring(idPrefix.length);
743    switch (key) {
744      case "caps":
745        var cp = document.getElementById(idPrefix+'caps');
746        if (VK_CAPS & (mode = mode ^ VK_CAPS))
747          cp.className += ' '+cssClasses['buttonDown'];
748        else
749          cp.className = cp.className.replace (new RegExp("\\s*\\b"+cssClasses['buttonDown']+"\\b","g"),'');
750        break;
751      case "shift_left":
752      case "shift_right":
753        /*
754        *  Shift is pressed in on both keyboard and virtual keyboard, return
755        */
756        if (mode&VK_SHIFT && e.shiftKey) break;
757        self.toggleLayoutMode('shift', VK_SHIFT);
758        break;
759      case "alt_left":
760      case "alt_right":
761        /*
762        *  Alt is pressed in on both keyboard and virtual keyboard, return
763        */
764        if (mode&VK_ALT && e.altKey) break;
765        self.toggleLayoutMode('alt', VK_ALT);
766        break;
767      /*
768      *  any real pressed key
769      */
770      default:
771        el.className += ' '+cssClasses['buttonDown'];
772        return;
773    }
774    /*
775    *  do uppercase transformation
776    */
777    if ((mode & VK_SHIFT || mode & VK_CAPS) && (mode ^ (VK_SHIFT | VK_CAPS)))
778      nodes.desk.className += ' '+cssClasses['capslock'];
779    else
780      nodes.desk.className = nodes.desk.className.replace(new RegExp("\\s*\\b"+cssClasses['capslock']+"\\b","g"),"");
781  }
782  /*
783  *  Handle mouseout event
784  *
785  *  Method is used to remove 'pressed' button state
786  *
787  *  @param {Event} mouseup event
788  *  @access protected
789  */
790  var _btnMouseout_ = function (e) {
791    /*
792    *  either pressed key or something new
793    */
794    var el = DOM.getParent(e.srcElement||e.target, 'a');
795    /*
796    *  skip invalid nodes
797    */
798    if (!el || el.parentNode.id.indexOf(idPrefix)<0) return;
799    el = el.parentNode;
800
801    var cn = el.className.replace(new RegExp("\\s*\\b"+cssClasses['buttonHover']+"\\b","g"),"");
802    /*
803    *  hard-to-avoid IE bug cleaner. if 'hover' state is get removed, button looses it's 'down' state
804    *  should be applied for every button, needed to save 'pressed' state on mouseover/out
805    */
806    if (el.id.indexOf('shift')>-1) {
807      /*
808      *  both shift keys should be blurred
809      */
810      var s1 = document.getElementById(idPrefix+'shift_left'),
811          s2 = document.getElementById(idPrefix+'shift_right');
812      s1.className = s2.className = cn;
813    } else {
814      el.className = cn;
815    }
816  }
817  /*
818  *  Handle mouseover event
819  *
820  *  Method is used to remove 'pressed' button state
821  *
822  *  @param {Event} mouseup event
823  *  @access protected
824  */
825  var _btnMouseover_ = function (e) {
826    /*
827    *  either pressed key or something new
828    */
829    var el = DOM.getParent(e.srcElement||e.target, 'a');
830    /*
831    *  skip invalid nodes
832    */
833    if (!el || el.parentNode.id.indexOf(idPrefix)<0) return;
834    el = el.parentNode;
835    el.className += ' '+cssClasses['buttonHover'];
836    /*
837    *  both shift keys should be highlighted
838    */
839    if (el.id.indexOf('shift')>-1) {
840      var s1 = document.getElementById(idPrefix+'shift_left'),
841          s2 = document.getElementById(idPrefix+'shift_right');
842      s1.className = s2.className = el.className;
843    }
844  }
845  /*
846  *  blocks link behavior
847  *
848  *  @param {Event} event to be blocked
849  *  @access protected
850  */
851  var _blockLink_ = function (e) {
852    /*
853    *  either pressed key or something new
854    */
855    var el = DOM.getParent(e.srcElement||e.target, 'a');
856    if (!el) return;
857
858    if (e.preventDefault) e.preventDefault();
859    e.returnValue = false;
860    if (e.stopPropagation) e.stopPropagation();
861    e.cancelBubble = true;
862  }
863  /**********************************************************
864  *  MOST COMMON METHODS
865  **********************************************************/
866  /*
867  *  Used to attach keyboard output to specified input
868  *
869  *  @param {HTMLInputElement,String} element to attach keyboard to
870  *  @return attach state
871  *  @access public
872  */
873  self.attachInput = function (el) {
874    if ('string' == typeof el) el = document.getElementById(el);
875    /*
876    *  only inputable nodes are allowed
877    */
878    if (!el || !el.tagName || (el.tagName.toLowerCase() != 'input' && el.tagName.toLowerCase() != 'textarea')) return false;
879    nodes.attachedInput = el;
880    return nodes.attachedInput;
881  }
882  /*
883  *  Shows keyboard
884  *
885  *  @param {HTMLElement, String} input element or it to bind keyboard to
886  *  @param {String} holder keyboard holder container, keyboard won't have drag-drop when holder is specified
887  *  @param {HTMLElement} kpTarget optional target to bind key* event handlers to,
888  *                       is useful for frame and popup keyboard placement
889  *  @return {Boolean} operation state
890  *  @access public
891  */
892  self.show = function (input, holder, kpTarget){
893    if ( input && !(input = self.attachInput(input))
894      || !nodes.keyboard || !document.body || nodes.attachedInput == null) return false;
895    /*
896    *  check pass means that node is not attached to the body
897    */
898    if (!nodes.keyboard.parentNode || nodes.keyboard.parentNode.nodeType==11) {
899        if (isString(holder)) holder = document.getElementById(holder);
900        if (!holder.appendChild) return false;
901        holder.appendChild(nodes.keyboard);
902        self.switchLayout(nodes.langbox.getValue(), nodes.lytbox.getValue())
903        /*
904        *  we'll bind event handler here
905        */
906        if (!input.attachEvent) input.attachEvent = nodes.desk.attachEvent;
907        input.attachEvent('onkeydown', _keydownHandler_);
908        input.attachEvent('onkeyup', _keydownHandler_);
909        input.attachEvent('onkeypress', _keydownHandler_);
910        if (!isUndefined(kpTarget) && input != kpTarget && kpTarget.appendChild) {
911            if (!kpTarget.attachEvent) kpTarget.attachEvent = nodes.desk.attachEvent;
912            kpTarget.attachEvent('onkeydown', _keydownHandler_);
913            kpTarget.attachEvent('onkeyup', _keydownHandler_);
914            kpTarget.attachEvent('onkeypress', _keydownHandler_);
915        }
916    }
917    /*
918    *  special, for IE
919    */
920    setTimeout(function(){nodes.keyboard.style.display = 'block';},1);
921
922    return true;
923  }
924  /**
925   *  Closes the keyboard
926   *
927   *  @return {Boolean}
928   *  @scope public
929   */
930  self.close = function () {
931    if (!nodes.keyboard || !self.isOpen()) return false;
932    nodes.keyboard.style.display = 'none';
933    nodes.attachedInput = null;
934    return true;
935  }
936  /**
937   *  Returns true if keyboard is opened
938   *
939   *  @return {Boolean}
940   *  @scope public
941   */
942  self.isOpen = function () /* :Boolean */ {
943      return nodes.keyboard.style.display == 'block';
944  }
945  //---------------------------------------------------------------------------
946  // PRIVATE METHODS
947  //---------------------------------------------------------------------------
948  /**
949   *  Char processor
950   *
951   *  It does process input letter, possibly modifies it
952   *
953   *  @param {String} char letter to be processed
954   *  @param {String} buf current keyboard buffer
955   *  @return {Array} new char, flag keep buffer contents
956   *  @scope private
957   */
958  var __charProcessor = function (tchr, buf) {
959    var res = [];
960    if (isFunction(lang.dk)) {
961      /*
962      *  call user-supplied converter
963      */
964      res = lang.dk.call(self,tchr,buf);
965    } else if (tchr == "\x08") {
966      res = ['',0];
967    } else {
968      /*
969      *  process char in buffer first
970      *  buffer size should be exactly 1 char to don't mess with the occasional selection
971      */
972      var fc = buf.charAt(0);
973      if ( buf.length==1 && lang.dk.indexOf(fc.charCodeAt(0))>-1 ) {
974        /*
975        *  dead key found, no more future processing
976        *  if new key is not an another deadkey
977        */
978        res[1] = tchr != fc & lang.dk.indexOf(tchr.charCodeAt(0))>-1;
979        res[0] = deadkeys[fc][tchr]?deadkeys[fc][tchr]:tchr;
980      } else {
981        /*
982        *  in all other cases, process char as usual
983        */
984        res[1] = deadkeys.hasOwnProperty(tchr);
985        res[0] = tchr;
986      }
987    }
988    return res;
989  }
990  /**
991   *  Char html constructor
992   *
993   *  @param {String} chr char code
994   *  @param {String} css optional additional class names
995   *  @return {String} resulting html
996   *  @scope private
997   */
998  var __getCharHtmlForKey = function (lyt, chr, css) {
999      /*
1000      *  if char exists
1001      */
1002      var html = [];
1003      /*
1004      *  if key matches agains current deadchar list
1005      */
1006      if (!isFunction(lyt.dk) && lyt.dk.indexOf(chr)>-1) css = [css, cssClasses['deadkey']].join(" ");
1007      html[html.length] = "<span ";
1008      if (css) {
1009          html[html.length] = "class=\"";
1010          html[html.length] = css;
1011          html[html.length] = "\"";
1012      }
1013      html[html.length] = ">"+(parseInt(chr)?String.fromCharCode(chr):"")+"</span>"
1014    return html.join("");
1015  }
1016  /**
1017   *  Keyboard constructor
1018   *
1019   *  @access public
1020   */
1021  var __construct = function() {
1022    /*
1023    *  process the deadkeys, to make better useable, but non-editable object
1024    */
1025    var dk = {};
1026    for (var i=0, dL=deadkeys.length; i<dL; i++) {
1027      if (!deadkeys.hasOwnProperty(i)) continue;
1028      /*
1029      *  got correct deadkey symbol
1030      */
1031      dk[deadkeys[i][0]] = {};
1032      var chars = deadkeys[i][1].split(" ");
1033      /*
1034      *  process char:mod_char pairs
1035      */
1036      for (var z=0, cL=chars.length; z<cL; z++) {
1037        dk[deadkeys[i][0]][chars[z].charAt(0)] = chars[z].charAt(1);
1038      }
1039    }
1040    /*
1041    *  resulting array:
1042    *
1043    *  { '<dead_char>' : { '<key>' : '<modification>', }
1044    */
1045    deadkeys = dk;
1046
1047    /*
1048    *  convert keymap array to the object, to have better typing speed
1049    */
1050    var tk = keymap;
1051    keymap = [];
1052    for (var i=0, kL=tk.length; i<kL; i++) {
1053        keymap[tk[i]] = i;
1054    }
1055    tk = null;
1056    /*
1057    *  create keyboard UI
1058    */
1059    nodes.keyboard = document.createElementExt('div',{'param' : { 'id' : 'virtualKeyboard'} });
1060    nodes.desk = document.createElementExt('div',{'param' : { 'id' : 'kbDesk'} });
1061    nodes.keyboard.appendChild(nodes.desk);
1062    /*
1063    *  reference to layout selector
1064    */
1065    nodes.langbox = new Selectbox();
1066    nodes.langbox.getEl().onchange = function(){self.switchLayout(this.value,0)};
1067    nodes.langbox.getEl().id = 'kb_langselector';
1068    nodes.lytbox = new Selectbox();
1069    nodes.lytbox.getEl().onchange = function(){self.switchLayout(null,this.value)};
1070    nodes.lytbox.getEl().id = 'kb_layoutselector';
1071    nodes.keyboard.appendChild(nodes.langbox.getEl());
1072    nodes.keyboard.appendChild(nodes.lytbox.getEl());
1073
1074    /*
1075    *  insert some copyright information
1076    */
1077    var copy = document.createElementExt('div',{'param' : { 'id' : 'copyrights'
1078                                                           ,'nofocus' : 'true'
1079                                                           ,'innerHTML' : '<a href="http://debugger.ru/projects/virtualkeyboard" target="_blank">VirtualKeyboard '+self.$VERSION$+'</a><br />&copy; 2006-2007 <a href="http://debugger.ru" target="_blank">Debugger.ru</a>'
1080                                                        }
1081                                               }
1082                                        );
1083    nodes.keyboard.appendChild(copy);
1084    nodes.desk.attachEvent('onmousedown', _btnMousedown_);
1085    nodes.desk.attachEvent('onmouseup', _btnClick_);
1086    nodes.desk.attachEvent('onmouseover', _btnMouseover_);
1087    nodes.desk.attachEvent('onmouseout', _btnMouseout_);
1088    nodes.desk.attachEvent('onclick', _blockLink_);
1089    nodes.desk.attachEvent('ondragstart', _blockLink_);
1090
1091  }
1092  /*
1093  *  call the constructor
1094  */
1095  __construct();
1096}