From 9dc86119b0190a5c4f573474e8d78c3bb2381456 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 19:13:36 -0800 Subject: [PATCH 01/22] GUACAMOLE-352: Do not attempt to send dead keys (only the composed key should be sent). --- .../src/main/webapp/modules/Keyboard.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index e41b03ed5..0480d4dd0 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -459,22 +459,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], From 3ee73d835c3a3ac8b275fea7220aa7ee1fa6ab6a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 21:09:15 -0800 Subject: [PATCH 02/22] GUACAMOLE-352: Handle both "input" and "compositionend" events as sources of keys. --- .../src/main/webapp/modules/Keyboard.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 0480d4dd0..42020c7c2 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -826,6 +826,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. @@ -1183,6 +1207,30 @@ Guacamole.Keyboard = function(element) { }, true); + // Automatically type text entered into the wrapped element + element.addEventListener("input", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Type all content written + if (e.data) + guac_keyboard.type(e.data); + + }, false); + + // Automatically type the result of composed characters/text + element.addEventListener("compositionend", function(e) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Type all content written + if (e.data) + guac_keyboard.type(e.data); + + }, false); + }; /** From cdacd570992ed57bd8b01eea3438473e3ddf0e6a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 21:17:46 -0800 Subject: [PATCH 03/22] GUACAMOLE-352: Handle "input" / "compositionend" events in a mutually-exclusive manner, as they may conflict. --- .../src/main/webapp/modules/Keyboard.js | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 42020c7c2..e16d48ef5 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1207,29 +1207,54 @@ Guacamole.Keyboard = function(element) { }, true); + /** + * 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) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Type all content written + if (e.data) { + element.removeEventListener("compositionend", handleComposition, false); + guac_keyboard.type(e.data); + } + + }; + + /** + * 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) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Type all content written + if (e.data) { + element.removeEventListener("input", handleInput, false); + guac_keyboard.type(e.data); + } + + }; + // Automatically type text entered into the wrapped element - element.addEventListener("input", function(e) { - - // Only intercept if handler set - if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; - - // Type all content written - if (e.data) - guac_keyboard.type(e.data); - - }, false); - - // Automatically type the result of composed characters/text - element.addEventListener("compositionend", function(e) { - - // Only intercept if handler set - if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; - - // Type all content written - if (e.data) - guac_keyboard.type(e.data); - - }, false); + element.addEventListener("input", handleInput, false); + element.addEventListener("compositionend", handleComposition, false); }; From fd47d1d7efe966813be75dab4bbb7af0ee912d8f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 21:28:12 -0800 Subject: [PATCH 04/22] GUACAMOLE-352: Only attempt to type fully-composed strings. --- .../src/main/webapp/modules/Keyboard.js | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index e16d48ef5..4c4b1bad9 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1207,6 +1207,30 @@ Guacamole.Keyboard = function(element) { }, true); + /** + * Returns whether the given string is fully composed. A string is fully + * composed if it does not end with combining characters. + * + * @private + * @param {String} str + * The string to test. + * + * @returns {Boolean} + * true of the string is fully composed, false otherwise. + */ + var isComposed = function isComposed(str) { + + // The empty string is fully composed + if (!str) + return true; + + // Test whether the last character is within the "Combining + // Diacritical Marks" Unicode block (U+0300 through U+036F) + var lastCodepoint = str.charCodeAt(str.length - 1); + return !(lastCodepoint >= 0x0300 && lastCodepoint <= 0x036F); + + }; + /** * Handles the given "input" event, typing the data within the input text. * If the event is complete (text is provided), handling of "compositionend" @@ -1222,7 +1246,7 @@ Guacamole.Keyboard = function(element) { if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; // Type all content written - if (e.data) { + if (e.data && isComposed(e.data)) { element.removeEventListener("compositionend", handleComposition, false); guac_keyboard.type(e.data); } @@ -1245,7 +1269,7 @@ Guacamole.Keyboard = function(element) { if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; // Type all content written - if (e.data) { + if (e.data && isComposed(e.data)) { element.removeEventListener("input", handleInput, false); guac_keyboard.type(e.data); } From 646f9732f38c80cb631af4b421bca7ef4051960f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 21:51:19 -0800 Subject: [PATCH 05/22] GUACAMOLE-352: Track in-progress composition, ignoring "input" events for a composition which is known to be incomplete. --- .../src/main/webapp/modules/Keyboard.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 4c4b1bad9..1df14e20f 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1231,6 +1231,15 @@ Guacamole.Keyboard = function(element) { }; + /** + * The in-progress composition, if any, such as the intermediate result of + * pressing a series of dead keys. + * + * @private + * @type {String} + */ + var inProgressComposition = ''; + /** * Handles the given "input" event, typing the data within the input text. * If the event is complete (text is provided), handling of "compositionend" @@ -1245,6 +1254,11 @@ Guacamole.Keyboard = function(element) { // Only intercept if handler set if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + // Ignore input events which represent the in-progress composition, + // as reported by composition events + if (e.data === inProgressComposition) + return; + // Type all content written if (e.data && isComposed(e.data)) { element.removeEventListener("compositionend", handleComposition, false); @@ -1280,6 +1294,14 @@ Guacamole.Keyboard = function(element) { element.addEventListener("input", handleInput, false); element.addEventListener("compositionend", handleComposition, false); + element.addEventListener("compositionstart", function resetComposition() { + inProgressComposition = ''; + }, false); + + element.addEventListener("compositionupdate", function updateComposition(e) { + inProgressComposition = e.data; + }, false); + }; /** From 3d6a3aaa2a8bafa40b545c2dd9a5047f401601e2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 21:56:28 -0800 Subject: [PATCH 06/22] GUACAMOLE-352: Rely on isComposing property of InputEvent to determine whether the event should be ignored. --- .../src/main/webapp/modules/Keyboard.js | 50 +------------------ 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 1df14e20f..a11050ff9 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1207,39 +1207,6 @@ Guacamole.Keyboard = function(element) { }, true); - /** - * Returns whether the given string is fully composed. A string is fully - * composed if it does not end with combining characters. - * - * @private - * @param {String} str - * The string to test. - * - * @returns {Boolean} - * true of the string is fully composed, false otherwise. - */ - var isComposed = function isComposed(str) { - - // The empty string is fully composed - if (!str) - return true; - - // Test whether the last character is within the "Combining - // Diacritical Marks" Unicode block (U+0300 through U+036F) - var lastCodepoint = str.charCodeAt(str.length - 1); - return !(lastCodepoint >= 0x0300 && lastCodepoint <= 0x036F); - - }; - - /** - * The in-progress composition, if any, such as the intermediate result of - * pressing a series of dead keys. - * - * @private - * @type {String} - */ - var inProgressComposition = ''; - /** * Handles the given "input" event, typing the data within the input text. * If the event is complete (text is provided), handling of "compositionend" @@ -1254,13 +1221,8 @@ Guacamole.Keyboard = function(element) { // Only intercept if handler set if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; - // Ignore input events which represent the in-progress composition, - // as reported by composition events - if (e.data === inProgressComposition) - return; - // Type all content written - if (e.data && isComposed(e.data)) { + if (e.data && !e.isComposing) { element.removeEventListener("compositionend", handleComposition, false); guac_keyboard.type(e.data); } @@ -1283,7 +1245,7 @@ Guacamole.Keyboard = function(element) { if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; // Type all content written - if (e.data && isComposed(e.data)) { + if (e.data) { element.removeEventListener("input", handleInput, false); guac_keyboard.type(e.data); } @@ -1294,14 +1256,6 @@ Guacamole.Keyboard = function(element) { element.addEventListener("input", handleInput, false); element.addEventListener("compositionend", handleComposition, false); - element.addEventListener("compositionstart", function resetComposition() { - inProgressComposition = ''; - }, false); - - element.addEventListener("compositionupdate", function updateComposition(e) { - inProgressComposition = e.data; - }, false); - }; /** From f9a639d2014815c8e68bdebb17dd78283d31ee68 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 22:38:10 -0800 Subject: [PATCH 07/22] GUACAMOLE-352: Add Guacamole.Keyboard.InputSink object to serve as a reliable default destination for input events. --- .../src/main/webapp/modules/Keyboard.js | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index a11050ff9..ea7128bfa 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1324,3 +1324,91 @@ Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) { return state; }; + +/** + * 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.Keyboard.InputSink = function InputSink() { + + /** + * Reference to this instance of Guacamole.Keyboard.InputSink. + * + * @private + * @type {Guacamole.Keyboard.InputSink} + */ + var sink = this; + + /** + * The underlying input field, styled to be invisible. + * + * @private + * @type {Element} + */ + var field = document.createElement('textarea'); + field.setAttribute('autofocus', 'autofocus'); + field.style.position = 'fixed'; + field.style.border = 'none'; + field.style.width = '10px'; + field.style.height = '10px'; + field.style.left = '-10px'; + field.style.top = '-10px'; + + /** + * Clears the contents of the underlying field. The actual clearing of the + * field is deferred, occurring asynchronously after the call completes. + * + * @private + */ + var clear = function clear() { + window.setTimeout(function deferClear() { + field.value = ''; + }, 0); + }; + + // Keep internal field contents clear + field.addEventListener("change", clear, false); + field.addEventListener("input", clear, false); + + /** + * 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.Keyboard.InputSink to have any + * effect. + * + * @returns {Element} + */ + this.getElement = function getElement() { + return field; + }; + + // Automatically refocus input sink if part of DOM + document.addEventListener("click", function refocusSink(e) { + + // Do not refocus if focus is on an input field + var focused = document.activeElement; + if (focused && focused !== document.body) + return; + + // Refocus input sink instead of handling click + sink.focus(); + e.preventDefault(); + + }, true); + +}; From f6968600674f1a2051a97267b826bbe2f6a535f3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 17 Dec 2017 22:51:34 -0800 Subject: [PATCH 08/22] GUACAMOLE-352: Leverage Guacamole.Keyboard.InputSink within webapp as a default destination for input events. --- .../src/main/webapp/app/index/controllers/indexController.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index 3a9230bf7..c4cad66fc 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -82,6 +82,10 @@ angular.module('index').controller('indexController', ['$scope', '$injector', }; + // Add default destination for input events + var sink = new Guacamole.Keyboard.InputSink(); + $document[0].body.appendChild(sink.getElement()); + // Create event listeners at the global level var keyboard = new Guacamole.Keyboard($document[0]); From 05822907b46528ac91c8405c1da2f38602309572 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 10:40:59 -0800 Subject: [PATCH 09/22] GUACAMOLE-352: Refocus InputSink through key events rather than clicks. --- guacamole-common-js/src/main/webapp/modules/Keyboard.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index ea7128bfa..3cc28f84d 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1398,7 +1398,7 @@ Guacamole.Keyboard.InputSink = function InputSink() { }; // Automatically refocus input sink if part of DOM - document.addEventListener("click", function refocusSink(e) { + document.addEventListener("keydown", function refocusSink(e) { // Do not refocus if focus is on an input field var focused = document.activeElement; @@ -1407,7 +1407,6 @@ Guacamole.Keyboard.InputSink = function InputSink() { // Refocus input sink instead of handling click sink.focus(); - e.preventDefault(); }, true); From 2d26d24dda30ba3679c3259e920487fdf28865bf Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 10:58:50 -0800 Subject: [PATCH 10/22] GUACAMOLE-352: Allow additional elements to be added to a single Guacamole.Keyboard. --- .../src/main/webapp/modules/Keyboard.js | 302 +++++++++++------- 1 file changed, 195 insertions(+), 107 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 3cc28f84d..c25a1fd45 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} [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. @@ -1134,130 +1156,196 @@ Guacamole.Keyboard = function(element) { }; - // When key pressed - element.addEventListener("keydown", function(e) { - - // Only intercept if handler set - if (!guac_keyboard.onkeydown) 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; - - 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; - - e.preventDefault(); - - var keyCode; - if (window.event) keyCode = window.event.keyCode; - else if (e.which) keyCode = e.which; - - // Fix modifier states - syncModifierStates(e); - - // Log event, call for interpretation - var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e)); - eventLog.push(keyupEvent); - interpret_events(); - - }, true); - /** - * 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. + * 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. * - * @private - * @param {InputEvent} e - * The "input" event to handle. + * @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 handleInput = function handleInput(e) { + var markEvent = function markEvent(e) { - // Only intercept if handler set - if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + // Fail if event is already marked + if (e[EVENT_MARKER]) + return false; - // Type all content written - if (e.data && !e.isComposing) { - element.removeEventListener("compositionend", handleComposition, false); - guac_keyboard.type(e.data); - } + // Mark event otherwise + e[EVENT_MARKER] = true; + return true; }; /** - * 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. + * 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. * - * @private - * @param {CompositionEvent} e - * The "compositionend" event to handle. + * @param {Element} element + * The Element to attach event listeners to for the sake of handling + * key or input events. */ - var handleComposition = function handleComposition(e) { + this.listenTo = function listenTo(element) { - // Only intercept if handler set - if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + // When key pressed + element.addEventListener("keydown", function(e) { - // Type all content written - if (e.data) { - element.removeEventListener("input", handleInput, false); - guac_keyboard.type(e.data); - } + // 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; + + e.preventDefault(); + + var keyCode; + if (window.event) keyCode = window.event.keyCode; + else if (e.which) keyCode = e.which; + + // Fix modifier states + syncModifierStates(e); + + // Log event, call for interpretation + var keyupEvent = new KeyupEvent(keyCode, e.keyIdentifier, e.key, getEventLocation(e)); + eventLog.push(keyupEvent); + interpret_events(); + + }, true); + + /** + * 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) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Ignore events which have already been handled + if (!markEvent(e)) return; + + // Type all content written + if (e.data && !e.isComposing) { + element.removeEventListener("compositionend", handleComposition, true); + guac_keyboard.type(e.data); + } + + }; + + /** + * 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) { + + // Only intercept if handler set + if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; + + // Ignore events which have already been handled + if (!markEvent(e)) return; + + // Type all content written + if (e.data) { + element.removeEventListener("input", handleInput, true); + guac_keyboard.type(e.data); + } + + }; + + // Automatically type text entered into the wrapped field + element.addEventListener("input", handleInput, true); + element.addEventListener("compositionend", handleComposition, true); }; - // Automatically type text entered into the wrapped element - 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 From 86d9cc12489af91d4926d57a45e9d6c4480f178a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 10:59:15 -0800 Subject: [PATCH 11/22] GUACAMOLE-352: Explicitly listen to the input sink. --- .../src/main/webapp/app/index/controllers/indexController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index c4cad66fc..5237db640 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -88,6 +88,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Create event listeners at the global level var keyboard = new Guacamole.Keyboard($document[0]); + keyboard.listenTo(sink.getElement()); // Broadcast keydown events keyboard.onkeydown = function onkeydown(keysym) { From 5136b1cf8e4637c253cca609c336038edb8274e3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 11:20:13 -0800 Subject: [PATCH 12/22] GUACAMOLE-352: Clarify that Guacamole.Keyboard can be given an Element or a Document. --- guacamole-common-js/src/main/webapp/modules/Keyboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index c25a1fd45..aaeb1f621 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -25,7 +25,7 @@ var Guacamole = Guacamole || {}; * which represent keys as their corresponding X11 keysym. * * @constructor - * @param {Element} [element] + * @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. @@ -1186,7 +1186,7 @@ Guacamole.Keyboard = function Keyboard(element) { * events signalled through this Guacamole.Keyboard's onkeydown and * onkeyup handlers. * - * @param {Element} element + * @param {Element|Document} element * The Element to attach event listeners to for the sake of handling * key or input events. */ From 7b29f7b082f4315a0a740823c6067466b16f2d8d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 11:20:48 -0800 Subject: [PATCH 13/22] GUACAMOLE-352: Do not clear InputSink field upon "input" event (may be fired after a partial composition). --- guacamole-common-js/src/main/webapp/modules/Keyboard.js | 1 - 1 file changed, 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index aaeb1f621..0563f72f9 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1462,7 +1462,6 @@ Guacamole.Keyboard.InputSink = function InputSink() { // Keep internal field contents clear field.addEventListener("change", clear, false); - field.addEventListener("input", clear, false); /** * Attempts to focus the underlying input field. The focus attempt occurs From e5e01beb60a780a54da2c7763d7f70a32adba913 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 14:07:24 -0800 Subject: [PATCH 14/22] GUACAMOLE-352: Ignore other input fields if they are invisible. --- .../src/main/webapp/modules/Keyboard.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 0563f72f9..3a114db4c 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1470,6 +1470,7 @@ Guacamole.Keyboard.InputSink = function InputSink() { this.focus = function focus() { window.setTimeout(function deferRefocus() { field.focus(); // Focus must be deferred to work reliably across browsers + field.select(); }, 0); }; @@ -1489,8 +1490,14 @@ Guacamole.Keyboard.InputSink = function InputSink() { // Do not refocus if focus is on an input field var focused = document.activeElement; - if (focused && focused !== document.body) - return; + 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(); From 9f6b2fad37aa8f087d1464523c4ff1ee75ef861f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 14:56:39 -0800 Subject: [PATCH 15/22] GUACAMOLE-352: Additionally click() while attempting to refocus the input sink field. Do not rely on autofocus, which may result in the field being partly focused (outlined as focused) but not receiving any actual text input. --- .../src/main/webapp/modules/Keyboard.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 3a114db4c..b0930c2ca 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -1440,8 +1440,8 @@ Guacamole.Keyboard.InputSink = function InputSink() { * @type {Element} */ var field = document.createElement('textarea'); - field.setAttribute('autofocus', 'autofocus'); field.style.position = 'fixed'; + field.style.outline = 'none'; field.style.border = 'none'; field.style.width = '10px'; field.style.height = '10px'; @@ -1463,6 +1463,16 @@ Guacamole.Keyboard.InputSink = function InputSink() { // Keep internal field contents clear field.addEventListener("change", clear, 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. @@ -1470,7 +1480,6 @@ Guacamole.Keyboard.InputSink = function InputSink() { this.focus = function focus() { window.setTimeout(function deferRefocus() { field.focus(); // Focus must be deferred to work reliably across browsers - field.select(); }, 0); }; From 9065b497c2333b6034a490ccfe41d36e91d21e5b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 18 Dec 2017 15:41:55 -0800 Subject: [PATCH 16/22] GUACAMOLE-352: Remove Guacamole menu entirely when closed. Input fields within the menu must not continue to receive input. --- guacamole/src/main/webapp/app/client/templates/client.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@