From 0eed6c32aeee5661f80e56d59affc12690acde75 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 21 Jun 2016 23:55:05 -0700 Subject: [PATCH 01/31] GUACAMOLE-55: Move clipboard functionality to a new guacClipboard directive. --- .../app/client/directives/guacClipboard.js | 68 +++++++++++++++++++ .../webapp/app/client/templates/client.html | 2 +- .../app/client/templates/guacClipboard.html | 1 + 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/webapp/app/client/directives/guacClipboard.js create mode 100644 guacamole/src/main/webapp/app/client/templates/guacClipboard.html diff --git a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js new file mode 100644 index 000000000..c656b9ade --- /dev/null +++ b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/** + * A directive which exposes the current clipboard contents, if possible, + * allowing the user to edit those contents. If the current clipboard contents + * cannot be directly accessed, the user can at least directly copy/paste data + * within the field provided by this directive. The contents of this clipboard + * directive, whether retrieved from the local or manipulated manually by the + * user, are exposed via the "data" attribute. + */ +angular.module('client').directive('guacClipboard', [function guacClipboard() { + + /** + * Configuration object for the guacClipboard directive. + * + * @type Object. + */ + var config = { + restrict : 'E', + replace : true, + templateUrl : 'app/client/templates/guacClipboard.html' + }; + + // Scope properties exposed by the guacClipboard directive + config.scope = { + + /** + * The data to display within the field provided by this directive. If + * the local clipboard can be accessed by JavaScript, this will be set + * automatically as the local clipboard changes. Failing that, this + * will be set when the user manually modifies the contents of the + * field. Changes to this value will be rendered within the field and, + * if possible, will be pushed to the local clipboard. + * + * @type String + */ + data : '=' + + }; + + // guacClipboard directive controller + config.controller = ['$scope', '$injector', '$element', + function guacClipboardController($scope, $injector, $element) { + + // STUB + + }]; + + return config; + +}]); diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html index f56b50632..8a4e7fea2 100644 --- a/guacamole/src/main/webapp/app/client/templates/client.html +++ b/guacamole/src/main/webapp/app/client/templates/client.html @@ -54,7 +54,7 @@

{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}

{{'CLIENT.HELP_CLIPBOARD' | translate}}

- +
diff --git a/guacamole/src/main/webapp/app/client/templates/guacClipboard.html b/guacamole/src/main/webapp/app/client/templates/guacClipboard.html new file mode 100644 index 000000000..8b56ac83c --- /dev/null +++ b/guacamole/src/main/webapp/app/client/templates/guacClipboard.html @@ -0,0 +1 @@ + From 0edc730308c5fc78a811c56a45cd6e30997c0dc7 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Jun 2016 00:26:46 -0700 Subject: [PATCH 02/31] GUACAMOLE-55: Only the guacClipboard directive should fire guacClipboard events. --- .../client/controllers/clientController.js | 47 +--------- .../app/client/directives/guacClipboard.js | 86 ++++++++++++++++++- .../app/client/services/clipboardService.js | 39 +-------- .../webapp/app/client/types/ManagedClient.js | 4 + 4 files changed, 90 insertions(+), 86 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index daa782229..6b1c943ae 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -32,7 +32,6 @@ 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'); @@ -245,15 +244,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams */ var keysCurrentlyPressed = {}; - /** - * Map of all currently pressed keys (by keysym) to the clipboard contents - * received from the remote desktop while those keys were pressed. All keys - * not currently pressed will not have entries within this map. - * - * @type Object. - */ - var clipboardDataFromKey = {}; - /* * Check to see if all currently pressed keys are in the set of menu keys. */ @@ -384,24 +374,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { - // Send clipboard data if menu is hidden - if (!menuShown && menuShownPreviousState) - $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); - // Disable client keyboard if the menu is shown $scope.client.clientProperties.keyboardEnabled = !menuShown; }); - // Watch clipboard for new data, associating it with any pressed keys - $scope.$watch('client.clipboardData', function clipboardChanged(data) { - - // Associate new clipboard data with any currently-pressed key - for (var keysym in keysCurrentlyPressed) - clipboardDataFromKey[keysym] = data; - - }); - // Track pressed keys, opening the Guacamole menu after Ctrl+Alt+Shift $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { @@ -437,18 +414,9 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); - // Update pressed keys as they are released, synchronizing the clipboard - // with any data that appears to have come from those key presses + // Update pressed keys as they are released $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - // Sync local clipboard with any clipboard data received while this - // key was pressed (if any) - var clipboardData = clipboardDataFromKey[keysym]; - if (clipboardData) { - clipboardService.setLocalClipboard(clipboardData); - delete clipboardDataFromKey[keysym]; - } - // Mark key as released delete keysCurrentlyPressed[keysym]; @@ -561,19 +529,6 @@ 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/directives/guacClipboard.js b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js index c656b9ade..eb17639db 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js @@ -23,7 +23,9 @@ * cannot be directly accessed, the user can at least directly copy/paste data * within the field provided by this directive. The contents of this clipboard * directive, whether retrieved from the local or manipulated manually by the - * user, are exposed via the "data" attribute. + * user, are exposed via the "data" attribute. In addition to updating the + * "data" attribute, changes to clipboard data will be broadcast on the scope + * via "guacClipboard" events. */ angular.module('client').directive('guacClipboard', [function guacClipboard() { @@ -59,7 +61,87 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { config.controller = ['$scope', '$injector', '$element', function guacClipboardController($scope, $injector, $element) { - // STUB + // Required services + var $rootScope = $injector.get('$rootScope'); + var clipboardService = $injector.get('clipboardService'); + + /** + * Map of all currently pressed keys by keysym. If a particular key is + * currently pressed, the value stored under that key's keysym within + * this map will be true. All keys not currently pressed will not have entries + * within this map. + * + * @type Object. + */ + var keysCurrentlyPressed = {}; + + /** + * Map of all currently pressed keys (by keysym) to the clipboard + * contents received while those keys were pressed. All keys not + * currently pressed will not have entries within this map. + * + * @type Object. + */ + var clipboardDataFromKey = {}; + + // Watch clipboard for new data, associating it with any pressed keys + $scope.$watch('data', function clipboardChanged(data) { + + // Associate new clipboard data with any currently-pressed key + for (var keysym in keysCurrentlyPressed) + clipboardDataFromKey[keysym] = data; + + // Notify of updated clipboard data + $rootScope.$broadcast('guacClipboard', 'text/plain', data); + + }); + + // Track pressed keys + $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { + + // Record key as pressed + keysCurrentlyPressed[keysym] = true; + + }); + + // Update pressed keys as they are released, synchronizing the clipboard + // with any data that appears to have come from those key presses + $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { + + // Sync local clipboard with any clipboard data received while this + // key was pressed (if any) + var clipboardData = clipboardDataFromKey[keysym]; + if (clipboardData) { + clipboardService.setLocalClipboard(clipboardData); + delete clipboardDataFromKey[keysym]; + } + + // Mark key as released + delete keysCurrentlyPressed[keysym]; + + }); + + /** + * Checks whether the clipboard data has changed, firing a new + * "guacClipboard" event if it has. + */ + var checkClipboard = function checkClipboard() { + clipboardService.getLocalClipboard().then(function clipboardRead(data) { + $scope.data = data; + }); + }; + + // 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) { + + // Only recheck clipboard if it's the window itself that gained focus + if (e.target === window) + checkClipboard(); + + }, true); }]; diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index 7a5950476..19cd865e0 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -24,8 +24,7 @@ angular.module('client').factory('clipboardService', ['$injector', function clipboardService($injector) { // Get required services - var $q = $injector.get('$q'); - var $rootScope = $injector.get('$rootScope'); + var $q = $injector.get('$q'); var service = {}; @@ -44,14 +43,6 @@ angular.module('client').factory('clipboardService', ['$injector', */ 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 = ''; - // Ensure textarea is selectable but not visible clipElement.appendChild(clipboardContent); clipElement.style.position = 'absolute'; @@ -129,34 +120,6 @@ angular.module('client').factory('clipboardService', ['$injector', return deferred.promise; }; - /** - * 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 - if (data !== lastClipboardEvent) { - $rootScope.$broadcast('guacClipboard', 'text/plain', data); - lastClipboardEvent = data; - } - - }); - }; - - // 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) { - - // Only recheck clipboard if it's the window itself that gained focus - if (e.target === window) - checkClipboard(); - - }, true); - 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 9a2870687..40a4dbf13 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -383,6 +383,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CONNECTED); + // Send any clipboard data already provided + if (managedClient.clipboardData) + client.setClipboard(managedClient.clipboardData); + // Begin streaming audio input if possible requestAudioStream(client); From 0c6383a0827a73a2f89c05e25ee00f4a1da48bbe Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Jun 2016 00:42:31 -0700 Subject: [PATCH 03/31] GUACAMOLE-55: Clipboard element must be "position: fixed" to avoid unnecessary scrolling of the document. --- .../src/main/webapp/app/client/services/clipboardService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/client/services/clipboardService.js index 19cd865e0..ed518a1d4 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -45,7 +45,7 @@ angular.module('client').factory('clipboardService', ['$injector', // Ensure textarea is selectable but not visible clipElement.appendChild(clipboardContent); - clipElement.style.position = 'absolute'; + clipElement.style.position = 'fixed'; clipElement.style.width = '1px'; clipElement.style.height = '1px'; clipElement.style.left = '-1px'; From 01eddd2772d0285025bf448dc279a3bacc5651ff Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Jun 2016 01:09:24 -0700 Subject: [PATCH 04/31] GUACAMOLE-55: Restore focus state after attempting to read local clipboard. --- .../webapp/app/client/services/clipboardService.js | 14 ++++++++++++-- 1 file changed, 12 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 ed518a1d4..a81dd0e73 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/client/services/clipboardService.js @@ -69,6 +69,9 @@ angular.module('client').factory('clipboardService', ['$injector', var deferred = $q.defer(); + // Track the originally-focused element prior to changing focus + var originalElement = document.activeElement; + // Copy the given value into the clipboard DOM element clipboardContent.value = text; clipboardContent.select(); @@ -79,8 +82,10 @@ angular.module('client').factory('clipboardService', ['$injector', else deferred.reject(); - // Unfocus the clipboard DOM event to avoid mobile keyboard opening + // Unfocus the clipboard DOM event to avoid mobile keyboard opening, + // restoring whichever element was originally focused clipboardContent.blur(); + originalElement.focus(); return deferred.promise; }; @@ -101,6 +106,9 @@ angular.module('client').factory('clipboardService', ['$injector', // clipboard data (in case the copy/cut has not yet completed) window.setTimeout(function deferredClipboardRead() { + // Track the originally-focused element prior to changing focus + var originalElement = document.activeElement; + // Clear and select the clipboard DOM element clipboardContent.value = ''; clipboardContent.focus(); @@ -112,8 +120,10 @@ angular.module('client').factory('clipboardService', ['$injector', else deferred.reject(); - // Unfocus the clipboard DOM event to avoid mobile keyboard opening + // Unfocus the clipboard DOM event to avoid mobile keyboard opening, + // restoring whichever element was originally focused clipboardContent.blur(); + originalElement.focus(); }, 100); From 65897fdb64d713e582b967ab57be23956bfeaefb Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Jun 2016 01:31:11 -0700 Subject: [PATCH 05/31] GUACAMOLE-55: Check clipboard when guacClipboard directive is initialized. --- .../src/main/webapp/app/client/directives/guacClipboard.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js index eb17639db..4f0354e52 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js @@ -143,6 +143,9 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { }, true); + // Perform initial clipboard check + checkClipboard(); + }]; return config; From 0a0933a23e935e80622b37ac292f06cdf3720a76 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Jun 2016 01:46:32 -0700 Subject: [PATCH 06/31] GUACAMOLE-55: Clean up event handlers after guacClipboard directive is destroyed. --- .../app/client/directives/guacClipboard.js | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js index 4f0354e52..bd050c923 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js @@ -63,6 +63,7 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { // Required services var $rootScope = $injector.get('$rootScope'); + var $window = $injector.get('$window'); var clipboardService = $injector.get('clipboardService'); /** @@ -122,26 +123,38 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { }); /** - * Checks whether the clipboard data has changed, firing a new - * "guacClipboard" event if it has. + * Checks whether the clipboard data has changed, updating the stored + * clipboard data if it has. If this function is being called due to a + * DOM event, that event should be passed to this function such that the + * context of the call can be taken into account. Focus events, in + * particular, need to be considered only in the context of the window. + * + * @param {Event} [e] + * The event currently being handled, if any. */ - var checkClipboard = function checkClipboard() { + var checkClipboard = function checkClipboard(e) { + + // Ignore focus events for anything except the window + if (e && e.type === 'focus' && e.target !== $window) + return; + clipboardService.getLocalClipboard().then(function clipboardRead(data) { $scope.data = data; }); + }; // 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) { + $window.addEventListener('copy', checkClipboard, true); + $window.addEventListener('cut', checkClipboard, true); + $window.addEventListener('focus', checkClipboard, true); - // Only recheck clipboard if it's the window itself that gained focus - if (e.target === window) - checkClipboard(); - - }, true); + // Clean up on destruction + $scope.$on('$destroy', function destroyClipboard() { + $window.removeEventListener('copy', checkClipboard); + $window.removeEventListener('cut', checkClipboard); + $window.removeEventListener('focus', checkClipboard); + }); // Perform initial clipboard check checkClipboard(); From 7f08766392027977d8e14452c5be04dcbf645b1e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Jun 2016 15:17:28 -0700 Subject: [PATCH 07/31] GUACAMOLE-55: Add image support to the guacClipboard directive. --- .../app/client/directives/guacClipboard.js | 87 ++++++++++++++++++- .../webapp/app/client/styles/clipboard.css | 51 +++++++++++ .../webapp/app/client/styles/guac-menu.css | 8 -- .../app/client/templates/guacClipboard.html | 10 ++- 4 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/styles/clipboard.css diff --git a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js index bd050c923..745577ad9 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClipboard.js @@ -51,7 +51,7 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { * field. Changes to this value will be rendered within the field and, * if possible, will be pushed to the local clipboard. * - * @type String + * @type String|Blob */ data : '=' @@ -85,6 +85,74 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { */ var clipboardDataFromKey = {}; + /** + * The URL of the image is currently stored within the clipboard. If + * the clipboard currently contains text, this will be null. + * + * @type String + */ + $scope.imageURL = null; + + // Intercept paste events, handling image data specifically + $element[0].addEventListener('paste', function dataPasted(e) { + + // For each item within the clipboard + var items = e.clipboardData.items; + for (var i = 0; i < items.length; i++) { + + // If the item is an image, attempt to read that image + if (items[i].kind === 'file' && /^image\//.exec(items[i].type)) { + + // Set clipboard data to contents + $scope.$apply(function setClipboardData() { + $scope.data = items[i].getAsFile(); + }); + + // Do not paste + e.preventDefault(); + return; + + } + + } // end for each item + + }); + + /** + * Returns whether the clipboard currently contains only an image, the + * URL of which is exposed via the imageURL property. + * + * @returns {Boolean} + * true if the current clipboard contains only an image, false + * otherwise. + */ + $scope.isImage = function isImage() { + return !!$scope.imageURL; + }; + + /** + * Returns whether the clipboard currently contains only text. + * + * @returns {Boolean} + * true if the clipboard currently contains only text, false + * otherwise. + */ + $scope.isText = function isText() { + return !$scope.isImage(); + }; + + /** + * Clears the current clipboard contents. If the clipboard currently + * displays an image, this will also return to a text-based clipboard + * display. + */ + $scope.resetClipboard = function resetClipboard() { + + // Reset to blank + $scope.data = ''; + + }; + // Watch clipboard for new data, associating it with any pressed keys $scope.$watch('data', function clipboardChanged(data) { @@ -92,8 +160,21 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { for (var keysym in keysCurrentlyPressed) clipboardDataFromKey[keysym] = data; - // Notify of updated clipboard data - $rootScope.$broadcast('guacClipboard', 'text/plain', data); + // Revoke old image URL, if any + if ($scope.imageURL) { + URL.revokeObjectURL($scope.imageURL); + $scope.imageURL = null; + } + + // If the copied data was an image, display it as such + if (data instanceof Blob) { + $scope.imageURL = URL.createObjectURL(data); + $rootScope.$broadcast('guacClipboard', data.type, data); + } + + // Otherwise, the data is simply text + else + $rootScope.$broadcast('guacClipboard', 'text/plain', data); }); diff --git a/guacamole/src/main/webapp/app/client/styles/clipboard.css b/guacamole/src/main/webapp/app/client/styles/clipboard.css new file mode 100644 index 000000000..cade1aecb --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/clipboard.css @@ -0,0 +1,51 @@ +/* + * 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. + */ + +.clipboard .image-clipboard, +.clipboard .text-clipboard textarea { + position: relative; + border: 1px solid #AAA; + -moz-border-radius: 0.25em; + -webkit-border-radius: 0.25em; + -khtml-border-radius: 0.25em; + border-radius: 0.25em; + background: white; +} + +.clipboard .text-clipboard textarea { + width: 100%; + white-space: pre; +} + +.clipboard .image-clipboard { + text-align: center; +} + +.clipboard .image-clipboard .reset-button { + position: absolute; + left: 0; + top: 0; + font-size: 0.75em; + min-width: 0; +} + +.clipboard .image-clipboard img { + max-width: 100%; + max-height: 480px; +} diff --git a/guacamole/src/main/webapp/app/client/styles/guac-menu.css b/guacamole/src/main/webapp/app/client/styles/guac-menu.css index 3f5460e93..2ddb432d3 100644 --- a/guacamole/src/main/webapp/app/client/styles/guac-menu.css +++ b/guacamole/src/main/webapp/app/client/styles/guac-menu.css @@ -66,14 +66,6 @@ } #guac-menu #clipboard-settings textarea { - width: 100%; - border: 1px solid #AAA; - -moz-border-radius: 0.25em; - -webkit-border-radius: 0.25em; - -khtml-border-radius: 0.25em; - border-radius: 0.25em; - white-space: pre; - display: block; font-size: 1em; } diff --git a/guacamole/src/main/webapp/app/client/templates/guacClipboard.html b/guacamole/src/main/webapp/app/client/templates/guacClipboard.html index 8b56ac83c..af4501956 100644 --- a/guacamole/src/main/webapp/app/client/templates/guacClipboard.html +++ b/guacamole/src/main/webapp/app/client/templates/guacClipboard.html @@ -1 +1,9 @@ - +
+
+ +
+
+ + +
+
From 8c5446127b1f11c63695fd125c20c32e2a1bc469 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 23 Jun 2016 00:24:01 -0700 Subject: [PATCH 08/31] GUACAMOLE-55: Move clipboard handling to own module. Represent clipboard contents with ClipboardData type. --- .../main/webapp/app/client/clientModule.js | 1 + .../app/client/directives/guacClient.js | 4 +- .../webapp/app/client/types/ManagedClient.js | 106 +++++++++++++---- .../webapp/app/clipboard/clipboardModule.js | 23 ++++ .../directives/guacClipboard.js | 108 ++++++++++++++---- .../services/clipboardService.js | 39 +++++-- .../styles/clipboard.css | 0 .../templates/guacClipboard.html | 4 +- .../app/clipboard/types/ClipboardData.js | 59 ++++++++++ 9 files changed, 287 insertions(+), 57 deletions(-) create mode 100644 guacamole/src/main/webapp/app/clipboard/clipboardModule.js rename guacamole/src/main/webapp/app/{client => clipboard}/directives/guacClipboard.js (72%) rename guacamole/src/main/webapp/app/{client => clipboard}/services/clipboardService.js (77%) rename guacamole/src/main/webapp/app/{client => clipboard}/styles/clipboard.css (100%) rename guacamole/src/main/webapp/app/{client => clipboard}/templates/guacClipboard.html (66%) create mode 100644 guacamole/src/main/webapp/app/clipboard/types/ClipboardData.js diff --git a/guacamole/src/main/webapp/app/client/clientModule.js b/guacamole/src/main/webapp/app/client/clientModule.js index 49131a328..73d322021 100644 --- a/guacamole/src/main/webapp/app/client/clientModule.js +++ b/guacamole/src/main/webapp/app/client/clientModule.js @@ -22,6 +22,7 @@ */ angular.module('client', [ 'auth', + 'clipboard', 'element', 'history', 'navigation', diff --git a/guacamole/src/main/webapp/app/client/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js index c3382d7fb..769edd703 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClient.js +++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js @@ -410,9 +410,9 @@ angular.module('client').directive('guacClient', [function guacClient() { }; // Update remote clipboard if local clipboard changes - $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { + $scope.$on('guacClipboard', function onClipboard(event, data) { if (client) { - client.setClipboard(data); + ManagedClient.setClipboard($scope.client, data); $scope.client.clipboardData = data; } }); diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 40a4dbf13..7b1d58800 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -26,6 +26,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Required types var ClientProperties = $injector.get('ClientProperties'); var ClientIdentifier = $injector.get('ClientIdentifier'); + var ClipboardData = $injector.get('ClipboardData'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedFilesystem = $injector.get('ManagedFilesystem'); @@ -37,7 +38,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', 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 tunnelService = $injector.get('tunnelService'); @@ -100,9 +100,12 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', /** * The current clipboard contents. * - * @type String + * @type ClipboardData */ - this.clipboardData = template.clipboardData || ''; + this.clipboardData = template.clipboardData || new ClipboardData({ + type : 'text/plain', + data : '' + }); /** * All uploaded files. As files are uploaded, their progress can be @@ -385,7 +388,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Send any clipboard data already provided if (managedClient.clipboardData) - client.setClipboard(managedClient.clipboardData); + ManagedClient.setClipboard(managedClient, managedClient.clipboardData); // Begin streaming audio input if possible requestAudioStream(client); @@ -421,28 +424,43 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', // Handle any received clipboard data client.onclipboard = function clientClipboardReceived(stream, mimetype) { - // Only text/plain is supported for now - if (mimetype !== "text/plain") { - stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); - return; + var reader; + + // If the received data is text, read it as a simple string + if (/^text\//.exec(mimetype)) { + + reader = new Guacamole.StringReader(stream); + + // Assemble received data into a single string + var data = ''; + reader.ontext = function textReceived(text) { + data += text; + }; + + // Set clipboard contents once stream is finished + reader.onend = function textComplete() { + $rootScope.$apply(function updateClipboard() { + managedClient.clipboardData = new ClipboardData({ + type : mimetype, + data : data + }); + }); + }; + } - var reader = new Guacamole.StringReader(stream); - var data = ""; - - // Append any received data to buffer - reader.ontext = function clipboard_text_received(text) { - data += text; - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // Update state when done - reader.onend = function clipboard_text_end() { - $rootScope.$apply(function updateClipboard() { - managedClient.clipboardData = data; - clipboardService.setLocalClipboard(data); - }); - }; + // Otherwise read the clipboard data as a Blob + else { + reader = new Guacamole.BlobReader(stream, mimetype); + reader.onend = function blobComplete() { + $rootScope.$apply(function updateClipboard() { + managedClient.clipboardData = new ClipboardData({ + type : mimetype, + data : reader.getBlob() + }); + }); + }; + } }; @@ -527,6 +545,46 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; + /** + * Sends the given clipboard data over the given Guacamole client, setting + * the contents of the remote clipboard to the data provided. + * + * @param {ManagedClient} managedClient + * The ManagedClient over which the given clipboard data is to be sent. + * + * @param {ClipboardData} data + * The clipboard data to send. + */ + ManagedClient.setClipboard = function setClipboard(managedClient, data) { + + var writer; + + // Create stream with proper mimetype + var stream = managedClient.client.createClipboardStream(data.type); + + // Send data as a string if it is stored as a string + if (typeof data.data === 'string') { + writer = new Guacamole.StringWriter(stream); + writer.sendText(data.data); + writer.sendEnd(); + } + + // Otherwise, assume the data is a File/Blob + else { + + // Write File/Blob asynchronously + writer = new Guacamole.BlobWriter(stream); + writer.oncomplete = function clipboardSent() { + writer.sendEnd(); + }; + + // Begin sending data + writer.sendBlob(data.data); + + } + + }; + return ManagedClient; }]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/clipboard/clipboardModule.js b/guacamole/src/main/webapp/app/clipboard/clipboardModule.js new file mode 100644 index 000000000..b7528a4d3 --- /dev/null +++ b/guacamole/src/main/webapp/app/clipboard/clipboardModule.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * The module for code used to manipulate/observe the clipboard. + */ +angular.module('clipboard', []); diff --git a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js similarity index 72% rename from guacamole/src/main/webapp/app/client/directives/guacClipboard.js rename to guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index 745577ad9..2307f8755 100644 --- a/guacamole/src/main/webapp/app/client/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -27,7 +27,11 @@ * "data" attribute, changes to clipboard data will be broadcast on the scope * via "guacClipboard" events. */ -angular.module('client').directive('guacClipboard', [function guacClipboard() { +angular.module('clipboard').directive('guacClipboard', ['$injector', + function guacClipboard($injector) { + + // Required types + var ClipboardData = $injector.get('ClipboardData'); /** * Configuration object for the guacClipboard directive. @@ -37,7 +41,7 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { var config = { restrict : 'E', replace : true, - templateUrl : 'app/client/templates/guacClipboard.html' + templateUrl : 'app/clipboard/templates/guacClipboard.html' }; // Scope properties exposed by the guacClipboard directive @@ -51,7 +55,7 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { * field. Changes to this value will be rendered within the field and, * if possible, will be pushed to the local clipboard. * - * @type String|Blob + * @type ClipboardData */ data : '=' @@ -81,17 +85,42 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { * contents received while those keys were pressed. All keys not * currently pressed will not have entries within this map. * - * @type Object. + * @type Object. */ var clipboardDataFromKey = {}; /** - * The URL of the image is currently stored within the clipboard. If - * the clipboard currently contains text, this will be null. + * The FileReader to use to read File or Blob data received from the + * clipboard. * - * @type String + * @type FileReader */ - $scope.imageURL = null; + var reader = new FileReader(); + + /** + * Properties which contain the current clipboard contents. Each + * property is mutually exclusive, and will only contain data if the + * clipboard contents are of a particular type. + */ + $scope.content = { + + /** + * The text contents of the clipboard. If the clipboard contents + * is not text, this will be null. + * + * @type String + */ + text : null, + + /** + * The URL of the image is currently stored within the clipboard. If + * the clipboard currently contains text, this will be null. + * + * @type String + */ + imageURL : null + + }; // Intercept paste events, handling image data specifically $element[0].addEventListener('paste', function dataPasted(e) { @@ -103,9 +132,15 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { // If the item is an image, attempt to read that image if (items[i].kind === 'file' && /^image\//.exec(items[i].type)) { + // Retrieven contents as a File + var file = items[i].getAsFile(); + // Set clipboard data to contents $scope.$apply(function setClipboardData() { - $scope.data = items[i].getAsFile(); + $scope.data = new ClipboardData({ + type : file.type, + data : file + }); }); // Do not paste @@ -127,7 +162,7 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { * otherwise. */ $scope.isImage = function isImage() { - return !!$scope.imageURL; + return !!$scope.content.imageURL; }; /** @@ -149,10 +184,25 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { $scope.resetClipboard = function resetClipboard() { // Reset to blank - $scope.data = ''; + $scope.data = new ClipboardData({ + type : 'text/plain', + data : '' + }); }; + // Keep data in sync with changes to text + $scope.$watch('content.text', function textChanged(text) { + + if (text) { + $scope.data = new ClipboardData({ + type : $scope.data.type, + data : text + }); + } + + }); + // Watch clipboard for new data, associating it with any pressed keys $scope.$watch('data', function clipboardChanged(data) { @@ -160,21 +210,35 @@ angular.module('client').directive('guacClipboard', [function guacClipboard() { for (var keysym in keysCurrentlyPressed) clipboardDataFromKey[keysym] = data; - // Revoke old image URL, if any - if ($scope.imageURL) { - URL.revokeObjectURL($scope.imageURL); - $scope.imageURL = null; + // Stop any current read process + reader.abort(); + + // If the clipboard data is a string, render it as text + if (typeof data.data === 'string') { + $scope.content.text = data.data; + $scope.content.imageURL = null; } - // If the copied data was an image, display it as such - if (data instanceof Blob) { - $scope.imageURL = URL.createObjectURL(data); - $rootScope.$broadcast('guacClipboard', data.type, data); + // Render Blob/File contents based on mimetype + else if (data.data instanceof Blob) { + + // If the copied data was an image, display it as such + if (/^image\//.exec(data.type)) { + reader.onload = function updateImageURL() { + $scope.$apply(function imageURLLoaded() { + $scope.content.text = null; + $scope.content.imageURL = reader.result; + }); + }; + reader.readAsDataURL(data.data); + } + + // Ignore other data types + } - // Otherwise, the data is simply text - else - $rootScope.$broadcast('guacClipboard', 'text/plain', data); + // Notify of change + $rootScope.$broadcast('guacClipboard', data); }); diff --git a/guacamole/src/main/webapp/app/client/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js similarity index 77% rename from guacamole/src/main/webapp/app/client/services/clipboardService.js rename to guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index a81dd0e73..ac0640004 100644 --- a/guacamole/src/main/webapp/app/client/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -20,7 +20,7 @@ /** * A service for accessing local clipboard data. */ -angular.module('client').factory('clipboardService', ['$injector', +angular.module('clipboard').factory('clipboardService', ['$injector', function clipboardService($injector) { // Get required services @@ -58,14 +58,14 @@ angular.module('client').factory('clipboardService', ['$injector', /** * Sets the local clipboard, if possible, to the given text. * - * @param {String} text - * The text to which the local clipboard should be set. + * @param {ClipboardData} data + * The data to assign to 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) { + service.setLocalClipboard = function setLocalClipboard(data) { var deferred = $q.defer(); @@ -73,9 +73,29 @@ angular.module('client').factory('clipboardService', ['$injector', var originalElement = document.activeElement; // Copy the given value into the clipboard DOM element - clipboardContent.value = text; + clipboardContent.value = 'X'; clipboardContent.select(); + // Override copied contents of clipboard with the provided value + clipboardContent.oncopy = function overrideContent(e) { + + // Override the contents of the clipboard + e.preventDefault(); + + // Remove anything already present within the clipboard + var items = e.clipboardData.items; + items.clear(); + + // If the provided data is a string, add it as such + if (typeof data.data === 'string') + items.add(data.data, data.type); + + // Otherwise, add as a File + else + items.add(new File([data.data], 'data', { type : data.type })); + + }; + // Attempt to copy data from clipboard element into local clipboard if (document.execCommand('copy')) deferred.resolve(); @@ -93,7 +113,7 @@ angular.module('client').factory('clipboardService', ['$injector', /** * Get the current value of the local clipboard. * - * @return {Promise} + * @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. @@ -114,9 +134,14 @@ angular.module('client').factory('clipboardService', ['$injector', clipboardContent.focus(); clipboardContent.select(); + // FIXME: Only handling text data + // Attempt paste local clipboard into clipboard DOM element if (document.activeElement === clipboardContent && document.execCommand('paste')) - deferred.resolve(clipboardContent.value); + deferred.resolve(new ClipboardData({ + type : 'text/plain', + data : clipboardContent.value + })); else deferred.reject(); diff --git a/guacamole/src/main/webapp/app/client/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css similarity index 100% rename from guacamole/src/main/webapp/app/client/styles/clipboard.css rename to guacamole/src/main/webapp/app/clipboard/styles/clipboard.css diff --git a/guacamole/src/main/webapp/app/client/templates/guacClipboard.html b/guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html similarity index 66% rename from guacamole/src/main/webapp/app/client/templates/guacClipboard.html rename to guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html index af4501956..8cb77c1e4 100644 --- a/guacamole/src/main/webapp/app/client/templates/guacClipboard.html +++ b/guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html @@ -1,9 +1,9 @@
- +
- +
diff --git a/guacamole/src/main/webapp/app/clipboard/types/ClipboardData.js b/guacamole/src/main/webapp/app/clipboard/types/ClipboardData.js new file mode 100644 index 000000000..f9f573a56 --- /dev/null +++ b/guacamole/src/main/webapp/app/clipboard/types/ClipboardData.js @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Provides the ClipboardData class used for interchange between the + * guacClipboard directive, clipboardService service, etc. + */ +angular.module('clipboard').factory('ClipboardData', [function defineClipboardData() { + + /** + * Arbitrary data which can be contained by the clipboard. + * + * @constructor + * @param {ClipboardData|Object} [template={}] + * The object whose properties should be copied within the new + * ClipboardData. + */ + var ClipboardData = function ClipboardData(template) { + + // Use empty object by default + template = template || {}; + + /** + * The mimetype of the data currently stored within the clipboard. + * + * @type String + */ + this.type = template.type || 'text/plain'; + + /** + * The data currently stored within the clipboard. Depending on the + * nature of the stored data, this may be either a String, a Blob, or a + * File. + * + * @type String|Blob|File + */ + this.data = template.data || ''; + + }; + + return ClipboardData; + +}]); From 17e97e43116238000391b60fcb87cbfd3d4cc98a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 23 Jun 2016 00:39:27 -0700 Subject: [PATCH 09/31] GUACAMOLE-55: Improve styling of copied images. --- .../webapp/app/clipboard/styles/clipboard.css | 3 +++ guacamole/src/main/webapp/images/checker.png | Bin 0 -> 208 bytes 2 files changed, 3 insertions(+) create mode 100644 guacamole/src/main/webapp/images/checker.png diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index cade1aecb..9e2e2b227 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -35,6 +35,7 @@ .clipboard .image-clipboard { text-align: center; + padding: 1em; } .clipboard .image-clipboard .reset-button { @@ -48,4 +49,6 @@ .clipboard .image-clipboard img { max-width: 100%; max-height: 480px; + border: 1px solid black; + background: url('images/checker.png'); } diff --git a/guacamole/src/main/webapp/images/checker.png b/guacamole/src/main/webapp/images/checker.png new file mode 100644 index 0000000000000000000000000000000000000000..06f89032de511444a8dce5c8e941ece077da5304 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=f!ePFaVKM%!0SaoLG}_)Usv{9%zPYHJinNt9{`0UOI#yLobz*Y zQ}ap~oQqNuOHxx5$}>wc6x=<11Hv2m#DR*mJzX3_EKVoSu`GU8&t~)B&CSg}mPpQ! t&|q22#>2)O!Eto+#*H5{6!_|y8Q4=ff@kl#S_0I@;OXk;vd$@?2>?DoJt_bI literal 0 HcmV?d00001 From 2a20aefa9284ca42a4862dc1700227703cbcf04b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 27 Jun 2016 18:33:15 -0700 Subject: [PATCH 10/31] GUACAMOLE-55: Do not attempt to send empty text blobs. --- guacamole-common-js/src/main/webapp/modules/StringWriter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/StringWriter.js b/guacamole-common-js/src/main/webapp/modules/StringWriter.js index 5a865e17d..abf1684ba 100644 --- a/guacamole-common-js/src/main/webapp/modules/StringWriter.js +++ b/guacamole-common-js/src/main/webapp/modules/StringWriter.js @@ -170,7 +170,8 @@ Guacamole.StringWriter = function(stream) { * @param {String} text The text to send. */ this.sendText = function(text) { - array_writer.sendData(__encode_utf8(text)); + if (text.length) + array_writer.sendData(__encode_utf8(text)); }; /** From 6e4e6454f77024bc77cc7021b0dee4d27c3cd0dc Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 27 Jun 2016 19:32:18 -0700 Subject: [PATCH 11/31] GUACAMOLE-55: Switch to a content-editable div for the clipboard (rather than a textarea). --- .../app/clipboard/directives/guacClipboard.js | 292 +++++++++++++----- .../webapp/app/clipboard/styles/clipboard.css | 20 +- .../clipboard/templates/guacClipboard.html | 10 +- 3 files changed, 231 insertions(+), 91 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index 2307f8755..a55f398a6 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -70,6 +70,14 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', var $window = $injector.get('$window'); var clipboardService = $injector.get('clipboardService'); + /** + * Reference to the window.document object. + * + * @private + * @type HTMLDocument + */ + var document = $window.document; + /** * Map of all currently pressed keys by keysym. If a particular key is * currently pressed, the value stored under that key's keysym within @@ -98,35 +106,184 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', var reader = new FileReader(); /** - * Properties which contain the current clipboard contents. Each - * property is mutually exclusive, and will only contain data if the - * clipboard contents are of a particular type. + * The content-editable DOM element which will contain the clipboard + * contents within the user interface provided by this directive. + * + * @type Element */ - $scope.content = { + var element = $element[0]; - /** - * The text contents of the clipboard. If the clipboard contents - * is not text, this will be null. - * - * @type String - */ - text : null, + /** + * Modifies the contents of the given element such that it contains + * only plain text. All non-text child elements will be stripped and + * replaced with their text equivalents. As this function performs the + * conversion through incremental changes only, cursor position within + * the given element is preserved. + * + * @param {Element} element + * The elements whose contents should be converted to plain text. + */ + var convertToText = function convertToText(element) { - /** - * The URL of the image is currently stored within the clipboard. If - * the clipboard currently contains text, this will be null. - * - * @type String - */ - imageURL : null + // For each child of the given element + var current = element.firstChild; + while (current) { + + // Preserve the next child in the list, in case the current + // node is replaced + var next = current.nextSibling; + + // If the child is not already a text node, replace it with its + // own text contents + if (current.nodeType !== Node.TEXT_NODE) { + var textNode = document.createTextNode(current.textContent); + current.parentElement.replaceChild(textNode, current); + } + + // Advance to next child + current = next; + + } + + }; + + /** + * Parses the given data URL, returning its decoded contents as a new + * Blob. If the URL is not a valid data URL, null will be returned + * instead. + * + * @param {String} url + * The data URL to parse. + * + * @returns {Blob} + * A new Blob containing the decoded contents of the data URL, or + * null if the URL is not a valid data URL. + */ + var parseDataURL = function parseDataURL(url) { + + // Parse given string as a data URL + var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url); + if (!result) + return null; + + // Pull the mimetype and base64 contents of the data URL + var type = result[1]; + var data = $window.atob(result[2]); + + // Convert the decoded binary string into a typed array + var buffer = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) + buffer[i] = data.charCodeAt(i); + + // Produce a proper blob containing the data and type provided in + // the data URL + return new Blob([buffer], { type : type }); + + }; + + /** + * Replaces the current text content of the given element with the + * given text. To avoid affecting the position of the cursor within an + * editable element, or firing unnecessary DOM modification events, the + * underlying textContent property of the element is only + * touched if doing so would actually change the text. + * + * @param {Element} element + * The element whose text content should be changed. + * + * @param {String} text + * The text content to assign to the given element. + */ + var setTextContent = function setTextContent(element, text) { + + // Strip out any non-text content while preserving cursor position + convertToText(element); + + // Reset text content only if doing so will actually change the content + if (element.textContent !== text) + element.textContent = text; + + }; + + /** + * Returns the URL of the single image within the given element, if the + * element truly contains only one child and that child is an image. If + * the content of the element is mixed or not an image, null is + * returned. + * + * @param {Element} element + * The element whose image content should be retrieved. + * + * @returns {String} + * The URL of the image contained within the given element, if that + * element contains only a single child element which happens to be + * an image, or null if the content of the element is not purely an + * image. + */ + var getImageContent = function getImageContent(element) { + + // Return the source of the single child element, if it is an image + var firstChild = element.firstChild; + if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling) + return firstChild.getAttribute('src'); + + // Otherwise, the content of this element is not simply an image + return null; + + }; + + /** + * Replaces the current contents of the given element with a single + * image having the given URL. To avoid affecting the position of the + * cursor within an editable element, or firing unnecessary DOM + * modification events, the content of the element is only touched if + * doing so would actually change content. + * + * @param {Element} element + * The element whose image content should be changed. + * + * @param {String} url + * The URL of the image which should be assigned as the contents of + * the given element. + */ + var setImageContent = function setImageContent(element, url) { + + // Retrieve the URL of the current image contents, if any + var currentImage = getImageContent(element); + + // If the current contents are not the given image (or not an image + // at all), reassign the contents + if (currentImage !== url) { + + // Clear current contents + element.innerHTML = ''; + + // Add a new image as the sole contents of the element + var img = document.createElement('img'); + img.src = url; + element.appendChild(img); + + } }; // Intercept paste events, handling image data specifically - $element[0].addEventListener('paste', function dataPasted(e) { + element.addEventListener('paste', function dataPasted(e) { + + // Always clear the current clipboard contents upon paste + element.innerHTML = ''; + + // If we can't read the clipboard contents at all, abort + var clipboardData = e.clipboardData; + if (!clipboardData) + return; + + // If the clipboard contents cannot be read as blobs, abort + var items = clipboardData.items; + if (!items) + return; // For each item within the clipboard - var items = e.clipboardData.items; for (var i = 0; i < items.length; i++) { // If the item is an image, attempt to read that image @@ -154,70 +311,68 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', }); /** - * Returns whether the clipboard currently contains only an image, the - * URL of which is exposed via the imageURL property. - * - * @returns {Boolean} - * true if the current clipboard contains only an image, false - * otherwise. + * Rereads the contents of the clipboard field, updating the + * ClipboardData object on the scope as necessary. The type of data + * stored within the ClipboardData object will be heuristically + * determined from the HTML contents of the clipboard field. */ - $scope.isImage = function isImage() { - return !!$scope.content.imageURL; - }; + var updateClipboardData = function updateClipboardData() { - /** - * Returns whether the clipboard currently contains only text. - * - * @returns {Boolean} - * true if the clipboard currently contains only text, false - * otherwise. - */ - $scope.isText = function isText() { - return !$scope.isImage(); - }; + // If the clipboard contains a single image, parse and assign the + // image data to the internal clipboard + var currentImage = getImageContent(element); + if (currentImage) { - /** - * Clears the current clipboard contents. If the clipboard currently - * displays an image, this will also return to a text-based clipboard - * display. - */ - $scope.resetClipboard = function resetClipboard() { + // Convert the image's data URL into a blob + var blob = parseDataURL(currentImage); + if (blob) { - // Reset to blank - $scope.data = new ClipboardData({ - type : 'text/plain', - data : '' + // Complete the assignment if conversion was successful + $scope.$evalAsync(function assignClipboardData() { + $scope.data = new ClipboardData({ + type : blob.type, + data : blob + }); + }); + + return; + + } + + } // end if clipboard is an image + + // If data does not appear to be an image, or image decoding fails, + // assume clipboard contents are text + $scope.$evalAsync(function assignClipboardText() { + $scope.data = new ClipboardData({ + type : 'text/plain', + data : element.textContent + }); }); }; - // Keep data in sync with changes to text - $scope.$watch('content.text', function textChanged(text) { - - if (text) { - $scope.data = new ClipboardData({ - type : $scope.data.type, - data : text - }); - } - - }); + // Update the internally-stored clipboard data when events are fired + // that indicate the clipboard field may have been changed + element.addEventListener('input', updateClipboardData); + element.addEventListener('DOMCharacterDataModified', updateClipboardData); + element.addEventListener('DOMNodeInserted', updateClipboardData); + element.addEventListener('DOMNodeRemoved', updateClipboardData); // Watch clipboard for new data, associating it with any pressed keys - $scope.$watch('data', function clipboardChanged(data) { + $scope.$watch('data', function clipboardDataChanged(data) { // Associate new clipboard data with any currently-pressed key for (var keysym in keysCurrentlyPressed) clipboardDataFromKey[keysym] = data; // Stop any current read process - reader.abort(); + if (reader.readyState === 1) + reader.abort(); // If the clipboard data is a string, render it as text - if (typeof data.data === 'string') { - $scope.content.text = data.data; - $scope.content.imageURL = null; - } + if (typeof data.data === 'string') + setTextContent(element, data.data); // Render Blob/File contents based on mimetype else if (data.data instanceof Blob) { @@ -225,10 +380,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // If the copied data was an image, display it as such if (/^image\//.exec(data.type)) { reader.onload = function updateImageURL() { - $scope.$apply(function imageURLLoaded() { - $scope.content.text = null; - $scope.content.imageURL = reader.result; - }); + setImageContent(element, reader.result); }; reader.readAsDataURL(data.data); } diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index 9e2e2b227..10e3eaf66 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -17,8 +17,7 @@ * under the License. */ -.clipboard .image-clipboard, -.clipboard .text-clipboard textarea { +.clipboard { position: relative; border: 1px solid #AAA; -moz-border-radius: 0.25em; @@ -26,16 +25,11 @@ -khtml-border-radius: 0.25em; border-radius: 0.25em; background: white; -} - -.clipboard .text-clipboard textarea { width: 100%; + height: 2in; white-space: pre; -} - -.clipboard .image-clipboard { - text-align: center; - padding: 1em; + overflow: auto; + padding: 0.25em; } .clipboard .image-clipboard .reset-button { @@ -46,9 +40,11 @@ min-width: 0; } -.clipboard .image-clipboard img { +.clipboard img { max-width: 100%; - max-height: 480px; + max-height: 100%; + display: block; + margin: 0 auto; border: 1px solid black; background: url('images/checker.png'); } diff --git a/guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html b/guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html index 8cb77c1e4..b3b604d4c 100644 --- a/guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html +++ b/guacamole/src/main/webapp/app/clipboard/templates/guacClipboard.html @@ -1,9 +1 @@ -
-
- -
-
- - -
-
+
From ea5ee1825b2190d2c62b3873d20e275377b059ab Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 13:43:31 -0700 Subject: [PATCH 12/31] GUACAMOLE-55: Update clipboardService to support non-text contents. --- .../app/clipboard/directives/guacClipboard.js | 162 +-------- .../clipboard/services/clipboardService.js | 327 +++++++++++++++--- .../webapp/app/clipboard/styles/clipboard.css | 9 + 3 files changed, 293 insertions(+), 205 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index a55f398a6..3a44d382f 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -113,160 +113,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', */ var element = $element[0]; - /** - * Modifies the contents of the given element such that it contains - * only plain text. All non-text child elements will be stripped and - * replaced with their text equivalents. As this function performs the - * conversion through incremental changes only, cursor position within - * the given element is preserved. - * - * @param {Element} element - * The elements whose contents should be converted to plain text. - */ - var convertToText = function convertToText(element) { - - // For each child of the given element - var current = element.firstChild; - while (current) { - - // Preserve the next child in the list, in case the current - // node is replaced - var next = current.nextSibling; - - // If the child is not already a text node, replace it with its - // own text contents - if (current.nodeType !== Node.TEXT_NODE) { - var textNode = document.createTextNode(current.textContent); - current.parentElement.replaceChild(textNode, current); - } - - // Advance to next child - current = next; - - } - - }; - - /** - * Parses the given data URL, returning its decoded contents as a new - * Blob. If the URL is not a valid data URL, null will be returned - * instead. - * - * @param {String} url - * The data URL to parse. - * - * @returns {Blob} - * A new Blob containing the decoded contents of the data URL, or - * null if the URL is not a valid data URL. - */ - var parseDataURL = function parseDataURL(url) { - - // Parse given string as a data URL - var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url); - if (!result) - return null; - - // Pull the mimetype and base64 contents of the data URL - var type = result[1]; - var data = $window.atob(result[2]); - - // Convert the decoded binary string into a typed array - var buffer = new Uint8Array(data.length); - for (var i = 0; i < data.length; i++) - buffer[i] = data.charCodeAt(i); - - // Produce a proper blob containing the data and type provided in - // the data URL - return new Blob([buffer], { type : type }); - - }; - - /** - * Replaces the current text content of the given element with the - * given text. To avoid affecting the position of the cursor within an - * editable element, or firing unnecessary DOM modification events, the - * underlying textContent property of the element is only - * touched if doing so would actually change the text. - * - * @param {Element} element - * The element whose text content should be changed. - * - * @param {String} text - * The text content to assign to the given element. - */ - var setTextContent = function setTextContent(element, text) { - - // Strip out any non-text content while preserving cursor position - convertToText(element); - - // Reset text content only if doing so will actually change the content - if (element.textContent !== text) - element.textContent = text; - - }; - - /** - * Returns the URL of the single image within the given element, if the - * element truly contains only one child and that child is an image. If - * the content of the element is mixed or not an image, null is - * returned. - * - * @param {Element} element - * The element whose image content should be retrieved. - * - * @returns {String} - * The URL of the image contained within the given element, if that - * element contains only a single child element which happens to be - * an image, or null if the content of the element is not purely an - * image. - */ - var getImageContent = function getImageContent(element) { - - // Return the source of the single child element, if it is an image - var firstChild = element.firstChild; - if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling) - return firstChild.getAttribute('src'); - - // Otherwise, the content of this element is not simply an image - return null; - - }; - - /** - * Replaces the current contents of the given element with a single - * image having the given URL. To avoid affecting the position of the - * cursor within an editable element, or firing unnecessary DOM - * modification events, the content of the element is only touched if - * doing so would actually change content. - * - * @param {Element} element - * The element whose image content should be changed. - * - * @param {String} url - * The URL of the image which should be assigned as the contents of - * the given element. - */ - var setImageContent = function setImageContent(element, url) { - - // Retrieve the URL of the current image contents, if any - var currentImage = getImageContent(element); - - // If the current contents are not the given image (or not an image - // at all), reassign the contents - if (currentImage !== url) { - - // Clear current contents - element.innerHTML = ''; - - // Add a new image as the sole contents of the element - var img = document.createElement('img'); - img.src = url; - element.appendChild(img); - - } - - }; - // Intercept paste events, handling image data specifically element.addEventListener('paste', function dataPasted(e) { @@ -320,11 +166,11 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // If the clipboard contains a single image, parse and assign the // image data to the internal clipboard - var currentImage = getImageContent(element); + var currentImage = clipboardService.getImageContent(element); if (currentImage) { // Convert the image's data URL into a blob - var blob = parseDataURL(currentImage); + var blob = clipboardService.parseDataURL(currentImage); if (blob) { // Complete the assignment if conversion was successful @@ -372,7 +218,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // If the clipboard data is a string, render it as text if (typeof data.data === 'string') - setTextContent(element, data.data); + clipboardService.setTextContent(element, data.data); // Render Blob/File contents based on mimetype else if (data.data instanceof Blob) { @@ -380,7 +226,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // If the copied data was an image, display it as such if (/^image\//.exec(data.type)) { reader.onload = function updateImageURL() { - setImageContent(element, reader.result); + clipboardService.setImageContent(element, reader.result); }; reader.readAsDataURL(data.data); } diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index ac0640004..0dc2b7aa3 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -24,36 +24,134 @@ angular.module('clipboard').factory('clipboardService', ['$injector', function clipboardService($injector) { // Get required services - var $q = $injector.get('$q'); + var $q = $injector.get('$q'); + var $window = $injector.get('$window'); + + // Required types + var ClipboardData = $injector.get('ClipboardData'); var service = {}; /** - * A div which is used to hide the clipboard textarea and remove it from - * document flow. + * Reference to the window.document object. * - * @type Element + * @private + * @type HTMLDocument */ - var clipElement = document.createElement('div'); + var document = $window.document; /** * The textarea that will be used to hold the local clipboard contents. * * @type Element */ - var clipboardContent = document.createElement('textarea'); + var clipboardContent = document.createElement('div'); - // Ensure textarea is selectable but not visible - clipElement.appendChild(clipboardContent); - clipElement.style.position = 'fixed'; - clipElement.style.width = '1px'; - clipElement.style.height = '1px'; - clipElement.style.left = '-1px'; - clipElement.style.top = '-1px'; - clipElement.style.overflow = 'hidden'; + // Ensure clipboard target is selectable but not visible + clipboardContent.setAttribute('contenteditable', 'true'); + clipboardContent.className = 'clipboard-service-target'; - // Add textarea to DOM - document.body.appendChild(clipElement); + // Add clipboard target to DOM + document.body.appendChild(clipboardContent); + + /** + * A stack of past node selection ranges. A range convering the nodes + * currently selected within the document can be pushed onto this stack + * with pushSelection(), and the most recently pushed selection can be + * popped off the stack (and thus re-selected) with popSelection(). + * + * @type Range[] + */ + var selectionStack = []; + + /** + * Pushes the current selection range to the selection stack such that it + * can later be restored with popSelection(). + */ + var pushSelection = function pushSelection() { + + // Add a range representing the current selection to the stack + var selection = $window.getSelection(); + if (selection.getRangeAt && selection.rangeCount) + selectionStack.push(selection.getRangeAt(0)); + + }; + + /** + * Pops a selection range off the selection stack restoring the document's + * previous selection state. The selection range will be the most recent + * selection range pushed by pushSelection(). If there are no selection + * ranges currently on the stack, this function has no effect. + */ + var popSelection = function popSelection() { + + // Pull one selection range from the stack + var range = selectionStack.pop(); + if (!range) + return; + + // Replace any current selection with the retrieved selection + var selection = $window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + }; + + /** + * Selects all nodes within the given element. This will replace the + * current selection with a new selection range that covers the element's + * contents. If the original selection should be preserved, use + * pushSelection() and popSelection(). + * + * @param {Element} element + * The element whose contents should be selected. + */ + var selectAll = function selectAll(element) { + + // Generate a range which selects all nodes within the given element + var range = document.createRange(); + range.selectNodeContents(element); + + // Replace any current selection with the generated range + var selection = $window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + }; + + /** + * Modifies the contents of the given element such that it contains only + * plain text. All non-text child elements will be stripped and replaced + * with their text equivalents. As this function performs the conversion + * through incremental changes only, cursor position within the given + * element is preserved. + * + * @param {Element} element + * The elements whose contents should be converted to plain text. + */ + var convertToText = function convertToText(element) { + + // For each child of the given element + var current = element.firstChild; + while (current) { + + // Preserve the next child in the list, in case the current + // node is replaced + var next = current.nextSibling; + + // If the child is not already a text node, replace it with its + // own text contents + if (current.nodeType !== Node.TEXT_NODE) { + var textNode = document.createTextNode(current.textContent); + current.parentElement.replaceChild(textNode, current); + } + + // Advance to next child + current = next; + + } + + }; /** * Sets the local clipboard, if possible, to the given text. @@ -71,30 +169,20 @@ angular.module('clipboard').factory('clipboardService', ['$injector', // Track the originally-focused element prior to changing focus var originalElement = document.activeElement; + pushSelection(); // Copy the given value into the clipboard DOM element - clipboardContent.value = 'X'; - clipboardContent.select(); + if (typeof data.data === 'string') + clipboardContent.textContent = data.data; + else { + clipboardContent.innerHTML = ''; + var img = document.createElement('img'); + img.src = URL.createObjectURL(data.data); + clipboardContent.appendChild(img); + } - // Override copied contents of clipboard with the provided value - clipboardContent.oncopy = function overrideContent(e) { - - // Override the contents of the clipboard - e.preventDefault(); - - // Remove anything already present within the clipboard - var items = e.clipboardData.items; - items.clear(); - - // If the provided data is a string, add it as such - if (typeof data.data === 'string') - items.add(data.data, data.type); - - // Otherwise, add as a File - else - items.add(new File([data.data], 'data', { type : data.type })); - - }; + // Select all data within the clipboard target + selectAll(clipboardContent); // Attempt to copy data from clipboard element into local clipboard if (document.execCommand('copy')) @@ -106,10 +194,128 @@ angular.module('clipboard').factory('clipboardService', ['$injector', // restoring whichever element was originally focused clipboardContent.blur(); originalElement.focus(); + popSelection(); return deferred.promise; }; + /** + * Parses the given data URL, returning its decoded contents as a new Blob. + * If the URL is not a valid data URL, null will be returned instead. + * + * @param {String} url + * The data URL to parse. + * + * @returns {Blob} + * A new Blob containing the decoded contents of the data URL, or null + * if the URL is not a valid data URL. + */ + service.parseDataURL = function parseDataURL(url) { + + // Parse given string as a data URL + var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url); + if (!result) + return null; + + // Pull the mimetype and base64 contents of the data URL + var type = result[1]; + var data = $window.atob(result[2]); + + // Convert the decoded binary string into a typed array + var buffer = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) + buffer[i] = data.charCodeAt(i); + + // Produce a proper blob containing the data and type provided in + // the data URL + return new Blob([buffer], { type : type }); + + }; + + /** + * Replaces the current text content of the given element with the given + * text. To avoid affecting the position of the cursor within an editable + * element, or firing unnecessary DOM modification events, the underlying + * textContent property of the element is only touched if + * doing so would actually change the text. + * + * @param {Element} element + * The element whose text content should be changed. + * + * @param {String} text + * The text content to assign to the given element. + */ + service.setTextContent = function setTextContent(element, text) { + + // Strip out any non-text content while preserving cursor position + convertToText(element); + + // Reset text content only if doing so will actually change the content + if (element.textContent !== text) + element.textContent = text; + + }; + + /** + * Returns the URL of the single image within the given element, if the + * element truly contains only one child and that child is an image. If the + * content of the element is mixed or not an image, null is returned. + * + * @param {Element} element + * The element whose image content should be retrieved. + * + * @returns {String} + * The URL of the image contained within the given element, if that + * element contains only a single child element which happens to be an + * image, or null if the content of the element is not purely an image. + */ + service.getImageContent = function getImageContent(element) { + + // Return the source of the single child element, if it is an image + var firstChild = element.firstChild; + if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling) + return firstChild.getAttribute('src'); + + // Otherwise, the content of this element is not simply an image + return null; + + }; + + /** + * Replaces the current contents of the given element with a single image + * having the given URL. To avoid affecting the position of the cursor + * within an editable element, or firing unnecessary DOM modification + * events, the content of the element is only touched if doing so would + * actually change content. + * + * @param {Element} element + * The element whose image content should be changed. + * + * @param {String} url + * The URL of the image which should be assigned as the contents of the + * given element. + */ + service.setImageContent = function setImageContent(element, url) { + + // Retrieve the URL of the current image contents, if any + var currentImage = service.getImageContent(element); + + // If the current contents are not the given image (or not an image + // at all), reassign the contents + if (currentImage !== url) { + + // Clear current contents + element.innerHTML = ''; + + // Add a new image as the sole contents of the element + var img = document.createElement('img'); + img.src = url; + element.appendChild(img); + + } + + }; + /** * Get the current value of the local clipboard. * @@ -124,24 +330,50 @@ angular.module('clipboard').factory('clipboardService', ['$injector', // 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() { + $window.setTimeout(function deferredClipboardRead() { // Track the originally-focused element prior to changing focus var originalElement = document.activeElement; + pushSelection(); // Clear and select the clipboard DOM element - clipboardContent.value = ''; + clipboardContent.innerHTML = ''; clipboardContent.focus(); - clipboardContent.select(); - - // FIXME: Only handling text data + selectAll(clipboardContent); // Attempt paste local clipboard into clipboard DOM element - if (document.activeElement === clipboardContent && document.execCommand('paste')) - deferred.resolve(new ClipboardData({ - type : 'text/plain', - data : clipboardContent.value - })); + if (document.activeElement === clipboardContent && document.execCommand('paste')) { + + // If the pasted data is a single image, resolve with a blob + // containing that image + var currentImage = service.getImageContent(clipboardContent); + if (currentImage) { + + // Convert the image's data URL into a blob + var blob = service.parseDataURL(currentImage); + if (blob) { + deferred.resolve(new ClipboardData({ + type : blob.type, + data : blob + })); + } + + // Reject if conversion fails + else + deferred.reject(); + + } // end if clipboard is an image + + // Otherwise, assume the clipboard contains plain text + else + deferred.resolve(new ClipboardData({ + type : 'text/plain', + data : clipboardContent.textContent + })); + + } + + // Otherwise, reading from the clipboard has failed else deferred.reject(); @@ -149,6 +381,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector', // restoring whichever element was originally focused clipboardContent.blur(); originalElement.focus(); + popSelection(); }, 100); diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index 10e3eaf66..ab02af03f 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -48,3 +48,12 @@ border: 1px solid black; background: url('images/checker.png'); } + +.clipboard-service-target { + position: fixed; + left: -1px; + right: -1px; + width: 1px; + height: 1px; + overflow: hidden; +} From 16637b07321b2526dcca6869c193d6c9cf537043 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 14:00:53 -0700 Subject: [PATCH 13/31] GUACAMOLE-55: Prevent clipboard events generated by the clipboardService from disturbing the operations of the guacClipboard directive. --- .../app/clipboard/directives/guacClipboard.js | 6 +++--- .../app/clipboard/services/clipboardService.js | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index 3a44d382f..b4099b3b7 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -288,9 +288,9 @@ angular.module('clipboard').directive('guacClipboard', ['$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('copy', checkClipboard); + $window.addEventListener('cut', checkClipboard); + $window.addEventListener('focus', checkClipboard); // Clean up on destruction $scope.$on('$destroy', function destroyClipboard() { diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index 0dc2b7aa3..d15781672 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -54,6 +54,22 @@ angular.module('clipboard').factory('clipboardService', ['$injector', // Add clipboard target to DOM document.body.appendChild(clipboardContent); + /** + * Stops the propogation of the given event through the DOM tree. This is + * identical to invoking stopPropogation() on the event directly, except + * that this function is usable as an event handler itself. + * + * @param {Event} e + * The event whose propogation through the DOM tree should be stopped. + */ + var stopEventPropagation = function stopEventPropagation(e) { + e.stopPropagation(); + }; + + // Prevent events generated due to execCommand() from disturbing external things + clipboardContent.addEventListener('copy', stopEventPropagation); + clipboardContent.addEventListener('paste', stopEventPropagation); + /** * A stack of past node selection ranges. A range convering the nodes * currently selected within the document can be pushed onto this stack From f289be05b2732eae4c057c4227b53cd87a248541 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 14:50:16 -0700 Subject: [PATCH 14/31] GUACAMOLE-55: Attempt to set local clipboard with received data. --- .../src/main/webapp/app/clipboard/directives/guacClipboard.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index b4099b3b7..c3e8361fd 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -238,6 +238,9 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // Notify of change $rootScope.$broadcast('guacClipboard', data); + // Attempt to set local clipboard with received data + clipboardService.setLocalClipboard(data); + }); // Track pressed keys From 942fd5dd677f21af409ea40c086cad0a422b396e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 15:05:49 -0700 Subject: [PATCH 15/31] GUACAMOLE-55: Remove unnecessary style - there is no longer a textarea in the guac menu. --- guacamole/src/main/webapp/app/client/styles/guac-menu.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/styles/guac-menu.css b/guacamole/src/main/webapp/app/client/styles/guac-menu.css index 2ddb432d3..bc96eb922 100644 --- a/guacamole/src/main/webapp/app/client/styles/guac-menu.css +++ b/guacamole/src/main/webapp/app/client/styles/guac-menu.css @@ -65,10 +65,6 @@ margin-top: 1em; } -#guac-menu #clipboard-settings textarea { - font-size: 1em; -} - #guac-menu #mouse-settings .choice { text-align: center; } From 058665e1f5d9affb8e21b0376646c91884c9e967 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 16:10:03 -0700 Subject: [PATCH 16/31] GUACAMOLE-55: Define wait period before clipboard reads with a documented constant. --- .../app/clipboard/services/clipboardService.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index d15781672..94c31c3d6 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -32,6 +32,18 @@ angular.module('clipboard').factory('clipboardService', ['$injector', var service = {}; + /** + * The amount of time to wait before actually serving a request to read + * clipboard data, in milliseconds. Providing a reasonable delay between + * request and read attempt allows the cut/copy operation to settle, in + * case the data we are anticipating to be present is not actually present + * in the clipboard yet. + * + * @constant + * @type Number + */ + var CLIPBOARD_READ_DELAY = 100; + /** * Reference to the window.document object. * @@ -399,7 +411,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector', originalElement.focus(); popSelection(); - }, 100); + }, CLIPBOARD_READ_DELAY); return deferred.promise; }; From da098c42e13674a83214b61d70e257bae1f6f2f1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 17:25:22 -0700 Subject: [PATCH 17/31] GUACAMOLE-55: Use the files collection if the clipboard data does not define an items collection. --- .../app/clipboard/directives/guacClipboard.js | 68 ++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index c3e8361fd..c2a3b46ef 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -113,30 +113,66 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', */ var element = $element[0]; + /** + * Returns all files currently contained within the local clipboard, + * given a ClipboardEvent which should contain the current clipboard + * data. If no files are contained within the local clipboard, null + * is returned. + * + * @param {ClipboardEvent} e + * The ClipboardEvent which should contain the current clipboard + * data. + * + * @returns {File[]} + * An array of all files currently contained with the clipboard, as + * provided by the given ClipboardEvent, or null if no files are + * present. + */ + var getClipboardFiles = function getClipboardFiles(e) { + + // Pull the clipboard data object + var clipboardData = e.clipboardData || $window.clipboardData; + + // Read from the standard clipboard API items collection first + var items = clipboardData.items; + if (items) { + + var files = []; + + // Produce array of all files from clipboard data + for (var i = 0; i < items.length; i++) { + if (items[i].kind === 'file') + files.push(items[i].getAsFile()); + } + + return files; + + } + + // Failing that, try the files collection + if (clipboardData.files) + return clipboardData.files; + + // No files accessible within given data + return null; + + }; + // Intercept paste events, handling image data specifically element.addEventListener('paste', function dataPasted(e) { - // Always clear the current clipboard contents upon paste - element.innerHTML = ''; - - // If we can't read the clipboard contents at all, abort - var clipboardData = e.clipboardData; - if (!clipboardData) - return; - - // If the clipboard contents cannot be read as blobs, abort - var items = clipboardData.items; - if (!items) + // Read all files from the clipboard data within the event + var files = getClipboardFiles(e); + if (!files) return; // For each item within the clipboard - for (var i = 0; i < items.length; i++) { + for (var i = 0; i < files.length; i++) { - // If the item is an image, attempt to read that image - if (items[i].kind === 'file' && /^image\//.exec(items[i].type)) { + var file = files[i]; - // Retrieven contents as a File - var file = items[i].getAsFile(); + // If the file is an image, attempt to read that image + if (/^image\//.exec(file.type)) { // Set clipboard data to contents $scope.$apply(function setClipboardData() { From e4f7c6d05852fc148f02a957b04d969b13bc1baf Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 20:43:21 -0700 Subject: [PATCH 18/31] GUACAMOLE-55: The clipboard-service-target needs to be "white-space: pre" as well, or newline characters are truncated by setLocalClipboard(). --- guacamole/src/main/webapp/app/clipboard/styles/clipboard.css | 1 + 1 file changed, 1 insertion(+) diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index ab02af03f..886271d55 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -55,5 +55,6 @@ right: -1px; width: 1px; height: 1px; + white-space: pre; overflow: hidden; } From 6e1bbb3cf485439b775b18eb181161afdb92a921 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Jun 2016 20:44:24 -0700 Subject: [PATCH 19/31] GUACAMOLE-55: Remove unneeded reference to document. --- .../main/webapp/app/clipboard/directives/guacClipboard.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index c2a3b46ef..34071706a 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -70,14 +70,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', var $window = $injector.get('$window'); var clipboardService = $injector.get('clipboardService'); - /** - * Reference to the window.document object. - * - * @private - * @type HTMLDocument - */ - var document = $window.document; - /** * Map of all currently pressed keys by keysym. If a particular key is * currently pressed, the value stored under that key's keysym within From 42e41560d8c3be9b7af4a1c6cff1989453b37281 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 12:49:57 -0700 Subject: [PATCH 20/31] GUACAMOLE-55: Ensure

/

elements within the clipboard contents are rendered with normal spacing. --- guacamole/src/main/webapp/app/clipboard/styles/clipboard.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index 886271d55..aaa5d7d87 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -40,6 +40,11 @@ min-width: 0; } +.clipboard p, +.clipboard div { + margin: 0; +} + .clipboard img { max-width: 100%; max-height: 100%; From 880df109e3df80d7873502fe5c7e757d7a7c0366 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 12:50:22 -0700 Subject: [PATCH 21/31] GUACAMOLE-55: Remove unnecessary reset-button style. --- .../src/main/webapp/app/clipboard/styles/clipboard.css | 8 -------- 1 file changed, 8 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index aaa5d7d87..838b8b47a 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -32,14 +32,6 @@ padding: 0.25em; } -.clipboard .image-clipboard .reset-button { - position: absolute; - left: 0; - top: 0; - font-size: 0.75em; - min-width: 0; -} - .clipboard p, .clipboard div { margin: 0; From 2471cece7f07e0dd609e27f9f3c5500afb0c3a96 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 12:57:00 -0700 Subject: [PATCH 22/31] GUACAMOLE-55: Use own getTextContent() rather than element.textContent, as the textContent property does not preserve line breaks due to block level elements. --- .../app/clipboard/directives/guacClipboard.js | 2 +- .../clipboard/services/clipboardService.js | 71 +++++++++---------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index 34071706a..a04c06f32 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -220,7 +220,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', $scope.$evalAsync(function assignClipboardText() { $scope.data = new ClipboardData({ type : 'text/plain', - data : element.textContent + data : clipboardService.getTextContent(element) }); }); diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index 94c31c3d6..8d07ecad4 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -147,40 +147,6 @@ angular.module('clipboard').factory('clipboardService', ['$injector', }; - /** - * Modifies the contents of the given element such that it contains only - * plain text. All non-text child elements will be stripped and replaced - * with their text equivalents. As this function performs the conversion - * through incremental changes only, cursor position within the given - * element is preserved. - * - * @param {Element} element - * The elements whose contents should be converted to plain text. - */ - var convertToText = function convertToText(element) { - - // For each child of the given element - var current = element.firstChild; - while (current) { - - // Preserve the next child in the list, in case the current - // node is replaced - var next = current.nextSibling; - - // If the child is not already a text node, replace it with its - // own text contents - if (current.nodeType !== Node.TEXT_NODE) { - var textNode = document.createTextNode(current.textContent); - current.parentElement.replaceChild(textNode, current); - } - - // Advance to next child - current = next; - - } - - }; - /** * Sets the local clipboard, if possible, to the given text. * @@ -260,6 +226,39 @@ angular.module('clipboard').factory('clipboardService', ['$injector', }; + /** + * Returns the content of the given element as plain, unformatted text, + * preserving only individual characters and newlines. Formatting, images, + * etc. are not taken into account. + * + * @param {Element} element + * The element whose text content should be returned. + * + * @returns {String} + * The plain text contents of the given element, including newlines and + * spacing but otherwise without any formatting. + */ + service.getTextContent = function getTextContent(element) { + + pushSelection(); + + // Generate a range which selects all nodes within the given element + var range = document.createRange(); + range.selectNodeContents(element); + + // Replace any current selection with the generated range + var selection = $window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + // Retrieve the visible text content of the element + var text = selection.toString(); + + popSelection(); + return text; + + }; + /** * Replaces the current text content of the given element with the given * text. To avoid affecting the position of the cursor within an editable @@ -276,10 +275,10 @@ angular.module('clipboard').factory('clipboardService', ['$injector', service.setTextContent = function setTextContent(element, text) { // Strip out any non-text content while preserving cursor position - convertToText(element); + var textContent = service.getTextContent(element); // Reset text content only if doing so will actually change the content - if (element.textContent !== text) + if (textContent !== text) element.textContent = text; }; From 3f51a6dd98f42183209627d643cca72834301d1f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 14:18:37 -0700 Subject: [PATCH 23/31] GUACAMOLE-55: Directly render content to text, rather than trusting Selection.toString(). --- .../clipboard/services/clipboardService.js | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index 8d07ecad4..06667562e 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -240,22 +240,54 @@ angular.module('clipboard').factory('clipboardService', ['$injector', */ service.getTextContent = function getTextContent(element) { - pushSelection(); + var blocks = []; + var currentBlock = ''; - // Generate a range which selects all nodes within the given element - var range = document.createRange(); - range.selectNodeContents(element); + // For each child of the given element + var current = element.firstChild; + while (current) { - // Replace any current selection with the generated range - var selection = $window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); + // Simply append the content of any text nodes + if (current.nodeType === Node.TEXT_NODE) + currentBlock += current.nodeValue; - // Retrieve the visible text content of the element - var text = selection.toString(); + // Render
as a newline character + else if (current.nodeName === 'BR') + currentBlock += '\n'; - popSelection(); - return text; + // For all other nodes, handling depends on whether they are + // block-level elements + else { + + // If we are entering a new block context, start a new block if + // the current block is non-empty + if (currentBlock.length && $window.getComputedStyle(current).display === 'block') { + + // Trim trailing newline (would otherwise inflate the line count by 1) + if (currentBlock.substring(currentBlock.length - 1) === '\n') + currentBlock = currentBlock.substring(0, currentBlock.length - 1); + + // Finish current block and start a new block + blocks.push(currentBlock); + currentBlock = ''; + + } + + // Append the content of the current element to the current block + currentBlock += service.getTextContent(current); + + } + + current = current.nextSibling; + + } + + // Add any in-progress block + if (currentBlock.length) + blocks.push(currentBlock); + + // Combine all non-empty blocks, separated by newlines + return blocks.join('\n'); }; From c68cedd6522de42e599a87b8969138672722ac10 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 15:27:51 -0700 Subject: [PATCH 24/31] GUACAMOLE-55: Correct outdated documentation within setTextContent(). --- .../main/webapp/app/clipboard/services/clipboardService.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index 06667562e..9c43ab455 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -306,11 +306,8 @@ angular.module('clipboard').factory('clipboardService', ['$injector', */ service.setTextContent = function setTextContent(element, text) { - // Strip out any non-text content while preserving cursor position - var textContent = service.getTextContent(element); - // Reset text content only if doing so will actually change the content - if (textContent !== text) + if (service.getTextContent(element) !== text) element.textContent = text; }; From 6ee4264a218b8194d746672b7d20d1141e17df48 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 16:11:20 -0700 Subject: [PATCH 25/31] GUACAMOLE-55: Strip any images which may be present prior to assigning text content. --- .../src/main/webapp/app/clipboard/services/clipboardService.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index 9c43ab455..bfbec647f 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -306,6 +306,9 @@ angular.module('clipboard').factory('clipboardService', ['$injector', */ service.setTextContent = function setTextContent(element, text) { + // Strip out any images + $(element).find('img').remove(); + // Reset text content only if doing so will actually change the content if (service.getTextContent(element) !== text) element.textContent = text; From 3f372cce68e480ce0cc0e28269b9db0b3e8f6f33 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 16:31:18 -0700 Subject: [PATCH 26/31] GUACAMOLE-55: Render tags as their alt text. --- .../main/webapp/app/clipboard/services/clipboardService.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js index bfbec647f..f0529d8b8 100644 --- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js +++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js @@ -255,6 +255,10 @@ angular.module('clipboard').factory('clipboardService', ['$injector', else if (current.nodeName === 'BR') currentBlock += '\n'; + // Render as alt text, if available + else if (current.nodeName === 'IMG') + currentBlock += current.getAttribute('alt') || ''; + // For all other nodes, handling depends on whether they are // block-level elements else { From b9a57897c54241f3700cbe333362b9f0b34eba93 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 17:24:51 -0700 Subject: [PATCH 27/31] GUACAMOLE-55: Set background to clipboard-service-target to match clipboard editor. Browser may otherwise explicitly add the background of the body to pasted text with inline styles. --- guacamole/src/main/webapp/app/clipboard/styles/clipboard.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css index 838b8b47a..8f4df25b1 100644 --- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css @@ -17,6 +17,10 @@ * under the License. */ +.clipboard, .clipboard-service-target { + background: white; +} + .clipboard { position: relative; border: 1px solid #AAA; @@ -24,7 +28,6 @@ -webkit-border-radius: 0.25em; -khtml-border-radius: 0.25em; border-radius: 0.25em; - background: white; width: 100%; height: 2in; white-space: pre; From 0679c10187e60ca16bd70c1cd37640e834972a0d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 18:40:35 -0700 Subject: [PATCH 28/31] GUACAMOLE-55: Move local clipboad sync logic back into clientController. Keep the guacClipboard directive lean and with a single purpose. --- .../client/controllers/clientController.js | 47 ++++++- .../app/clipboard/directives/guacClipboard.js | 115 ++---------------- 2 files changed, 54 insertions(+), 108 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 6b1c943ae..aec8252ee 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -32,6 +32,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'); @@ -244,6 +245,15 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams */ var keysCurrentlyPressed = {}; + /** + * Map of all currently pressed keys (by keysym) to the clipboard contents + * received from the remote desktop while those keys were pressed. All keys + * not currently pressed will not have entries within this map. + * + * @type Object. + */ + var clipboardDataFromKey = {}; + /* * Check to see if all currently pressed keys are in the set of menu keys. */ @@ -374,11 +384,24 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { + // Send clipboard data if menu is hidden + if (!menuShown && menuShownPreviousState) + $scope.$broadcast('guacClipboard', $scope.client.clipboardData); + // Disable client keyboard if the menu is shown $scope.client.clientProperties.keyboardEnabled = !menuShown; }); + // Watch clipboard for new data, associating it with any pressed keys + $scope.$watch('client.clipboardData', function clipboardChanged(data) { + + // Associate new clipboard data with any currently-pressed key + for (var keysym in keysCurrentlyPressed) + clipboardDataFromKey[keysym] = data; + + }); + // Track pressed keys, opening the Guacamole menu after Ctrl+Alt+Shift $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { @@ -414,9 +437,18 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams }); - // Update pressed keys as they are released + // Update pressed keys as they are released, synchronizing the clipboard + // with any data that appears to have come from those key presses $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { + // Sync local clipboard with any clipboard data received while this + // key was pressed (if any) + var clipboardData = clipboardDataFromKey[keysym]; + if (clipboardData) { + clipboardService.setLocalClipboard(clipboardData); + delete clipboardDataFromKey[keysym]; + } + // Mark key as released delete keysCurrentlyPressed[keysym]; @@ -529,6 +561,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', 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/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js index a04c06f32..97fc6e026 100644 --- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js +++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js @@ -18,14 +18,10 @@ */ /** - * A directive which exposes the current clipboard contents, if possible, - * allowing the user to edit those contents. If the current clipboard contents - * cannot be directly accessed, the user can at least directly copy/paste data - * within the field provided by this directive. The contents of this clipboard - * directive, whether retrieved from the local or manipulated manually by the - * user, are exposed via the "data" attribute. In addition to updating the - * "data" attribute, changes to clipboard data will be broadcast on the scope - * via "guacClipboard" events. + * A directive provides an editor whose contents are exposed via a + * ClipboardData object via the "data" attribute. If this data should also be + * synced to the local clipboard, or sent via a connected Guacamole client + * using a "guacClipboard" event, it is up to external code to do so. */ angular.module('clipboard').directive('guacClipboard', ['$injector', function guacClipboard($injector) { @@ -48,12 +44,9 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', config.scope = { /** - * The data to display within the field provided by this directive. If - * the local clipboard can be accessed by JavaScript, this will be set - * automatically as the local clipboard changes. Failing that, this - * will be set when the user manually modifies the contents of the - * field. Changes to this value will be rendered within the field and, - * if possible, will be pushed to the local clipboard. + * The data to display within the field provided by this directive. This + * data will modified or replaced when the user manually alters the + * contents of the field. * * @type ClipboardData */ @@ -66,29 +59,9 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', function guacClipboardController($scope, $injector, $element) { // Required services - var $rootScope = $injector.get('$rootScope'); var $window = $injector.get('$window'); var clipboardService = $injector.get('clipboardService'); - /** - * Map of all currently pressed keys by keysym. If a particular key is - * currently pressed, the value stored under that key's keysym within - * this map will be true. All keys not currently pressed will not have entries - * within this map. - * - * @type Object. - */ - var keysCurrentlyPressed = {}; - - /** - * Map of all currently pressed keys (by keysym) to the clipboard - * contents received while those keys were pressed. All keys not - * currently pressed will not have entries within this map. - * - * @type Object. - */ - var clipboardDataFromKey = {}; - /** * The FileReader to use to read File or Blob data received from the * clipboard. @@ -236,10 +209,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', // Watch clipboard for new data, associating it with any pressed keys $scope.$watch('data', function clipboardDataChanged(data) { - // Associate new clipboard data with any currently-pressed key - for (var keysym in keysCurrentlyPressed) - clipboardDataFromKey[keysym] = data; - // Stop any current read process if (reader.readyState === 1) reader.abort(); @@ -263,75 +232,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', } - // Notify of change - $rootScope.$broadcast('guacClipboard', data); - - // Attempt to set local clipboard with received data - clipboardService.setLocalClipboard(data); - - }); - - // Track pressed keys - $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - - // Record key as pressed - keysCurrentlyPressed[keysym] = true; - - }); - - // Update pressed keys as they are released, synchronizing the clipboard - // with any data that appears to have come from those key presses - $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - - // Sync local clipboard with any clipboard data received while this - // key was pressed (if any) - var clipboardData = clipboardDataFromKey[keysym]; - if (clipboardData) { - clipboardService.setLocalClipboard(clipboardData); - delete clipboardDataFromKey[keysym]; - } - - // Mark key as released - delete keysCurrentlyPressed[keysym]; - - }); - - /** - * Checks whether the clipboard data has changed, updating the stored - * clipboard data if it has. If this function is being called due to a - * DOM event, that event should be passed to this function such that the - * context of the call can be taken into account. Focus events, in - * particular, need to be considered only in the context of the window. - * - * @param {Event} [e] - * The event currently being handled, if any. - */ - var checkClipboard = function checkClipboard(e) { - - // Ignore focus events for anything except the window - if (e && e.type === 'focus' && e.target !== $window) - return; - - clipboardService.getLocalClipboard().then(function clipboardRead(data) { - $scope.data = data; - }); - - }; - - // Attempt to read the clipboard if it may have changed - $window.addEventListener('copy', checkClipboard); - $window.addEventListener('cut', checkClipboard); - $window.addEventListener('focus', checkClipboard); - - // Clean up on destruction - $scope.$on('$destroy', function destroyClipboard() { - $window.removeEventListener('copy', checkClipboard); - $window.removeEventListener('cut', checkClipboard); - $window.removeEventListener('focus', checkClipboard); - }); - - // Perform initial clipboard check - checkClipboard(); + }); // end $scope.data watch }]; From b55c4c02115f859ef67387042bc2173fbd3f2d45 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 19:36:39 -0700 Subject: [PATCH 29/31] GUACAMOLE-55: Do not sync local clipboard if menu is open. --- .../main/webapp/app/client/controllers/clientController.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index aec8252ee..7477abc86 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -442,14 +442,13 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { // Sync local clipboard with any clipboard data received while this - // key was pressed (if any) + // key was pressed (if any) as long as the menu is not open var clipboardData = clipboardDataFromKey[keysym]; - if (clipboardData) { + if (clipboardData && !$scope.menu.shown) clipboardService.setLocalClipboard(clipboardData); - delete clipboardDataFromKey[keysym]; - } // Mark key as released + delete clipboardDataFromKey[keysym]; delete keysCurrentlyPressed[keysym]; }); From a4019b7f82db6b4a1febeb225289e661df745250 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 19:57:01 -0700 Subject: [PATCH 30/31] GUACAMOLE-55: Automatically fire guacClipboard events from index controller when local clipboard has changed. --- .../app/index/controllers/indexController.js | 23 +++++++++++++++++++ .../src/main/webapp/app/index/indexModule.js | 1 + 2 files changed, 24 insertions(+) diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index 4d951fc46..25e6b6190 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -26,6 +26,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Required services var $document = $injector.get('$document'); var $window = $injector.get('$window'); + var clipboardService = $injector.get('clipboardService'); var guacNotification = $injector.get('guacNotification'); /** @@ -124,6 +125,28 @@ angular.module('index').controller('indexController', ['$scope', '$injector', keyboard.reset(); }; + /** + * Checks whether the clipboard data has changed, firing a new + * "guacClipboard" event if it has. + */ + var checkClipboard = function checkClipboard() { + clipboardService.getLocalClipboard().then(function clipboardRead(data) { + $scope.$broadcast('guacClipboard', data); + }); + }; + + // 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) { + + // Only recheck clipboard if it's the window itself that gained focus + if (e.target === $window) + checkClipboard(); + + }, true); + // Display login screen if a whole new set of credentials is needed $scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) { $scope.page.title = 'APP.NAME'; diff --git a/guacamole/src/main/webapp/app/index/indexModule.js b/guacamole/src/main/webapp/app/index/indexModule.js index e1bbd5173..22ab84f96 100644 --- a/guacamole/src/main/webapp/app/index/indexModule.js +++ b/guacamole/src/main/webapp/app/index/indexModule.js @@ -23,6 +23,7 @@ angular.module('index', [ 'auth', 'client', + 'clipboard', 'home', 'login', 'manage', From 92ff80a503fece01545f48c40d5776d9c9faf8e9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 29 Jun 2016 20:03:59 -0700 Subject: [PATCH 31/31] GUACAMOLE-55: Sync to local clipboard when client clipboardData changes. --- .../main/webapp/app/client/controllers/clientController.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 7477abc86..74aea6220 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -396,6 +396,10 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams // Watch clipboard for new data, associating it with any pressed keys $scope.$watch('client.clipboardData', function clipboardChanged(data) { + // Sync local clipboard as long as the menu is not open + if (!$scope.menu.shown) + clipboardService.setLocalClipboard(data); + // Associate new clipboard data with any currently-pressed key for (var keysym in keysCurrentlyPressed) clipboardDataFromKey[keysym] = data;