From 7ec53c94ce889b6f5bdcedb689f580dff0eb5fbe Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 1 Feb 2016 22:03:44 -0800 Subject: [PATCH 01/11] GUAC-1480: Use AllowClipboard and execCommand for local/remote clipboard integration. --- .../client/controllers/clientController.js | 19 +- .../app/client/services/clipboardService.js | 171 ++++++++++++++++++ .../webapp/app/client/types/ManagedClient.js | 3 + 3 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/services/clipboardService.js diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index fd445d1c7..99bf72328 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -35,6 +35,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); + var clipboardService = $injector.get('clipboardService'); var guacClientManager = $injector.get('guacClientManager'); var guacNotification = $injector.get('guacNotification'); var preferenceService = $injector.get('preferenceService'); @@ -230,11 +231,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.menu.shown = false; }; - // Update the model when clipboard data received from client - $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { - $scope.clipboardData = clipboardData; - }); - /** * The client which should be attached to the client UI. * @@ -526,6 +522,19 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); } + // Hide status and sync local clipboard once connected + else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) { + + // Sync with local clipboard + clipboardService.getLocalClipboard().then(function clipboardRead(data) { + $scope.$broadcast('guacClipboard', 'text/plain', data); + }); + + // Hide status notification + guacNotification.showStatus(false); + + } + // Hide status for all other states else guacNotification.showStatus(false); diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js new file mode 100644 index 000000000..788015928 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2016 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A service for accessing local clipboard data. + */ +angular.module('client').factory('clipboardService', ['$injector', + function clipboardService($injector) { + + // Get required services + var $q = $injector.get('$q'); + var $rootScope = $injector.get('$rootScope'); + + var service = {}; + + /** + * A div which is used to hide the clipboard textarea and remove it from + * document flow. + * + * @type Element + */ + var clipElement = document.createElement('div'); + + /** + * The textarea that will be used to hold the local clipboard contents. + * + * @type Element + */ + var clipboardContent = document.createElement('textarea'); + + /** + * The contents of the last clipboard event broadcast by this service when + * the clipboard contents changed. + * + * @type String + */ + var lastClipboardEvent = ''; + + clipElement.appendChild(clipboardContent); + clipElement.style.position = 'absolute'; + clipElement.style.width = '0px'; + clipElement.style.height = '0px'; + clipElement.style.overflow = 'hidden'; + + document.body.appendChild(clipElement); + + /** + * Sets the local clipboard, if possible, to the given text. + * + * @param {String} text + * The text to which the local clipboard should be set. + * + * @return {Promise} + * A promise that will resolve if setting the clipboard was successful, + * and will reject if it failed. + */ + service.setLocalClipboard = function setLocalClipboard(text) { + + var deferred = $q.defer(); + + // First, see if allow clipboard extension is installed + if (window.AllowClipboard) { + + var clipboardClient = new AllowClipboard.Client.ClipboardClient(); + + clipboardClient.write(text, function(success) { + if (success) + deferred.resolve(); + else + deferred.reject(); + }); + + } + + // Otherwise, try execCommand + else { + + // Copy the given value into the clipboard DOM element + clipboardContent.value = text; + clipboardContent.select(); + + // Attempt to copy data from clipboard element into local clipboard + if (document.execCommand('copy')) + deferred.resolve(); + else + deferred.reject(); + + } + + return deferred.promise; + }; + + /** + * Get the current value of the local clipboard. + * + * @return {Promise} + * A promise that will resolve with the contents of the local clipboard + * if getting the clipboard was successful, and will reject if it + * failed. + */ + service.getLocalClipboard = function getLocalClipboard() { + + var deferred = $q.defer(); + + // First, see if allow clipboard extension is installed + if (window.AllowClipboard) { + + var clipboardClient = new AllowClipboard.Client.ClipboardClient(); + + clipboardClient.read(function(success, data) { + if (success) + deferred.resolve(data); + else + deferred.reject(); + }); + + } + + // Otherwise, try execCommand + else { + + // Clear and select the clipboard DOM element + clipboardContent.value = ''; + clipboardContent.select(); + + // Attempt paste local clipboard into clipboard DOM element + if (document.execCommand('paste')) + deferred.resolve(clipboardContent.value); + else + deferred.reject(); + + } + + return deferred.promise; + }; + + // Periodically attempt to read the clipboard, firing an event if successful + window.setInterval(function periodicallyReadClipboard() { + service.getLocalClipboard().then(function clipboardRead(data) { + + // Fire clipboard event if the data has changed + if (data !== lastClipboardEvent) { + $rootScope.$broadcast('guacClipboard', 'text/plain', data); + lastClipboardEvent = data; + } + + }); + }, 100); + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index a518ca88a..5187cb00c 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -38,8 +38,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Required services var $document = $injector.get('$document'); var $q = $injector.get('$q'); + var $rootScope = $injector.get('$rootScope'); var $window = $injector.get('$window'); var authenticationService = $injector.get('authenticationService'); + var clipboardService = $injector.get('clipboardService'); var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); var guacAudio = $injector.get('guacAudio'); @@ -403,6 +405,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', reader.onend = function clipboard_text_end() { $rootScope.$apply(function updateClipboard() { managedClient.clipboardData = data; + clipboardService.setLocalClipboard(data); }); }; From b361168bb9232ea9809b49c5c268c5888b54bbf6 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 1 Feb 2016 23:05:05 -0800 Subject: [PATCH 02/11] GUAC-1480: Don't set the clipboard state if it hasn't changed. --- guacamole/src/main/webapp/app/client/directives/guacClient.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index 5ea960ff9..f12cc79ee 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -414,8 +414,10 @@ angular.module('client').directive('guacClient', [function guacClient() { // Update remote clipboard if local clipboard changes $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { - if (client) + if (client && data !== $scope.client.clipboardData) { client.setClipboard(data); + $scope.client.clipboardData = data; + } }); // Translate local keydown events to remote keydown events if keyboard is enabled From ca20d6a17d7b4ff8defb093caf0182e6ce16d366 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 3 Feb 2016 16:53:13 -0800 Subject: [PATCH 03/11] GUAC-1480: Remove use of "Allow Clipboard" extension. --- .../app/client/services/clipboardService.js | 70 +++++-------------- 1 file changed, 16 insertions(+), 54 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index 788015928..801377177 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -77,34 +77,15 @@ angular.module('client').factory('clipboardService', ['$injector', var deferred = $q.defer(); - // First, see if allow clipboard extension is installed - if (window.AllowClipboard) { + // Copy the given value into the clipboard DOM element + clipboardContent.value = text; + clipboardContent.select(); - var clipboardClient = new AllowClipboard.Client.ClipboardClient(); - - clipboardClient.write(text, function(success) { - if (success) - deferred.resolve(); - else - deferred.reject(); - }); - - } - - // Otherwise, try execCommand - else { - - // Copy the given value into the clipboard DOM element - clipboardContent.value = text; - clipboardContent.select(); - - // Attempt to copy data from clipboard element into local clipboard - if (document.execCommand('copy')) - deferred.resolve(); - else - deferred.reject(); - - } + // Attempt to copy data from clipboard element into local clipboard + if (document.execCommand('copy')) + deferred.resolve(); + else + deferred.reject(); return deferred.promise; }; @@ -121,34 +102,15 @@ angular.module('client').factory('clipboardService', ['$injector', var deferred = $q.defer(); - // First, see if allow clipboard extension is installed - if (window.AllowClipboard) { + // Clear and select the clipboard DOM element + clipboardContent.value = ''; + clipboardContent.select(); - var clipboardClient = new AllowClipboard.Client.ClipboardClient(); - - clipboardClient.read(function(success, data) { - if (success) - deferred.resolve(data); - else - deferred.reject(); - }); - - } - - // Otherwise, try execCommand - else { - - // Clear and select the clipboard DOM element - clipboardContent.value = ''; - clipboardContent.select(); - - // Attempt paste local clipboard into clipboard DOM element - if (document.execCommand('paste')) - deferred.resolve(clipboardContent.value); - else - deferred.reject(); - - } + // Attempt paste local clipboard into clipboard DOM element + if (document.execCommand('paste')) + deferred.resolve(clipboardContent.value); + else + deferred.reject(); return deferred.promise; }; From 8740d365eb2be96adb6df62237bee8c1aa31640b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 3 Feb 2016 16:54:25 -0800 Subject: [PATCH 04/11] GUAC-1480: Do not rely on 0x0 clipping - it causes window.getSelection().toString() to return nothing. Things need to be at least 1x1 for selection to work reliably. --- .../src/main/webapp/app/client/services/clipboardService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index 801377177..c0ff6f789 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -57,8 +57,10 @@ angular.module('client').factory('clipboardService', ['$injector', clipElement.appendChild(clipboardContent); clipElement.style.position = 'absolute'; - clipElement.style.width = '0px'; - clipElement.style.height = '0px'; + clipElement.style.width = '1px'; + clipElement.style.height = '1px'; + clipElement.style.left = '-1px'; + clipElement.style.top = '-1px'; clipElement.style.overflow = 'hidden'; document.body.appendChild(clipElement); From 31eb5ec73f6205b185c9e543367d6f4fe5ba7db4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Feb 2016 17:33:26 -0800 Subject: [PATCH 05/11] GUAC-1480: Do not poll - just hook into events where the clipboard may have changed. --- .../app/client/services/clipboardService.js | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index c0ff6f789..b981b8bfb 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -104,21 +104,30 @@ angular.module('client').factory('clipboardService', ['$injector', var deferred = $q.defer(); - // Clear and select the clipboard DOM element - clipboardContent.value = ''; - clipboardContent.select(); + // Wait for the next event queue run before attempting to read + // clipboard data (in case the copy/cut has not yet completed) + window.setTimeout(function deferredClipboardRead() { - // Attempt paste local clipboard into clipboard DOM element - if (document.execCommand('paste')) - deferred.resolve(clipboardContent.value); - else - deferred.reject(); + // Clear and select the clipboard DOM element + clipboardContent.value = ''; + clipboardContent.select(); + + // Attempt paste local clipboard into clipboard DOM element + if (document.execCommand('paste')) + deferred.resolve(clipboardContent.value); + else + deferred.reject(); + + }, 10); return deferred.promise; }; - // Periodically attempt to read the clipboard, firing an event if successful - window.setInterval(function periodicallyReadClipboard() { + /** + * Checks whether the clipboard data has changed, firing a new + * "guacClipboard" event if it has. + */ + var checkClipboard = function checkClipboard() { service.getLocalClipboard().then(function clipboardRead(data) { // Fire clipboard event if the data has changed @@ -128,7 +137,12 @@ angular.module('client').factory('clipboardService', ['$injector', } }); - }, 100); + }; + + // Attempt to read the clipboard if it may have changed + window.addEventListener('copy', checkClipboard, true); + window.addEventListener('cut', checkClipboard, true); + window.addEventListener('focus', checkClipboard, true); return service; From 43022c14b383dc519d6ab2ea46f57de7b0f92b9b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 4 Feb 2016 22:01:36 -0800 Subject: [PATCH 06/11] GUAC-1480: Only check clipboard when window regains focus. Do not attempt to read clipboard unless textarea actually has focus (paste may occur elsewhere otherwise). --- .../webapp/app/client/services/clipboardService.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index b981b8bfb..0aa68ed2b 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -110,10 +110,11 @@ angular.module('client').factory('clipboardService', ['$injector', // Clear and select the clipboard DOM element clipboardContent.value = ''; + clipboardContent.focus(); clipboardContent.select(); // Attempt paste local clipboard into clipboard DOM element - if (document.execCommand('paste')) + if (document.activeElement === clipboardContent && document.execCommand('paste')) deferred.resolve(clipboardContent.value); else deferred.reject(); @@ -142,7 +143,13 @@ angular.module('client').factory('clipboardService', ['$injector', // Attempt to read the clipboard if it may have changed window.addEventListener('copy', checkClipboard, true); window.addEventListener('cut', checkClipboard, true); - window.addEventListener('focus', checkClipboard, true); + window.addEventListener('focus', function focusGained(e) { + + // Only recheck clipboard if it's the window itself that gained focus + if (e.target === window) + checkClipboard(); + + }, true); return service; From 3869ca25c9e0e26edde3a157991b9b52d6a44372 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 5 Feb 2016 10:43:06 -0800 Subject: [PATCH 07/11] GUAC-1480: Increase size of timing window when waiting for clipboard to settle. --- .../src/main/webapp/app/client/services/clipboardService.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index 0aa68ed2b..c760120da 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -119,7 +119,7 @@ angular.module('client').factory('clipboardService', ['$injector', else deferred.reject(); - }, 10); + }, 100); return deferred.promise; }; @@ -133,8 +133,8 @@ angular.module('client').factory('clipboardService', ['$injector', // Fire clipboard event if the data has changed if (data !== lastClipboardEvent) { - $rootScope.$broadcast('guacClipboard', 'text/plain', data); - lastClipboardEvent = data; + $rootScope.$broadcast('guacClipboard', 'text/plain', data); + lastClipboardEvent = data; } }); From 944d126c422c9b460099fef7bc70908e265c52c4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 5 Feb 2016 10:47:30 -0800 Subject: [PATCH 08/11] GUAC-1480: Clean up and document init. --- .../webapp/app/client/services/clipboardService.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index c760120da..71f3ec2c8 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -55,14 +55,16 @@ angular.module('client').factory('clipboardService', ['$injector', */ var lastClipboardEvent = ''; + // Ensure textarea is selectable but not visible clipElement.appendChild(clipboardContent); clipElement.style.position = 'absolute'; - clipElement.style.width = '1px'; - clipElement.style.height = '1px'; - clipElement.style.left = '-1px'; - clipElement.style.top = '-1px'; + clipElement.style.width = '1px'; + clipElement.style.height = '1px'; + clipElement.style.left = '-1px'; + clipElement.style.top = '-1px'; clipElement.style.overflow = 'hidden'; + // Add textarea to DOM document.body.appendChild(clipElement); /** From 1896d9418f1c6d20cc68e5e2f7d698b3751810c6 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 5 Feb 2016 11:39:39 -0800 Subject: [PATCH 09/11] GUAC-1480: Check clipboard on load as well. --- .../src/main/webapp/app/client/services/clipboardService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index 71f3ec2c8..efa847875 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -143,6 +143,7 @@ angular.module('client').factory('clipboardService', ['$injector', }; // Attempt to read the clipboard if it may have changed + window.addEventListener('load', checkClipboard, true); window.addEventListener('copy', checkClipboard, true); window.addEventListener('cut', checkClipboard, true); window.addEventListener('focus', function focusGained(e) { From 1564aeed87dff4a2841c6b9f98c04ec17c64a109 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 8 Feb 2016 21:00:56 -0800 Subject: [PATCH 10/11] GUAC-1480: Unfocus clipboard DOM element to avoid mobile keyboard popping up. --- .../src/main/webapp/app/client/services/clipboardService.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index efa847875..30a3d8bed 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -91,6 +91,9 @@ angular.module('client').factory('clipboardService', ['$injector', else deferred.reject(); + // Unfocus the clipboard DOM event to avoid mobile keyboard opening + clipboardContent.blur(); + return deferred.promise; }; @@ -121,6 +124,9 @@ angular.module('client').factory('clipboardService', ['$injector', else deferred.reject(); + // Unfocus the clipboard DOM event to avoid mobile keyboard opening + clipboardContent.blur(); + }, 100); return deferred.promise; From a492e2a8f32cbd34b598b6886e330847a1ac0bda Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 8 Feb 2016 21:39:58 -0800 Subject: [PATCH 11/11] GUAC-1480: Fix broken menu clipboard regression. --- .../webapp/app/client/controllers/clientController.js | 9 +++++++-- .../src/main/webapp/app/client/directives/guacClient.js | 6 ++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 99bf72328..e1b5c991b 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -372,13 +372,18 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Send clipboard data if menu is hidden if (!menuShown && menuShownPreviousState) - $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); + $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); // Disable client keyboard if the menu is shown $scope.client.clientProperties.keyboardEnabled = !menuShown; }); - + + // Update remote clipboard if local clipboard changes + $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { + $scope.client.clipboardData = data; + }); + $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { keysCurrentlyPressed[keysym] = true; diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index f12cc79ee..31f17fd03 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -413,11 +413,9 @@ angular.module('client').directive('guacClient', [function guacClient() { }; // Update remote clipboard if local clipboard changes - $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { - if (client && data !== $scope.client.clipboardData) { + $scope.$watch('client.clipboardData', function clipboardChanged(data) { + if (client) client.setClipboard(data); - $scope.client.clipboardData = data; - } }); // Translate local keydown events to remote keydown events if keyboard is enabled