1 2 /* ***** BEGIN LICENSE BLOCK ***** 3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 4 * 5 * The contents of this file are subject to the Mozilla Public License Version 6 * 1.1 (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * http://www.mozilla.org/MPL/ 9 * 10 * Software distributed under the License is distributed on an "AS IS" basis, 11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 12 * for the specific language governing rights and limitations under the 13 * License. 14 * 15 * The Original Code is guac-common-js. 16 * 17 * The Initial Developer of the Original Code is 18 * Michael Jumper. 19 * Portions created by the Initial Developer are Copyright (C) 2010 20 * the Initial Developer. All Rights Reserved. 21 * 22 * Contributor(s): 23 * 24 * Alternatively, the contents of this file may be used under the terms of 25 * either the GNU General Public License Version 2 or later (the "GPL"), or 26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 27 * in which case the provisions of the GPL or the LGPL are applicable instead 28 * of those above. If you wish to allow use of your version of this file only 29 * under the terms of either the GPL or the LGPL, and not to allow others to 30 * use your version of this file under the terms of the MPL, indicate your 31 * decision by deleting the provisions above and replace them with the notice 32 * and other provisions required by the GPL or the LGPL. If you do not delete 33 * the provisions above, a recipient may use your version of this file under 34 * the terms of any one of the MPL, the GPL or the LGPL. 35 * 36 * ***** END LICENSE BLOCK ***** */ 37 38 /** 39 * Namespace for all Guacamole JavaScript objects. 40 * @namespace 41 */ 42 var Guacamole = Guacamole || {}; 43 44 /** 45 * Dynamic on-screen keyboard. Given the URL to an XML keyboard layout file, 46 * this object will download and use the XML to construct a clickable on-screen 47 * keyboard with its own key events. 48 * 49 * @constructor 50 * @param {String} url The URL of an XML keyboard layout file. 51 */ 52 Guacamole.OnScreenKeyboard = function(url) { 53 54 var on_screen_keyboard = this; 55 56 /** 57 * State of all modifiers. This is the bitwise OR of all active modifier 58 * values. 59 * 60 * @private 61 */ 62 var modifiers = 0; 63 64 var scaledElements = []; 65 66 var modifier_masks = {}; 67 var next_mask = 1; 68 69 /** 70 * Adds a class to an element. 71 * 72 * @private 73 * @function 74 * @param {Element} element The element to add a class to. 75 * @param {String} classname The name of the class to add. 76 */ 77 var addClass; 78 79 /** 80 * Removes a class from an element. 81 * 82 * @private 83 * @function 84 * @param {Element} element The element to remove a class from. 85 * @param {String} classname The name of the class to remove. 86 */ 87 var removeClass; 88 89 /** 90 * The number of mousemove events to require before re-enabling mouse 91 * event handling after receiving a touch event. 92 */ 93 this.touchMouseThreshold = 3; 94 95 /** 96 * Counter of mouse events to ignore. This decremented by mousemove, and 97 * while non-zero, mouse events will have no effect. 98 * @private 99 */ 100 var ignore_mouse = 0; 101 102 // Ignore all pending mouse events when touch events are the apparent source 103 function ignorePendingMouseEvents() { ignore_mouse = on_screen_keyboard.touchMouseThreshold; } 104 105 // If Node.classList is supported, implement addClass/removeClass using that 106 if (Node.classList) { 107 108 /** @ignore */ 109 addClass = function(element, classname) { 110 element.classList.add(classname); 111 }; 112 113 /** @ignore */ 114 removeClass = function(element, classname) { 115 element.classList.remove(classname); 116 }; 117 118 } 119 120 // Otherwise, implement own 121 else { 122 123 /** @ignore */ 124 addClass = function(element, classname) { 125 126 // Simply add new class 127 element.className += " " + classname; 128 129 }; 130 131 /** @ignore */ 132 removeClass = function(element, classname) { 133 134 // Filter out classes with given name 135 element.className = element.className.replace(/([^ ]+)[ ]*/g, 136 function(match, testClassname, spaces, offset, string) { 137 138 // If same class, remove 139 if (testClassname == classname) 140 return ""; 141 142 // Otherwise, allow 143 return match; 144 145 } 146 ); 147 148 }; 149 150 } 151 152 // Returns a unique power-of-two value for the modifier with the 153 // given name. The same value will be returned for the same modifier. 154 function getModifierMask(name) { 155 156 var value = modifier_masks[name]; 157 if (!value) { 158 159 // Get current modifier, advance to next 160 value = next_mask; 161 next_mask <<= 1; 162 163 // Store value of this modifier 164 modifier_masks[name] = value; 165 166 } 167 168 return value; 169 170 } 171 172 function ScaledElement(element, width, height, scaleFont) { 173 174 this.width = width; 175 this.height = height; 176 177 this.scale = function(pixels) { 178 element.style.width = (width * pixels) + "px"; 179 element.style.height = (height * pixels) + "px"; 180 181 if (scaleFont) { 182 element.style.lineHeight = (height * pixels) + "px"; 183 element.style.fontSize = pixels + "px"; 184 } 185 } 186 187 } 188 189 // For each child of element, call handler defined in next 190 function parseChildren(element, next) { 191 192 var children = element.childNodes; 193 for (var i=0; i<children.length; i++) { 194 195 // Get child node 196 var child = children[i]; 197 198 // Do not parse text nodes 199 if (!child.tagName) 200 continue; 201 202 // Get handler for node 203 var handler = next[child.tagName]; 204 205 // Call handler if defined 206 if (handler) 207 handler(child); 208 209 // Throw exception if no handler 210 else 211 throw new Error( 212 "Unexpected " + child.tagName 213 + " within " + element.tagName 214 ); 215 216 } 217 218 } 219 220 // Create keyboard 221 var keyboard = document.createElement("div"); 222 keyboard.className = "guac-keyboard"; 223 224 // Retrieve keyboard XML 225 var xmlhttprequest = new XMLHttpRequest(); 226 xmlhttprequest.open("GET", url, false); 227 xmlhttprequest.send(null); 228 229 var xml = xmlhttprequest.responseXML; 230 231 if (xml) { 232 233 function parse_row(e) { 234 235 var row = document.createElement("div"); 236 row.className = "guac-keyboard-row"; 237 238 parseChildren(e, { 239 240 "column": function(e) { 241 row.appendChild(parse_column(e)); 242 }, 243 244 "gap": function parse_gap(e) { 245 246 // Create element 247 var gap = document.createElement("div"); 248 gap.className = "guac-keyboard-gap"; 249 250 // Set gap size 251 var gap_units = 1; 252 if (e.getAttribute("size")) 253 gap_units = parseFloat(e.getAttribute("size")); 254 255 scaledElements.push(new ScaledElement(gap, gap_units, gap_units)); 256 row.appendChild(gap); 257 258 }, 259 260 "key": function parse_key(e) { 261 262 // Create element 263 var key_element = document.createElement("div"); 264 key_element.className = "guac-keyboard-key"; 265 266 // Append class if specified 267 if (e.getAttribute("class")) 268 key_element.className += " " + e.getAttribute("class"); 269 270 // Position keys using container div 271 var key_container_element = document.createElement("div"); 272 key_container_element.className = "guac-keyboard-key-container"; 273 key_container_element.appendChild(key_element); 274 275 // Create key 276 var key = new Guacamole.OnScreenKeyboard.Key(); 277 278 // Set key size 279 var key_units = 1; 280 if (e.getAttribute("size")) 281 key_units = parseFloat(e.getAttribute("size")); 282 283 key.size = key_units; 284 285 parseChildren(e, { 286 "cap": function parse_cap(e) { 287 288 // TODO: Handle "sticky" attribute 289 290 // Get content of key cap 291 var content = e.textContent || e.text; 292 293 // If read as blank, assume cap is a single space. 294 if (content.length == 0) 295 content = " "; 296 297 // Get keysym 298 var real_keysym = null; 299 if (e.getAttribute("keysym")) 300 real_keysym = parseInt(e.getAttribute("keysym")); 301 302 // If no keysym specified, try to get from key content 303 else if (content.length == 1) { 304 305 var charCode = content.charCodeAt(0); 306 if (charCode >= 0x0000 && charCode <= 0x00FF) 307 real_keysym = charCode; 308 else if (charCode >= 0x0100 && charCode <= 0x10FFFF) 309 real_keysym = 0x01000000 | charCode; 310 311 } 312 313 // Create cap 314 var cap = new Guacamole.OnScreenKeyboard.Cap(content, real_keysym); 315 316 if (e.getAttribute("modifier")) 317 cap.modifier = e.getAttribute("modifier"); 318 319 // Create cap element 320 var cap_element = document.createElement("div"); 321 cap_element.className = "guac-keyboard-cap"; 322 cap_element.textContent = content; 323 key_element.appendChild(cap_element); 324 325 // Append class if specified 326 if (e.getAttribute("class")) 327 cap_element.className += " " + e.getAttribute("class"); 328 329 // Get modifier value 330 var modifierValue = 0; 331 if (e.getAttribute("if")) { 332 333 // Get modifier value for specified comma-delimited 334 // list of required modifiers. 335 var requirements = e.getAttribute("if").split(","); 336 for (var i=0; i<requirements.length; i++) { 337 modifierValue |= getModifierMask(requirements[i]); 338 addClass(cap_element, "guac-keyboard-requires-" + requirements[i]); 339 addClass(key_element, "guac-keyboard-uses-" + requirements[i]); 340 } 341 342 } 343 344 // Store cap 345 key.modifierMask |= modifierValue; 346 key.caps[modifierValue] = cap; 347 348 } 349 }); 350 351 scaledElements.push(new ScaledElement(key_container_element, key_units, 1, true)); 352 row.appendChild(key_container_element); 353 354 // Set up click handler for key 355 function press() { 356 357 // Press key if not yet pressed 358 if (!key.pressed) { 359 360 addClass(key_element, "guac-keyboard-pressed"); 361 362 // Get current cap based on modifier state 363 var cap = key.getCap(modifiers); 364 365 // Update modifier state 366 if (cap.modifier) { 367 368 // Construct classname for modifier 369 var modifierClass = "guac-keyboard-modifier-" + cap.modifier; 370 var modifierMask = getModifierMask(cap.modifier); 371 372 // Toggle modifier state 373 modifiers ^= modifierMask; 374 375 // Activate modifier if pressed 376 if (modifiers & modifierMask) { 377 378 addClass(keyboard, modifierClass); 379 380 // Send key event 381 if (on_screen_keyboard.onkeydown && cap.keysym) 382 on_screen_keyboard.onkeydown(cap.keysym); 383 384 } 385 386 // Deactivate if not pressed 387 else { 388 389 removeClass(keyboard, modifierClass); 390 391 // Send key event 392 if (on_screen_keyboard.onkeyup && cap.keysym) 393 on_screen_keyboard.onkeyup(cap.keysym); 394 395 } 396 397 } 398 399 // If not modifier, send key event now 400 else if (on_screen_keyboard.onkeydown && cap.keysym) 401 on_screen_keyboard.onkeydown(cap.keysym); 402 403 // Mark key as pressed 404 key.pressed = true; 405 406 } 407 408 } 409 410 function release() { 411 412 // Release key if currently pressed 413 if (key.pressed) { 414 415 // Get current cap based on modifier state 416 var cap = key.getCap(modifiers); 417 418 removeClass(key_element, "guac-keyboard-pressed"); 419 420 // Send key event if not a modifier key 421 if (!cap.modifier && on_screen_keyboard.onkeyup && cap.keysym) 422 on_screen_keyboard.onkeyup(cap.keysym); 423 424 // Mark key as released 425 key.pressed = false; 426 427 } 428 429 } 430 431 function touchPress(e) { 432 e.preventDefault(); 433 ignore_mouse = on_screen_keyboard.touchMouseThreshold; 434 press(); 435 } 436 437 function touchRelease(e) { 438 e.preventDefault(); 439 ignore_mouse = on_screen_keyboard.touchMouseThreshold; 440 release(); 441 } 442 443 function mousePress(e) { 444 e.preventDefault(); 445 if (ignore_mouse == 0) 446 press(); 447 } 448 449 function mouseRelease(e) { 450 e.preventDefault(); 451 if (ignore_mouse == 0) 452 release(); 453 } 454 455 key_element.addEventListener("touchstart", touchPress, true); 456 key_element.addEventListener("touchend", touchRelease, true); 457 458 key_element.addEventListener("mousedown", mousePress, true); 459 key_element.addEventListener("mouseup", mouseRelease, true); 460 key_element.addEventListener("mouseout", mouseRelease, true); 461 462 } 463 464 }); 465 466 return row; 467 468 } 469 470 function parse_column(e) { 471 472 var col = document.createElement("div"); 473 col.className = "guac-keyboard-column"; 474 475 if (col.getAttribute("align")) 476 col.style.textAlign = col.getAttribute("align"); 477 478 // Columns can only contain rows 479 parseChildren(e, { 480 "row": function(e) { 481 col.appendChild(parse_row(e)); 482 } 483 }); 484 485 return col; 486 487 } 488 489 490 // Parse document 491 var keyboard_element = xml.documentElement; 492 if (keyboard_element.tagName != "keyboard") 493 throw new Error("Root element must be keyboard"); 494 495 // Get attributes 496 if (!keyboard_element.getAttribute("size")) 497 throw new Error("size attribute is required for keyboard"); 498 499 var keyboard_size = parseFloat(keyboard_element.getAttribute("size")); 500 501 parseChildren(keyboard_element, { 502 503 "row": function(e) { 504 keyboard.appendChild(parse_row(e)); 505 }, 506 507 "column": function(e) { 508 keyboard.appendChild(parse_column(e)); 509 } 510 511 }); 512 513 } 514 515 // Do not allow selection or mouse movement to propagate/register. 516 keyboard.onselectstart = 517 keyboard.onmousemove = 518 keyboard.onmouseup = 519 keyboard.onmousedown = 520 function(e) { 521 522 // If ignoring events, decrement counter 523 if (ignore_mouse) 524 ignore_mouse--; 525 526 e.stopPropagation(); 527 return false; 528 529 }; 530 531 /** 532 * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard. 533 * 534 * @event 535 * @param {Number} keysym The keysym of the key being pressed. 536 */ 537 this.onkeydown = null; 538 539 /** 540 * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard. 541 * 542 * @event 543 * @param {Number} keysym The keysym of the key being released. 544 */ 545 this.onkeyup = null; 546 547 /** 548 * Returns the element containing the entire on-screen keyboard. 549 * @returns {Element} The element containing the entire on-screen keyboard. 550 */ 551 this.getElement = function() { 552 return keyboard; 553 }; 554 555 /** 556 * Resizes all elements within this Guacamole.OnScreenKeyboard such that 557 * the width is close to but does not exceed the specified width. The 558 * height of the keyboard is determined based on the width. 559 * 560 * @param {Number} width The width to resize this Guacamole.OnScreenKeyboard 561 * to, in pixels. 562 */ 563 this.resize = function(width) { 564 565 // Get pixel size of a unit 566 var unit = Math.floor(width * 10 / keyboard_size) / 10; 567 568 // Resize all scaled elements 569 for (var i=0; i<scaledElements.length; i++) { 570 var scaledElement = scaledElements[i]; 571 scaledElement.scale(unit) 572 } 573 574 }; 575 576 }; 577 578 579 /** 580 * Basic representation of a single key of a keyboard. Each key has a set of 581 * caps associated with tuples of modifiers. The cap determins what happens 582 * when a key is pressed, while it is the state of modifier keys that determines 583 * what cap is in effect on any particular key. 584 * 585 * @constructor 586 */ 587 Guacamole.OnScreenKeyboard.Key = function() { 588 589 var key = this; 590 591 /** 592 * Whether this key is currently pressed. 593 */ 594 this.pressed = false; 595 596 /** 597 * Width of the key, relative to the size of the keyboard. 598 */ 599 this.size = 1; 600 601 /** 602 * An associative map of all caps by modifier. 603 */ 604 this.caps = {}; 605 606 /** 607 * Bit mask with all modifiers that affect this key set. 608 */ 609 this.modifierMask = 0; 610 611 /** 612 * Given the bitwise OR of all active modifiers, returns the key cap 613 * which applies. 614 */ 615 this.getCap = function(modifier) { 616 return key.caps[modifier & key.modifierMask]; 617 }; 618 619 }; 620 621 /** 622 * Basic representation of a cap of a key. The cap is the visible part of a key 623 * and determines the active behavior of a key when pressed. The state of all 624 * modifiers on the keyboard determines the active cap for all keys, thus 625 * each cap is associated with a set of modifiers. 626 * 627 * @constructor 628 * @param {String} text The text to be displayed within this cap. 629 * @param {Number} keysym The keysym this cap sends when its associated key is 630 * pressed or released. 631 * @param {String} modifier The modifier represented by this cap. 632 */ 633 Guacamole.OnScreenKeyboard.Cap = function(text, keysym, modifier) { 634 635 /** 636 * Modifier represented by this keycap 637 */ 638 this.modifier = null; 639 640 /** 641 * The text to be displayed within this keycap 642 */ 643 this.text = text; 644 645 /** 646 * The keysym this cap sends when its associated key is pressed/released 647 */ 648 this.keysym = keysym; 649 650 // Set modifier if provided 651 if (modifier) this.modifier = modifier; 652 653 }; 654