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 @@
-