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