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