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