1 /* 2 * Copyright (C) 2015 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 layout object for an on-screen 27 * keyboard, this object will construct a clickable on-screen keyboard with its 28 * own key events. 29 * 30 * @constructor 31 * @param {Guacamole.OnScreenKeyboard.Layout} layout 32 * The layout of the on-screen keyboard to display. 33 */ 34 Guacamole.OnScreenKeyboard = function(layout) { 35 36 /** 37 * Reference to this Guacamole.OnScreenKeyboard. 38 * 39 * @type Guacamole.OnScreenKeyboard 40 */ 41 var osk = this; 42 43 /** 44 * Map of currently-set modifiers to the keysym associated with their 45 * original press. When the modifier is cleared, this keysym must be 46 * released. 47 * 48 * @private 49 * @type Object.<String, Number> 50 */ 51 var modifierKeysyms = {}; 52 53 /** 54 * Map of all key names to their current pressed states. If a key is not 55 * pressed, it may not be in this map at all, but all pressed keys will 56 * have a corresponding mapping to true. 57 * 58 * @type Object.<String, Boolean> 59 */ 60 var pressed = {}; 61 62 /** 63 * All scalable elements which are part of the on-screen keyboard. Each 64 * scalable element is carefully controlled to ensure the interface layout 65 * and sizing remains constant, even on browsers that would otherwise 66 * experience rounding error due to unit conversions. 67 * 68 * @private 69 * @type ScaledElement[] 70 */ 71 var scaledElements = []; 72 73 /** 74 * Adds a CSS class to an element. 75 * 76 * @private 77 * @function 78 * @param {Element} element 79 * The element to add a class to. 80 * 81 * @param {String} classname 82 * The name of the class to add. 83 */ 84 var addClass = function addClass(element, classname) { 85 86 // If classList supported, use that 87 if (element.classList) 88 element.classList.add(classname); 89 90 // Otherwise, simply append the class 91 else 92 element.className += " " + classname; 93 94 }; 95 96 /** 97 * Removes a CSS class from an element. 98 * 99 * @private 100 * @function 101 * @param {Element} element 102 * The element to remove a class from. 103 * 104 * @param {String} classname 105 * The name of the class to remove. 106 */ 107 var removeClass = function removeClass(element, classname) { 108 109 // If classList supported, use that 110 if (element.classList) 111 element.classList.remove(classname); 112 113 // Otherwise, manually filter out classes with given name 114 else { 115 element.className = element.className.replace(/([^ ]+)[ ]*/g, 116 function removeMatchingClasses(match, testClassname) { 117 118 // If same class, remove 119 if (testClassname === classname) 120 return ""; 121 122 // Otherwise, allow 123 return match; 124 125 } 126 ); 127 } 128 129 }; 130 131 /** 132 * Counter of mouse events to ignore. This decremented by mousemove, and 133 * while non-zero, mouse events will have no effect. 134 * 135 * @private 136 * @type Number 137 */ 138 var ignoreMouse = 0; 139 140 /** 141 * Ignores all pending mouse events when touch events are the apparent 142 * source. Mouse events are ignored until at least touchMouseThreshold 143 * mouse events occur without corresponding touch events. 144 * 145 * @private 146 */ 147 var ignorePendingMouseEvents = function ignorePendingMouseEvents() { 148 ignoreMouse = osk.touchMouseThreshold; 149 }; 150 151 /** 152 * An element whose dimensions are maintained according to an arbitrary 153 * scale. The conversion factor for these arbitrary units to pixels is 154 * provided later via a call to scale(). 155 * 156 * @private 157 * @constructor 158 * @param {Element} element 159 * The element whose scale should be maintained. 160 * 161 * @param {Number} width 162 * The width of the element, in arbitrary units, relative to other 163 * ScaledElements. 164 * 165 * @param {Number} height 166 * The height of the element, in arbitrary units, relative to other 167 * ScaledElements. 168 * 169 * @param {Boolean} [scaleFont=false] 170 * Whether the line height and font size should be scaled as well. 171 */ 172 var ScaledElement = function ScaledElement(element, width, height, scaleFont) { 173 174 /** 175 * The width of this ScaledElement, in arbitrary units, relative to 176 * other ScaledElements. 177 * 178 * @type Number 179 */ 180 this.width = width; 181 182 /** 183 * The height of this ScaledElement, in arbitrary units, relative to 184 * other ScaledElements. 185 * 186 * @type Number 187 */ 188 this.height = height; 189 190 /** 191 * Resizes the associated element, updating its dimensions according to 192 * the given pixels per unit. 193 * 194 * @param {Number} pixels 195 * The number of pixels to assign per arbitrary unit. 196 */ 197 this.scale = function(pixels) { 198 199 // Scale element width/height 200 element.style.width = (width * pixels) + "px"; 201 element.style.height = (height * pixels) + "px"; 202 203 // Scale font, if requested 204 if (scaleFont) { 205 element.style.lineHeight = (height * pixels) + "px"; 206 element.style.fontSize = pixels + "px"; 207 } 208 209 }; 210 211 }; 212 213 /** 214 * Returns whether all modifiers having the given names are currently 215 * active. 216 * 217 * @param {String[]} names 218 * The names of all modifiers to test. 219 * 220 * @returns {Boolean} 221 * true if all specified modifiers are pressed, false otherwise. 222 */ 223 var modifiersPressed = function modifiersPressed(names) { 224 225 // If any required modifiers are not pressed, return false 226 for (var i=0; i < names.length; i++) { 227 228 // Test whether current modifier is pressed 229 var name = names[i]; 230 if (!(name in modifierKeysyms)) 231 return false; 232 233 } 234 235 // Otherwise, all required modifiers are pressed 236 return true; 237 238 }; 239 240 /** 241 * Returns the single matching Key object associated with the key of the 242 * given name, where that Key object's requirements (such as pressed 243 * modifiers) are all currently satisfied. 244 * 245 * @param {String} keyName 246 * The name of the key to retrieve. 247 * 248 * @returns {Guacamole.OnScreenKeyboard.Key} 249 * The Key object associated with the given name, where that object's 250 * requirements are all currently satisfied, or null if no such Key 251 * can be found. 252 */ 253 var getActiveKey = function getActiveKey(keyName) { 254 255 // Get key array for given name 256 var keys = osk.keys[keyName]; 257 if (!keys) 258 return null; 259 260 // Find last matching key 261 for (var i = keys.length - 1; i >= 0; i--) { 262 263 // Get candidate key 264 var candidate = keys[i]; 265 266 // If all required modifiers are pressed, use that key 267 if (modifiersPressed(candidate.requires)) 268 return candidate; 269 270 } 271 272 // No valid key 273 return null; 274 275 }; 276 277 /** 278 * Presses the key having the given name, updating the associated key 279 * element with the "guac-keyboard-pressed" CSS class. If the key is 280 * already pressed, this function has no effect. 281 * 282 * @param {String} keyName 283 * The name of the key to press. 284 * 285 * @param {String} keyElement 286 * The element associated with the given key. 287 */ 288 var press = function press(keyName, keyElement) { 289 290 // Press key if not yet pressed 291 if (!pressed[keyName]) { 292 293 addClass(keyElement, "guac-keyboard-pressed"); 294 295 // Get current key based on modifier state 296 var key = getActiveKey(keyName); 297 298 // Update modifier state 299 if (key.modifier) { 300 301 // Construct classname for modifier 302 var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier); 303 304 // Retrieve originally-pressed keysym, if modifier was already pressed 305 var originalKeysym = modifierKeysyms[key.modifier]; 306 307 // Activate modifier if not pressed 308 if (!originalKeysym) { 309 310 addClass(keyboard, modifierClass); 311 modifierKeysyms[key.modifier] = key.keysym; 312 313 // Send key event 314 if (osk.onkeydown) 315 osk.onkeydown(key.keysym); 316 317 } 318 319 // Deactivate if not pressed 320 else { 321 322 removeClass(keyboard, modifierClass); 323 delete modifierKeysyms[key.modifier]; 324 325 // Send key event 326 if (osk.onkeyup) 327 osk.onkeyup(originalKeysym); 328 329 } 330 331 } 332 333 // If not modifier, send key event now 334 else if (osk.onkeydown) 335 osk.onkeydown(key.keysym); 336 337 // Mark key as pressed 338 pressed[keyName] = true; 339 340 } 341 342 }; 343 344 /** 345 * Releases the key having the given name, removing the 346 * "guac-keyboard-pressed" CSS class from the associated element. If the 347 * key is already released, this function has no effect. 348 * 349 * @param {String} keyName 350 * The name of the key to release. 351 * 352 * @param {String} keyElement 353 * The element associated with the given key. 354 */ 355 var release = function release(keyName, keyElement) { 356 357 // Release key if currently pressed 358 if (pressed[keyName]) { 359 360 removeClass(keyElement, "guac-keyboard-pressed"); 361 362 // Get current key based on modifier state 363 var key = getActiveKey(keyName); 364 365 // Send key event if not a modifier key 366 if (!key.modifier && osk.onkeyup) 367 osk.onkeyup(key.keysym); 368 369 // Mark key as released 370 pressed[keyName] = false; 371 372 } 373 374 }; 375 376 // Create keyboard 377 var keyboard = document.createElement("div"); 378 keyboard.className = "guac-keyboard"; 379 380 // Do not allow selection or mouse movement to propagate/register. 381 keyboard.onselectstart = 382 keyboard.onmousemove = 383 keyboard.onmouseup = 384 keyboard.onmousedown = function handleMouseEvents(e) { 385 386 // If ignoring events, decrement counter 387 if (ignoreMouse) 388 ignoreMouse--; 389 390 e.stopPropagation(); 391 return false; 392 393 }; 394 395 /** 396 * The number of mousemove events to require before re-enabling mouse 397 * event handling after receiving a touch event. 398 * 399 * @type Number 400 */ 401 this.touchMouseThreshold = 3; 402 403 /** 404 * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard. 405 * 406 * @event 407 * @param {Number} keysym The keysym of the key being pressed. 408 */ 409 this.onkeydown = null; 410 411 /** 412 * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard. 413 * 414 * @event 415 * @param {Number} keysym The keysym of the key being released. 416 */ 417 this.onkeyup = null; 418 419 /** 420 * The keyboard layout provided at time of construction. 421 * 422 * @type Guacamole.OnScreenKeyboard.Layout 423 */ 424 this.layout = new Guacamole.OnScreenKeyboard.Layout(layout); 425 426 /** 427 * Returns the element containing the entire on-screen keyboard. 428 * @returns {Element} The element containing the entire on-screen keyboard. 429 */ 430 this.getElement = function() { 431 return keyboard; 432 }; 433 434 /** 435 * Resizes all elements within this Guacamole.OnScreenKeyboard such that 436 * the width is close to but does not exceed the specified width. The 437 * height of the keyboard is determined based on the width. 438 * 439 * @param {Number} width The width to resize this Guacamole.OnScreenKeyboard 440 * to, in pixels. 441 */ 442 this.resize = function(width) { 443 444 // Get pixel size of a unit 445 var unit = Math.floor(width * 10 / osk.layout.width) / 10; 446 447 // Resize all scaled elements 448 for (var i=0; i<scaledElements.length; i++) { 449 var scaledElement = scaledElements[i]; 450 scaledElement.scale(unit); 451 } 452 453 }; 454 455 /** 456 * Given the name of a key and its corresponding definition, which may be 457 * an array of keys objects, a number (keysym), a string (key title), or a 458 * single key object, returns an array of key objects, deriving any missing 459 * properties as needed, and ensuring the key name is defined. 460 * 461 * @private 462 * @param {String} name 463 * The name of the key being coerced into an array of Key objects. 464 * 465 * @param {Number|String|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]} object 466 * The object defining the behavior of the key having the given name, 467 * which may be the title of the key (a string), the keysym (a number), 468 * a single Key object, or an array of Key objects. 469 * 470 * @returns {Guacamole.OnScreenKeyboard.Key[]} 471 * An array of all keys associated with the given name. 472 */ 473 var asKeyArray = function asKeyArray(name, object) { 474 475 // If already an array, just coerce into a true Key[] 476 if (object instanceof Array) { 477 var keys = []; 478 for (var i=0; i < object.length; i++) { 479 keys.push(new Guacamole.OnScreenKeyboard.Key(object[i], name)); 480 } 481 return keys; 482 } 483 484 // Derive key object from keysym if that's all we have 485 if (typeof object === 'number') { 486 return [new Guacamole.OnScreenKeyboard.Key({ 487 name : name, 488 keysym : object 489 })]; 490 } 491 492 // Derive key object from title if that's all we have 493 if (typeof object === 'string') { 494 return [new Guacamole.OnScreenKeyboard.Key({ 495 name : name, 496 title : object 497 })]; 498 } 499 500 // Otherwise, assume it's already a key object, just not an array 501 return [new Guacamole.OnScreenKeyboard.Key(object, name)]; 502 503 }; 504 505 /** 506 * Converts the rather forgiving key mapping allowed by 507 * Guacamole.OnScreenKeyboard.Layout into a rigorous mapping of key name 508 * to key definition, where the key definition is always an array of Key 509 * objects. 510 * 511 * @private 512 * @param {Object.<String, Number|String|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>} keys 513 * A mapping of key name to key definition, where the key definition is 514 * the title of the key (a string), the keysym (a number), a single 515 * Key object, or an array of Key objects. 516 * 517 * @returns {Object.<String, Guacamole.OnScreenKeyboard.Key[]>} 518 * A more-predictable mapping of key name to key definition, where the 519 * key definition is always simply an array of Key objects. 520 */ 521 var getKeys = function getKeys(keys) { 522 523 var keyArrays = {}; 524 525 // Coerce all keys into individual key arrays 526 for (var name in layout.keys) { 527 keyArrays[name] = asKeyArray(name, keys[name]); 528 } 529 530 return keyArrays; 531 532 }; 533 534 /** 535 * Map of all key names to their corresponding set of keys. Each key name 536 * may correspond to multiple keys due to the effect of modifiers. 537 * 538 * @type Object.<String, Guacamole.OnScreenKeyboard.Key[]> 539 */ 540 this.keys = getKeys(layout.keys); 541 542 /** 543 * Given an arbitrary string representing the name of some component of the 544 * on-screen keyboard, returns a string formatted for use as a CSS class 545 * name. The result will be lowercase. Word boundaries previously denoted 546 * by CamelCase will be replaced by individual hyphens, as will all 547 * contiguous non-alphanumeric characters. 548 * 549 * @private 550 * @param {String} name 551 * An arbitrary string representing the name of some component of the 552 * on-screen keyboard. 553 * 554 * @returns {String} 555 * A string formatted for use as a CSS class name. 556 */ 557 var getCSSName = function getCSSName(name) { 558 559 // Convert name from possibly-CamelCase to hyphenated lowercase 560 var cssName = name 561 .replace(/([a-z])([A-Z])/g, '$1-$2') 562 .replace(/[^A-Za-z0-9]+/g, '-') 563 .toLowerCase(); 564 565 return cssName; 566 567 }; 568 569 /** 570 * Appends DOM elements to the given element as dictated by the layout 571 * structure object provided. If a name is provided, an additional CSS 572 * class, prepended with "guac-keyboard-", will be added to the top-level 573 * element. 574 * 575 * If the layout structure object is an array, all elements within that 576 * array will be recursively appended as children of a group, and the 577 * top-level element will be given the CSS class "guac-keyboard-group". 578 * 579 * If the layout structure object is an object, all properties within that 580 * object will be recursively appended as children of a group, and the 581 * top-level element will be given the CSS class "guac-keyboard-group". The 582 * name of each property will be applied as the name of each child object 583 * for the sake of CSS. Each property will be added in sorted order. 584 * 585 * If the layout structure object is a string, the key having that name 586 * will be appended. The key will be given the CSS class 587 * "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name 588 * of the key. If the name of the key is a single character, this will 589 * first be transformed into the C-style hexadecimal literal for the 590 * Unicode codepoint of that character. For example, the key "A" would 591 * become "guac-keyboard-key-0x41". 592 * 593 * If the layout structure object is a number, a gap of that size will be 594 * inserted. The gap will be given the CSS class "guac-keyboard-gap", and 595 * will be scaled according to the same size units as each key. 596 * 597 * @private 598 * @param {Element} element 599 * The element to append elements to. 600 * 601 * @param {Array|Object|String|Number} object 602 * The layout structure object to use when constructing the elements to 603 * append. 604 * 605 * @param {String} [name] 606 * The name of the top-level element being appended, if any. 607 */ 608 var appendElements = function appendElements(element, object, name) { 609 610 var i; 611 612 // Create div which will become the group or key 613 var div = document.createElement('div'); 614 615 // Add class based on name, if name given 616 if (name) 617 addClass(div, 'guac-keyboard-' + getCSSName(name)); 618 619 // If an array, append each element 620 if (object instanceof Array) { 621 622 // Add group class 623 addClass(div, 'guac-keyboard-group'); 624 625 // Append all elements of array 626 for (i=0; i < object.length; i++) 627 appendElements(div, object[i]); 628 629 } 630 631 // If an object, append each property value 632 else if (object instanceof Object) { 633 634 // Add group class 635 addClass(div, 'guac-keyboard-group'); 636 637 // Append all children, sorted by name 638 var names = Object.keys(object).sort(); 639 for (i=0; i < names.length; i++) { 640 var name = names[i]; 641 appendElements(div, object[name], name); 642 } 643 644 } 645 646 // If a number, create as a gap 647 else if (typeof object === 'number') { 648 649 // Add gap class 650 addClass(div, 'guac-keyboard-gap'); 651 652 // Maintain scale 653 scaledElements.push(new ScaledElement(div, object, object)); 654 655 } 656 657 // If a string, create as a key 658 else if (typeof object === 'string') { 659 660 // If key name is only one character, use codepoint for name 661 var keyName = object; 662 if (keyName.length === 1) 663 keyName = '0x' + keyName.charCodeAt(0).toString(16); 664 665 // Add key container class 666 addClass(div, 'guac-keyboard-key-container'); 667 668 // Create key element which will contain all possible caps 669 var keyElement = document.createElement('div'); 670 keyElement.className = 'guac-keyboard-key ' 671 + 'guac-keyboard-key-' + getCSSName(keyName); 672 673 // Add all associated keys as caps within DOM 674 var keys = osk.keys[object]; 675 if (keys) { 676 for (i=0; i < keys.length; i++) { 677 678 // Get current key 679 var key = keys[i]; 680 681 // Create cap element for key 682 var capElement = document.createElement('div'); 683 capElement.className = 'guac-keyboard-cap'; 684 capElement.textContent = key.title; 685 686 // Add classes for any requirements 687 for (var j=0; j < key.requires.length; j++) { 688 var requirement = key.requires[j]; 689 addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement)); 690 addClass(keyElement, 'guac-keyboard-uses-' + getCSSName(requirement)); 691 } 692 693 // Add cap to key within DOM 694 keyElement.appendChild(capElement); 695 696 } 697 } 698 699 // Add key to DOM, maintain scale 700 div.appendChild(keyElement); 701 scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true)); 702 703 /** 704 * Handles a touch event which results in the pressing of an OSK 705 * key. Touch events will result in mouse events being ignored for 706 * touchMouseThreshold events. 707 * 708 * @param {TouchEvent} e 709 * The touch event being handled. 710 */ 711 var touchPress = function touchPress(e) { 712 e.preventDefault(); 713 ignoreMouse = osk.touchMouseThreshold; 714 press(object, keyElement); 715 }; 716 717 /** 718 * Handles a touch event which results in the release of an OSK 719 * key. Touch events will result in mouse events being ignored for 720 * touchMouseThreshold events. 721 * 722 * @param {TouchEvent} e 723 * The touch event being handled. 724 */ 725 var touchRelease = function touchRelease(e) { 726 e.preventDefault(); 727 ignoreMouse = osk.touchMouseThreshold; 728 release(object, keyElement); 729 }; 730 731 /** 732 * Handles a mouse event which results in the pressing of an OSK 733 * key. If mouse events are currently being ignored, this handler 734 * does nothing. 735 * 736 * @param {MouseEvent} e 737 * The touch event being handled. 738 */ 739 var mousePress = function mousePress(e) { 740 e.preventDefault(); 741 if (ignoreMouse === 0) 742 press(object, keyElement); 743 }; 744 745 /** 746 * Handles a mouse event which results in the release of an OSK 747 * key. If mouse events are currently being ignored, this handler 748 * does nothing. 749 * 750 * @param {MouseEvent} e 751 * The touch event being handled. 752 */ 753 var mouseRelease = function mouseRelease(e) { 754 e.preventDefault(); 755 if (ignoreMouse === 0) 756 release(object, keyElement); 757 }; 758 759 // Handle touch events on key 760 keyElement.addEventListener("touchstart", touchPress, true); 761 keyElement.addEventListener("touchend", touchRelease, true); 762 763 // Handle mouse events on key 764 keyElement.addEventListener("mousedown", mousePress, true); 765 keyElement.addEventListener("mouseup", mouseRelease, true); 766 keyElement.addEventListener("mouseout", mouseRelease, true); 767 768 } // end if object is key name 769 770 // Add newly-created group/key 771 element.appendChild(div); 772 773 }; 774 775 // Create keyboard layout in DOM 776 appendElements(keyboard, layout.layout); 777 778 }; 779 780 /** 781 * Represents an entire on-screen keyboard layout, including all available 782 * keys, their behaviors, and their relative position and sizing. 783 * 784 * @constructor 785 * @param {Guacamole.OnScreenKeyboard.Layout|Object} template 786 * The object whose identically-named properties will be used to initialize 787 * the properties of this layout. 788 */ 789 Guacamole.OnScreenKeyboard.Layout = function(template) { 790 791 /** 792 * The language of keyboard layout, such as "en_US". This property is for 793 * informational purposes only, but it is recommend to conform to the 794 * [language code]_[country code] format. 795 * 796 * @type String 797 */ 798 this.language = template.language; 799 800 /** 801 * The type of keyboard layout, such as "qwerty". This property is for 802 * informational purposes only, and does not conform to any standard. 803 * 804 * @type String 805 */ 806 this.type = template.type; 807 808 /** 809 * Map of key name to corresponding keysym, title, or key object. If only 810 * the keysym or title is provided, the key object will be created 811 * implicitly. In all cases, the name property of the key object will be 812 * taken from the name given in the mapping. 813 * 814 * @type Object.<String, Number|String|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]> 815 */ 816 this.keys = template.keys; 817 818 /** 819 * Arbitrarily nested, arbitrarily grouped key names. The contents of the 820 * layout will be traversed to produce an identically-nested grouping of 821 * keys in the DOM tree. All strings will be transformed into their 822 * corresponding sets of keys, while all objects and arrays will be 823 * transformed into named groups and anonymous groups respectively. Any 824 * numbers present will be transformed into gaps of that size, scaled 825 * according to the same units as each key. 826 * 827 * @type Object 828 */ 829 this.layout = template.layout; 830 831 /** 832 * The width of the entire keyboard, in arbitrary units. The width of each 833 * key is relative to this width, as both width values are assumed to be in 834 * the same units. The conversion factor between these units and pixels is 835 * derived later via a call to resize() on the Guacamole.OnScreenKeyboard. 836 * 837 * @type Number 838 */ 839 this.width = template.width; 840 841 /** 842 * The width of each key, in arbitrary units, relative to other keys in 843 * this layout. The true pixel size of each key will be determined by the 844 * overall size of the keyboard. If not defined here, the width of each 845 * key will default to 1. 846 * 847 * @type Object.<String, Number> 848 */ 849 this.keyWidths = template.keyWidths || {}; 850 851 }; 852 853 /** 854 * Represents a single key, or a single possible behavior of a key. Each key 855 * on the on-screen keyboard must have at least one associated 856 * Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or 857 * implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior 858 * depends on modifier states. 859 * 860 * @constructor 861 * @param {Guacamole.OnScreenKeyboard.Key|Object} template 862 * The object whose identically-named properties will be used to initialize 863 * the properties of this key. 864 * 865 * @param {String} [name] 866 * The name to use instead of any name provided within the template, if 867 * any. If omitted, the name within the template will be used, assuming the 868 * template contains a name. 869 */ 870 Guacamole.OnScreenKeyboard.Key = function(template, name) { 871 872 /** 873 * The unique name identifying this key within the keyboard layout. 874 * 875 * @type String 876 */ 877 this.name = name || template.name; 878 879 /** 880 * The human-readable title that will be displayed to the user within the 881 * key. If not provided, this will be derived from the key name. 882 * 883 * @type String 884 */ 885 this.title = template.title || this.name; 886 887 /** 888 * The keysym to be pressed/released when this key is pressed/released. If 889 * not provided, this will be derived from the title if the title is a 890 * single character. 891 * 892 * @type Number 893 */ 894 this.keysym = template.keysym || (function deriveKeysym(title) { 895 896 // Do not derive keysym if title is not exactly one character 897 if (!title || title.length !== 1) 898 return null; 899 900 // For characters between U+0000 and U+00FF, the keysym is the codepoint 901 var charCode = title.charCodeAt(0); 902 if (charCode >= 0x0000 && charCode <= 0x00FF) 903 return charCode; 904 905 // For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000 906 if (charCode >= 0x0100 && charCode <= 0x10FFFF) 907 return 0x01000000 | charCode; 908 909 // Unable to derive keysym 910 return null; 911 912 })(this.title); 913 914 /** 915 * The name of the modifier set when the key is pressed and cleared when 916 * this key is released, if any. The names of modifiers are distinct from 917 * the names of keys; both the "RightShift" and "LeftShift" keys may set 918 * the "shift" modifier, for example. By default, the key will affect no 919 * modifiers. 920 * 921 * @type String 922 */ 923 this.modifier = template.modifier; 924 925 /** 926 * An array containing the names of each modifier required for this key to 927 * have an effect. For example, a lowercase letter may require nothing, 928 * while an uppercase letter would require "shift", assuming the Shift key 929 * is named "shift" within the layout. By default, the key will require 930 * no modifiers. 931 * 932 * @type String[] 933 */ 934 this.requires = template.requires || []; 935 936 }; 937