Source: main/webapp/modules/OnScreenKeyboard.js

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