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 />© 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}