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