diff --git a/guacamole-common-js/src/main/webapp/modules/InputSink.js b/guacamole-common-js/src/main/webapp/modules/InputSink.js new file mode 100644 index 000000000..5b1bfdfa0 --- /dev/null +++ b/guacamole-common-js/src/main/webapp/modules/InputSink.js @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var Guacamole = Guacamole || {}; + +/** + * A hidden input field which attempts to keep itself focused at all times, + * except when another input field has been intentionally focused, whether + * programatically or by the user. The actual underlying input field, returned + * by getElement(), may be used as a reliable source of keyboard-related events, + * particularly composition and input events which may require a focused input + * field to be dispatched at all. + * + * @constructor + */ +Guacamole.InputSink = function InputSink() { + + /** + * Reference to this instance of Guacamole.InputSink. + * + * @private + * @type {Guacamole.InputSink} + */ + var sink = this; + + /** + * The underlying input field, styled to be invisible. + * + * @private + * @type {Element} + */ + var field = document.createElement('textarea'); + field.style.position = 'fixed'; + field.style.outline = 'none'; + field.style.border = 'none'; + field.style.margin = '0'; + field.style.padding = '0'; + field.style.height = '0'; + field.style.width = '0'; + field.style.left = '0'; + field.style.bottom = '0'; + field.style.resize = 'none'; + field.style.background = 'transparent'; + field.style.color = 'transparent'; + + // Keep field clear when modified via normal keypresses + field.addEventListener("keypress", function clearKeypress(e) { + field.value = ''; + }, false); + + // Keep field clear when modofied via composition events + field.addEventListener("compositionend", function clearCompletedComposition(e) { + if (e.data) + field.value = ''; + }, false); + + // Keep field clear when modofied via input events + field.addEventListener("input", function clearCompletedInput(e) { + if (e.data && !e.isComposing) + field.value = ''; + }, false); + + // Whenever focus is gained, automatically click to ensure cursor is + // actually placed within the field (the field may simply be highlighted or + // outlined otherwise) + field.addEventListener("focus", function focusReceived() { + window.setTimeout(function deferRefocus() { + field.click(); + field.select(); + }, 0); + }, true); + + /** + * Attempts to focus the underlying input field. The focus attempt occurs + * asynchronously, and may silently fail depending on browser restrictions. + */ + this.focus = function focus() { + window.setTimeout(function deferRefocus() { + field.focus(); // Focus must be deferred to work reliably across browsers + }, 0); + }; + + /** + * Returns the underlying input field. This input field MUST be manually + * added to the DOM for the Guacamole.InputSink to have any effect. + * + * @returns {Element} + */ + this.getElement = function getElement() { + return field; + }; + + // Automatically refocus input sink if part of DOM + document.addEventListener("keydown", function refocusSink(e) { + + // Do not refocus if focus is on an input field + var focused = document.activeElement; + if (focused && focused !== document.body) { + + // Only consider focused input fields which are actually visible + var rect = focused.getBoundingClientRect(); + if (rect.left + rect.width > 0 && rect.top + rect.height > 0) + return; + + } + + // Refocus input sink instead of handling click + sink.focus(); + + }, true); + +}; diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index e41b03ed5..7902e6ab4 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -25,9 +25,12 @@ var Guacamole = Guacamole || {}; * which represent keys as their corresponding X11 keysym. * * @constructor - * @param {Element} element The Element to use to provide keyboard events. + * @param {Element|Document} [element] + * The Element to use to provide keyboard events. If omitted, at least one + * Element must be manually provided through the listenTo() function for + * the Guacamole.Keyboard instance to have any effect. */ -Guacamole.Keyboard = function(element) { +Guacamole.Keyboard = function Keyboard(element) { /** * Reference to this Guacamole.Keyboard. @@ -35,6 +38,25 @@ Guacamole.Keyboard = function(element) { */ var guac_keyboard = this; + /** + * An integer value which uniquely identifies this Guacamole.Keyboard + * instance with respect to other Guacamole.Keyboard instances. + * + * @private + * @type {Number} + */ + var guacKeyboardID = Guacamole.Keyboard._nextID++; + + /** + * The name of the property which is added to event objects via markEvent() + * to note that they have already been handled by this Guacamole.Keyboard. + * + * @private + * @constant + * @type {String} + */ + var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID; + /** * Fired whenever the user presses a key with the element associated * with this Guacamole.Keyboard in focus. @@ -459,22 +481,6 @@ Guacamole.Keyboard = function(element) { "Compose": [0xFF20], "Control": [0xFFE3, 0xFFE3, 0xFFE4], "ContextMenu": [0xFF67], - "DeadGrave": [0xFE50], - "DeadAcute": [0xFE51], - "DeadCircumflex": [0xFE52], - "DeadTilde": [0xFE53], - "DeadMacron": [0xFE54], - "DeadBreve": [0xFE55], - "DeadAboveDot": [0xFE56], - "DeadUmlaut": [0xFE57], - "DeadAboveRing": [0xFE58], - "DeadDoubleacute": [0xFE59], - "DeadCaron": [0xFE5A], - "DeadCedilla": [0xFE5B], - "DeadOgonek": [0xFE5C], - "DeadIota": [0xFE5D], - "DeadVoicedSound": [0xFE5E], - "DeadSemivoicedSound": [0xFE5F], "Delete": [0xFFFF], "Down": [0xFF54], "End": [0xFF57], @@ -842,6 +848,30 @@ Guacamole.Keyboard = function(element) { }; + /** + * Presses and releases the keys necessary to type the given string of + * text. + * + * @param {String} str + * The string to type. + */ + this.type = function type(str) { + + // Press/release the key corresponding to each character in the string + for (var i = 0; i < str.length; i++) { + + // Determine keysym of current character + var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i); + var keysym = keysym_from_charcode(codepoint); + + // Press and release key for current character + guac_keyboard.press(keysym); + guac_keyboard.release(keysym); + + } + + }; + /** * Resets the state of this keyboard, releasing all keys, and firing keyup * events for each released key. @@ -1126,81 +1156,196 @@ Guacamole.Keyboard = function(element) { }; - // When key pressed - element.addEventListener("keydown", function(e) { + /** + * Attempts to mark the given Event as having been handled by this + * Guacamole.Keyboard. If the Event has already been marked as handled, + * false is returned. + * + * @param {Event} e + * The Event to mark. + * + * @returns {Boolean} + * true if the given Event was successfully marked, false if the given + * Event was already marked. + */ + var markEvent = function markEvent(e) { - // Only intercept if handler set - if (!guac_keyboard.onkeydown) return; + // Fail if event is already marked + if (e[EVENT_MARKER]) + return false; - var keyCode; - if (window.event) keyCode = window.event.keyCode; - else if (e.which) keyCode = e.which; + // Mark event otherwise + e[EVENT_MARKER] = true; + return true; - // Fix modifier states - syncModifierStates(e); + }; - // Ignore (but do not prevent) the "composition" keycode sent by some - // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) - if (keyCode === 229) - return; + /** + * Attaches event listeners to the given Element, automatically translating + * received key, input, and composition events into simple keydown/keyup + * events signalled through this Guacamole.Keyboard's onkeydown and + * onkeyup handlers. + * + * @param {Element|Document} element + * The Element to attach event listeners to for the sake of handling + * key or input events. + */ + this.listenTo = function listenTo(element) { - // Log event - var keydownEvent = new KeydownEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e)); - eventLog.push(keydownEvent); + // When key pressed + element.addEventListener("keydown", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown) return; + + // Ignore events which have already been handled + if (!markEvent(e)) return; + + var keyCode; + if (window.event) keyCode = window.event.keyCode; + else if (e.which) keyCode = e.which; + + // Fix modifier states + syncModifierStates(e); + + // Ignore (but do not prevent) the "composition" keycode sent by some + // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) + if (keyCode === 229) + return; + + // Log event + var keydownEvent = new KeydownEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e)); + eventLog.push(keydownEvent); + + // Interpret as many events as possible, prevent default if indicated + if (interpret_events()) + e.preventDefault(); + + }, true); + + // When key pressed + element.addEventListener("keypress", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Ignore events which have already been handled + if (!markEvent(e)) return; + + var charCode; + if (window.event) charCode = window.event.keyCode; + else if (e.which) charCode = e.which; + + // Fix modifier states + syncModifierStates(e); + + // Log event + var keypressEvent = new KeypressEvent(charCode); + eventLog.push(keypressEvent); + + // Interpret as many events as possible, prevent default if indicated + if (interpret_events()) + e.preventDefault(); + + }, true); + + // When key released + element.addEventListener("keyup", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeyup) return; + + // Ignore events which have already been handled + if (!markEvent(e)) return; - // Interpret as many events as possible, prevent default if indicated - if (interpret_events()) e.preventDefault(); - }, true); + var keyCode; + if (window.event) keyCode = window.event.keyCode; + else if (e.which) keyCode = e.which; - // When key pressed - element.addEventListener("keypress", function(e) { + // Fix modifier states + syncModifierStates(e); - // Only intercept if handler set - if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + // Log event, call for interpretation + var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e)); + eventLog.push(keyupEvent); + interpret_events(); - var charCode; - if (window.event) charCode = window.event.keyCode; - else if (e.which) charCode = e.which; + }, true); - // Fix modifier states - syncModifierStates(e); + /** + * Handles the given "input" event, typing the data within the input text. + * If the event is complete (text is provided), handling of "compositionend" + * events is suspended, as such events may conflict with input events. + * + * @private + * @param {InputEvent} e + * The "input" event to handle. + */ + var handleInput = function handleInput(e) { - // Log event - var keypressEvent = new KeypressEvent(charCode); - eventLog.push(keypressEvent); + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; - // Interpret as many events as possible, prevent default if indicated - if (interpret_events()) - e.preventDefault(); + // Ignore events which have already been handled + if (!markEvent(e)) return; - }, true); + // Type all content written + if (e.data && !e.isComposing) { + element.removeEventListener("compositionend", handleComposition, false); + guac_keyboard.type(e.data); + } - // When key released - element.addEventListener("keyup", function(e) { + }; - // Only intercept if handler set - if (!guac_keyboard.onkeyup) return; + /** + * Handles the given "compositionend" event, typing the data within the + * composed text. If the event is complete (composed text is provided), + * handling of "input" events is suspended, as such events may conflict + * with composition events. + * + * @private + * @param {CompositionEvent} e + * The "compositionend" event to handle. + */ + var handleComposition = function handleComposition(e) { - e.preventDefault(); + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; - var keyCode; - if (window.event) keyCode = window.event.keyCode; - else if (e.which) keyCode = e.which; - - // Fix modifier states - syncModifierStates(e); + // Ignore events which have already been handled + if (!markEvent(e)) return; - // Log event, call for interpretation - var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e)); - eventLog.push(keyupEvent); - interpret_events(); + // Type all content written + if (e.data) { + element.removeEventListener("input", handleInput, false); + guac_keyboard.type(e.data); + } - }, true); + }; + + // Automatically type text entered into the wrapped field + element.addEventListener("input", handleInput, false); + element.addEventListener("compositionend", handleComposition, false); + + }; + + // Listen to given element, if any + if (element) + guac_keyboard.listenTo(element); }; +/** + * The unique numerical identifier to assign to the next Guacamole.Keyboard + * instance. + * + * @private + * @type {Number} + */ +Guacamole.Keyboard._nextID = 0; + /** * The state of all supported keyboard modifiers. * @constructor diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index 0580c8bbb..0d2b688d4 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -38,7 +38,7 @@