mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-10 07:01:21 +00:00
GUACAMOLE-55: Move clipboard handling to own module. Represent clipboard contents with ClipboardData type.
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
angular.module('client', [
|
||||
'auth',
|
||||
'clipboard',
|
||||
'element',
|
||||
'history',
|
||||
'navigation',
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
|
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* 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. 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() {
|
||||
|
||||
/**
|
||||
* Configuration object for the guacClipboard directive.
|
||||
*
|
||||
* @type Object.<String, 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|Blob
|
||||
*/
|
||||
data : '='
|
||||
|
||||
};
|
||||
|
||||
// guacClipboard directive controller
|
||||
config.controller = ['$scope', '$injector', '$element',
|
||||
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.<Number, Boolean>
|
||||
*/
|
||||
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.<Number, String>
|
||||
*/
|
||||
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) {
|
||||
|
||||
// Associate new clipboard data with any currently-pressed key
|
||||
for (var keysym in keysCurrentlyPressed)
|
||||
clipboardDataFromKey[keysym] = 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);
|
||||
|
||||
});
|
||||
|
||||
// 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, true);
|
||||
$window.addEventListener('cut', checkClipboard, true);
|
||||
$window.addEventListener('focus', 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();
|
||||
|
||||
}];
|
||||
|
||||
return config;
|
||||
|
||||
}]);
|
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* 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 service for accessing local clipboard data.
|
||||
*/
|
||||
angular.module('client').factory('clipboardService', ['$injector',
|
||||
function clipboardService($injector) {
|
||||
|
||||
// Get required services
|
||||
var $q = $injector.get('$q');
|
||||
|
||||
var service = {};
|
||||
|
||||
/**
|
||||
* A div which is used to hide the clipboard textarea and remove it from
|
||||
* document flow.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var clipElement = document.createElement('div');
|
||||
|
||||
/**
|
||||
* The textarea that will be used to hold the local clipboard contents.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var clipboardContent = document.createElement('textarea');
|
||||
|
||||
// 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';
|
||||
|
||||
// Add textarea to DOM
|
||||
document.body.appendChild(clipElement);
|
||||
|
||||
/**
|
||||
* Sets the local clipboard, if possible, to the given text.
|
||||
*
|
||||
* @param {String} text
|
||||
* The text to which the local clipboard should be set.
|
||||
*
|
||||
* @return {Promise}
|
||||
* A promise that will resolve if setting the clipboard was successful,
|
||||
* and will reject if it failed.
|
||||
*/
|
||||
service.setLocalClipboard = function setLocalClipboard(text) {
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
// 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();
|
||||
|
||||
// Attempt to copy data from clipboard element into local clipboard
|
||||
if (document.execCommand('copy'))
|
||||
deferred.resolve();
|
||||
else
|
||||
deferred.reject();
|
||||
|
||||
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
|
||||
// restoring whichever element was originally focused
|
||||
clipboardContent.blur();
|
||||
originalElement.focus();
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current value of the local clipboard.
|
||||
*
|
||||
* @return {Promise}
|
||||
* A promise that will resolve with the contents of the local clipboard
|
||||
* if getting the clipboard was successful, and will reject if it
|
||||
* failed.
|
||||
*/
|
||||
service.getLocalClipboard = function getLocalClipboard() {
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
// 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() {
|
||||
|
||||
// Track the originally-focused element prior to changing focus
|
||||
var originalElement = document.activeElement;
|
||||
|
||||
// Clear and select the clipboard DOM element
|
||||
clipboardContent.value = '';
|
||||
clipboardContent.focus();
|
||||
clipboardContent.select();
|
||||
|
||||
// Attempt paste local clipboard into clipboard DOM element
|
||||
if (document.activeElement === clipboardContent && document.execCommand('paste'))
|
||||
deferred.resolve(clipboardContent.value);
|
||||
else
|
||||
deferred.reject();
|
||||
|
||||
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
|
||||
// restoring whichever element was originally focused
|
||||
clipboardContent.blur();
|
||||
originalElement.focus();
|
||||
|
||||
}, 100);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
}]);
|
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
<div class="clipboard">
|
||||
<div ng-if="isText()" class="text-clipboard">
|
||||
<textarea ng-model="data" rows="10" cols="40"></textarea>
|
||||
</div>
|
||||
<div ng-if="isImage()" class="image-clipboard">
|
||||
<button ng-click="resetClipboard()" class="reset-button">Clear</button>
|
||||
<img ng-src="{{imageURL}}">
|
||||
</div>
|
||||
</div>
|
@@ -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;
|
||||
|
||||
}]);
|
Reference in New Issue
Block a user