mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-09 06:31:22 +00:00
GUACAMOLE-55: Merge image copy/paste changes.
This commit is contained in:
@@ -170,7 +170,8 @@ Guacamole.StringWriter = function(stream) {
|
|||||||
* @param {String} text The text to send.
|
* @param {String} text The text to send.
|
||||||
*/
|
*/
|
||||||
this.sendText = function(text) {
|
this.sendText = function(text) {
|
||||||
array_writer.sendData(__encode_utf8(text));
|
if (text.length)
|
||||||
|
array_writer.sendData(__encode_utf8(text));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
angular.module('client', [
|
angular.module('client', [
|
||||||
'auth',
|
'auth',
|
||||||
|
'clipboard',
|
||||||
'element',
|
'element',
|
||||||
'history',
|
'history',
|
||||||
'navigation',
|
'navigation',
|
||||||
|
@@ -250,7 +250,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
* received from the remote desktop while those keys were pressed. All keys
|
* received from the remote desktop while those keys were pressed. All keys
|
||||||
* not currently pressed will not have entries within this map.
|
* not currently pressed will not have entries within this map.
|
||||||
*
|
*
|
||||||
* @type Object.<Number, String>
|
* @type Object.<Number, ClipboardData>
|
||||||
*/
|
*/
|
||||||
var clipboardDataFromKey = {};
|
var clipboardDataFromKey = {};
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
|
|
||||||
// Send clipboard data if menu is hidden
|
// Send clipboard data if menu is hidden
|
||||||
if (!menuShown && menuShownPreviousState)
|
if (!menuShown && menuShownPreviousState)
|
||||||
$scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData);
|
$scope.$broadcast('guacClipboard', $scope.client.clipboardData);
|
||||||
|
|
||||||
// Disable client keyboard if the menu is shown
|
// Disable client keyboard if the menu is shown
|
||||||
$scope.client.clientProperties.keyboardEnabled = !menuShown;
|
$scope.client.clientProperties.keyboardEnabled = !menuShown;
|
||||||
@@ -396,6 +396,10 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
// Watch clipboard for new data, associating it with any pressed keys
|
// Watch clipboard for new data, associating it with any pressed keys
|
||||||
$scope.$watch('client.clipboardData', function clipboardChanged(data) {
|
$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
|
// Associate new clipboard data with any currently-pressed key
|
||||||
for (var keysym in keysCurrentlyPressed)
|
for (var keysym in keysCurrentlyPressed)
|
||||||
clipboardDataFromKey[keysym] = data;
|
clipboardDataFromKey[keysym] = data;
|
||||||
@@ -442,14 +446,13 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
|
||||||
|
|
||||||
// Sync local clipboard with any clipboard data received while this
|
// 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];
|
var clipboardData = clipboardDataFromKey[keysym];
|
||||||
if (clipboardData) {
|
if (clipboardData && !$scope.menu.shown)
|
||||||
clipboardService.setLocalClipboard(clipboardData);
|
clipboardService.setLocalClipboard(clipboardData);
|
||||||
delete clipboardDataFromKey[keysym];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark key as released
|
// Mark key as released
|
||||||
|
delete clipboardDataFromKey[keysym];
|
||||||
delete keysCurrentlyPressed[keysym];
|
delete keysCurrentlyPressed[keysym];
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -566,7 +569,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
|
|||||||
|
|
||||||
// Sync with local clipboard
|
// Sync with local clipboard
|
||||||
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
|
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
|
||||||
$scope.$broadcast('guacClipboard', 'text/plain', data);
|
$scope.$broadcast('guacClipboard', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hide status notification
|
// Hide status notification
|
||||||
|
@@ -410,9 +410,9 @@ angular.module('client').directive('guacClient', [function guacClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Update remote clipboard if local clipboard changes
|
// Update remote clipboard if local clipboard changes
|
||||||
$scope.$on('guacClipboard', function onClipboard(event, mimetype, data) {
|
$scope.$on('guacClipboard', function onClipboard(event, data) {
|
||||||
if (client) {
|
if (client) {
|
||||||
client.setClipboard(data);
|
ManagedClient.setClipboard($scope.client, data);
|
||||||
$scope.client.clipboardData = data;
|
$scope.client.clipboardData = data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,162 +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 $rootScope = $injector.get('$rootScope');
|
|
||||||
|
|
||||||
var service = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A div which is used to hide the clipboard textarea and remove it from
|
|
||||||
* document flow.
|
|
||||||
*
|
|
||||||
* @type Element
|
|
||||||
*/
|
|
||||||
var clipElement = document.createElement('div');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The textarea that will be used to hold the local clipboard contents.
|
|
||||||
*
|
|
||||||
* @type Element
|
|
||||||
*/
|
|
||||||
var clipboardContent = document.createElement('textarea');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The contents of the last clipboard event broadcast by this service when
|
|
||||||
* the clipboard contents changed.
|
|
||||||
*
|
|
||||||
* @type String
|
|
||||||
*/
|
|
||||||
var lastClipboardEvent = '';
|
|
||||||
|
|
||||||
// Ensure textarea is selectable but not visible
|
|
||||||
clipElement.appendChild(clipboardContent);
|
|
||||||
clipElement.style.position = 'absolute';
|
|
||||||
clipElement.style.width = '1px';
|
|
||||||
clipElement.style.height = '1px';
|
|
||||||
clipElement.style.left = '-1px';
|
|
||||||
clipElement.style.top = '-1px';
|
|
||||||
clipElement.style.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();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
clipboardContent.blur();
|
|
||||||
|
|
||||||
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() {
|
|
||||||
|
|
||||||
// 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
|
|
||||||
clipboardContent.blur();
|
|
||||||
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}]);
|
|
@@ -65,18 +65,6 @@
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#guac-menu #mouse-settings .choice {
|
#guac-menu #mouse-settings .choice {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@@ -54,7 +54,7 @@
|
|||||||
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
|
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
|
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
|
||||||
<textarea ng-model="client.clipboardData" rows="10" cols="40" id="clipboard"></textarea>
|
<guac-clipboard data="client.clipboardData"></guac-clipboard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -26,6 +26,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
|||||||
// Required types
|
// Required types
|
||||||
var ClientProperties = $injector.get('ClientProperties');
|
var ClientProperties = $injector.get('ClientProperties');
|
||||||
var ClientIdentifier = $injector.get('ClientIdentifier');
|
var ClientIdentifier = $injector.get('ClientIdentifier');
|
||||||
|
var ClipboardData = $injector.get('ClipboardData');
|
||||||
var ManagedClientState = $injector.get('ManagedClientState');
|
var ManagedClientState = $injector.get('ManagedClientState');
|
||||||
var ManagedDisplay = $injector.get('ManagedDisplay');
|
var ManagedDisplay = $injector.get('ManagedDisplay');
|
||||||
var ManagedFilesystem = $injector.get('ManagedFilesystem');
|
var ManagedFilesystem = $injector.get('ManagedFilesystem');
|
||||||
@@ -37,7 +38,6 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
|||||||
var $rootScope = $injector.get('$rootScope');
|
var $rootScope = $injector.get('$rootScope');
|
||||||
var $window = $injector.get('$window');
|
var $window = $injector.get('$window');
|
||||||
var authenticationService = $injector.get('authenticationService');
|
var authenticationService = $injector.get('authenticationService');
|
||||||
var clipboardService = $injector.get('clipboardService');
|
|
||||||
var connectionGroupService = $injector.get('connectionGroupService');
|
var connectionGroupService = $injector.get('connectionGroupService');
|
||||||
var connectionService = $injector.get('connectionService');
|
var connectionService = $injector.get('connectionService');
|
||||||
var tunnelService = $injector.get('tunnelService');
|
var tunnelService = $injector.get('tunnelService');
|
||||||
@@ -100,9 +100,12 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
|||||||
/**
|
/**
|
||||||
* The current clipboard contents.
|
* 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
|
* All uploaded files. As files are uploaded, their progress can be
|
||||||
@@ -383,6 +386,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
|||||||
ManagedClientState.setConnectionState(managedClient.clientState,
|
ManagedClientState.setConnectionState(managedClient.clientState,
|
||||||
ManagedClientState.ConnectionState.CONNECTED);
|
ManagedClientState.ConnectionState.CONNECTED);
|
||||||
|
|
||||||
|
// Send any clipboard data already provided
|
||||||
|
if (managedClient.clipboardData)
|
||||||
|
ManagedClient.setClipboard(managedClient, managedClient.clipboardData);
|
||||||
|
|
||||||
// Begin streaming audio input if possible
|
// Begin streaming audio input if possible
|
||||||
requestAudioStream(client);
|
requestAudioStream(client);
|
||||||
|
|
||||||
@@ -417,28 +424,43 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
|
|||||||
// Handle any received clipboard data
|
// Handle any received clipboard data
|
||||||
client.onclipboard = function clientClipboardReceived(stream, mimetype) {
|
client.onclipboard = function clientClipboardReceived(stream, mimetype) {
|
||||||
|
|
||||||
// Only text/plain is supported for now
|
var reader;
|
||||||
if (mimetype !== "text/plain") {
|
|
||||||
stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED);
|
// If the received data is text, read it as a simple string
|
||||||
return;
|
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);
|
// Otherwise read the clipboard data as a Blob
|
||||||
var data = "";
|
else {
|
||||||
|
reader = new Guacamole.BlobReader(stream, mimetype);
|
||||||
// Append any received data to buffer
|
reader.onend = function blobComplete() {
|
||||||
reader.ontext = function clipboard_text_received(text) {
|
$rootScope.$apply(function updateClipboard() {
|
||||||
data += text;
|
managedClient.clipboardData = new ClipboardData({
|
||||||
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
|
type : mimetype,
|
||||||
};
|
data : reader.getBlob()
|
||||||
|
});
|
||||||
// Update state when done
|
});
|
||||||
reader.onend = function clipboard_text_end() {
|
};
|
||||||
$rootScope.$apply(function updateClipboard() {
|
}
|
||||||
managedClient.clipboardData = data;
|
|
||||||
clipboardService.setLocalClipboard(data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -523,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;
|
return ManagedClient;
|
||||||
|
|
||||||
}]);
|
}]);
|
23
guacamole/src/main/webapp/app/clipboard/clipboardModule.js
Normal file
23
guacamole/src/main/webapp/app/clipboard/clipboardModule.js
Normal file
@@ -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', []);
|
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* 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 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) {
|
||||||
|
|
||||||
|
// Required types
|
||||||
|
var ClipboardData = $injector.get('ClipboardData');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object for the guacClipboard directive.
|
||||||
|
*
|
||||||
|
* @type Object.<String, Object>
|
||||||
|
*/
|
||||||
|
var config = {
|
||||||
|
restrict : 'E',
|
||||||
|
replace : true,
|
||||||
|
templateUrl : 'app/clipboard/templates/guacClipboard.html'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scope properties exposed by the guacClipboard directive
|
||||||
|
config.scope = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
data : '='
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// guacClipboard directive controller
|
||||||
|
config.controller = ['$scope', '$injector', '$element',
|
||||||
|
function guacClipboardController($scope, $injector, $element) {
|
||||||
|
|
||||||
|
// Required services
|
||||||
|
var $window = $injector.get('$window');
|
||||||
|
var clipboardService = $injector.get('clipboardService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FileReader to use to read File or Blob data received from the
|
||||||
|
* clipboard.
|
||||||
|
*
|
||||||
|
* @type FileReader
|
||||||
|
*/
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content-editable DOM element which will contain the clipboard
|
||||||
|
* contents within the user interface provided by this directive.
|
||||||
|
*
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
|
||||||
|
// 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 < files.length; i++) {
|
||||||
|
|
||||||
|
var file = files[i];
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
$scope.data = new ClipboardData({
|
||||||
|
type : file.type,
|
||||||
|
data : file
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not paste
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end for each item
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
var updateClipboardData = function updateClipboardData() {
|
||||||
|
|
||||||
|
// If the clipboard contains a single image, parse and assign the
|
||||||
|
// image data to the internal clipboard
|
||||||
|
var currentImage = clipboardService.getImageContent(element);
|
||||||
|
if (currentImage) {
|
||||||
|
|
||||||
|
// Convert the image's data URL into a blob
|
||||||
|
var blob = clipboardService.parseDataURL(currentImage);
|
||||||
|
if (blob) {
|
||||||
|
|
||||||
|
// 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 : clipboardService.getTextContent(element)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 clipboardDataChanged(data) {
|
||||||
|
|
||||||
|
// Stop any current read process
|
||||||
|
if (reader.readyState === 1)
|
||||||
|
reader.abort();
|
||||||
|
|
||||||
|
// If the clipboard data is a string, render it as text
|
||||||
|
if (typeof data.data === 'string')
|
||||||
|
clipboardService.setTextContent(element, data.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() {
|
||||||
|
clipboardService.setImageContent(element, reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore other data types
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}); // end $scope.data watch
|
||||||
|
|
||||||
|
}];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
|
||||||
|
}]);
|
@@ -0,0 +1,456 @@
|
|||||||
|
/*
|
||||||
|
* 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('clipboard').factory('clipboardService', ['$injector',
|
||||||
|
function clipboardService($injector) {
|
||||||
|
|
||||||
|
// Get required services
|
||||||
|
var $q = $injector.get('$q');
|
||||||
|
var $window = $injector.get('$window');
|
||||||
|
|
||||||
|
// Required types
|
||||||
|
var ClipboardData = $injector.get('ClipboardData');
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type HTMLDocument
|
||||||
|
*/
|
||||||
|
var document = $window.document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The textarea that will be used to hold the local clipboard contents.
|
||||||
|
*
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
|
var clipboardContent = document.createElement('div');
|
||||||
|
|
||||||
|
// Ensure clipboard target is selectable but not visible
|
||||||
|
clipboardContent.setAttribute('contenteditable', 'true');
|
||||||
|
clipboardContent.className = 'clipboard-service-target';
|
||||||
|
|
||||||
|
// 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
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local clipboard, if possible, to the given text.
|
||||||
|
*
|
||||||
|
* @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(data) {
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
// Track the originally-focused element prior to changing focus
|
||||||
|
var originalElement = document.activeElement;
|
||||||
|
pushSelection();
|
||||||
|
|
||||||
|
// Copy the given value into the clipboard DOM element
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select all data within the clipboard target
|
||||||
|
selectAll(clipboardContent);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
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 });
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
|
||||||
|
var blocks = [];
|
||||||
|
var currentBlock = '';
|
||||||
|
|
||||||
|
// For each child of the given element
|
||||||
|
var current = element.firstChild;
|
||||||
|
while (current) {
|
||||||
|
|
||||||
|
// Simply append the content of any text nodes
|
||||||
|
if (current.nodeType === Node.TEXT_NODE)
|
||||||
|
currentBlock += current.nodeValue;
|
||||||
|
|
||||||
|
// Render <br> as a newline character
|
||||||
|
else if (current.nodeName === 'BR')
|
||||||
|
currentBlock += '\n';
|
||||||
|
|
||||||
|
// Render <img> 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 {
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* <code>textContent</code> 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 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;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @return {Promise.<ClipboardData>}
|
||||||
|
* 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;
|
||||||
|
pushSelection();
|
||||||
|
|
||||||
|
// Clear and select the clipboard DOM element
|
||||||
|
clipboardContent.innerHTML = '';
|
||||||
|
clipboardContent.focus();
|
||||||
|
selectAll(clipboardContent);
|
||||||
|
|
||||||
|
// Attempt paste local clipboard into clipboard DOM element
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
|
||||||
|
// restoring whichever element was originally focused
|
||||||
|
clipboardContent.blur();
|
||||||
|
originalElement.focus();
|
||||||
|
popSelection();
|
||||||
|
|
||||||
|
}, CLIPBOARD_READ_DELAY);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
|
||||||
|
}]);
|
60
guacamole/src/main/webapp/app/clipboard/styles/clipboard.css
Normal file
60
guacamole/src/main/webapp/app/clipboard/styles/clipboard.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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, .clipboard-service-target {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard {
|
||||||
|
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;
|
||||||
|
width: 100%;
|
||||||
|
height: 2in;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard p,
|
||||||
|
.clipboard div {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid black;
|
||||||
|
background: url('images/checker.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipboard-service-target {
|
||||||
|
position: fixed;
|
||||||
|
left: -1px;
|
||||||
|
right: -1px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
<div class="clipboard" contenteditable="true"></div>
|
@@ -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;
|
||||||
|
|
||||||
|
}]);
|
@@ -26,6 +26,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
|||||||
// Required services
|
// Required services
|
||||||
var $document = $injector.get('$document');
|
var $document = $injector.get('$document');
|
||||||
var $window = $injector.get('$window');
|
var $window = $injector.get('$window');
|
||||||
|
var clipboardService = $injector.get('clipboardService');
|
||||||
var guacNotification = $injector.get('guacNotification');
|
var guacNotification = $injector.get('guacNotification');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,6 +125,28 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
|
|||||||
keyboard.reset();
|
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
|
// Display login screen if a whole new set of credentials is needed
|
||||||
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) {
|
$scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) {
|
||||||
$scope.page.title = 'APP.NAME';
|
$scope.page.title = 'APP.NAME';
|
||||||
|
@@ -23,6 +23,7 @@
|
|||||||
angular.module('index', [
|
angular.module('index', [
|
||||||
'auth',
|
'auth',
|
||||||
'client',
|
'client',
|
||||||
|
'clipboard',
|
||||||
'home',
|
'home',
|
||||||
'login',
|
'login',
|
||||||
'manage',
|
'manage',
|
||||||
|
BIN
guacamole/src/main/webapp/images/checker.png
Normal file
BIN
guacamole/src/main/webapp/images/checker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 208 B |
Reference in New Issue
Block a user