mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 05:31:22 +00:00
GUACAMOLE-55: Switch to a content-editable div for the clipboard (rather than a textarea).
This commit is contained in:
@@ -70,6 +70,14 @@ angular.module('clipboard').directive('guacClipboard', ['$injector',
|
|||||||
var $window = $injector.get('$window');
|
var $window = $injector.get('$window');
|
||||||
var clipboardService = $injector.get('clipboardService');
|
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
|
* Map of all currently pressed keys by keysym. If a particular key is
|
||||||
* currently pressed, the value stored under that key's keysym within
|
* 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();
|
var reader = new FileReader();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties which contain the current clipboard contents. Each
|
* The content-editable DOM element which will contain the clipboard
|
||||||
* property is mutually exclusive, and will only contain data if the
|
* contents within the user interface provided by this directive.
|
||||||
* clipboard contents are of a particular type.
|
*
|
||||||
|
* @type Element
|
||||||
*/
|
*/
|
||||||
$scope.content = {
|
var element = $element[0];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text contents of the clipboard. If the clipboard contents
|
* Modifies the contents of the given element such that it contains
|
||||||
* is not text, this will be null.
|
* only plain text. All non-text child elements will be stripped and
|
||||||
*
|
* replaced with their text equivalents. As this function performs the
|
||||||
* @type String
|
* conversion through incremental changes only, cursor position within
|
||||||
*/
|
* the given element is preserved.
|
||||||
text : null,
|
*
|
||||||
|
* @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
|
||||||
* The URL of the image is currently stored within the clipboard. If
|
var current = element.firstChild;
|
||||||
* the clipboard currently contains text, this will be null.
|
while (current) {
|
||||||
*
|
|
||||||
* @type String
|
// Preserve the next child in the list, in case the current
|
||||||
*/
|
// node is replaced
|
||||||
imageURL : null
|
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 <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.
|
||||||
|
*/
|
||||||
|
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
|
// 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
|
// For each item within the clipboard
|
||||||
var items = e.clipboardData.items;
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
|
||||||
// If the item is an image, attempt to read that image
|
// 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
|
* Rereads the contents of the clipboard field, updating the
|
||||||
* URL of which is exposed via the imageURL property.
|
* ClipboardData object on the scope as necessary. The type of data
|
||||||
*
|
* stored within the ClipboardData object will be heuristically
|
||||||
* @returns {Boolean}
|
* determined from the HTML contents of the clipboard field.
|
||||||
* true if the current clipboard contains only an image, false
|
|
||||||
* otherwise.
|
|
||||||
*/
|
*/
|
||||||
$scope.isImage = function isImage() {
|
var updateClipboardData = function updateClipboardData() {
|
||||||
return !!$scope.content.imageURL;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// If the clipboard contains a single image, parse and assign the
|
||||||
* Returns whether the clipboard currently contains only text.
|
// image data to the internal clipboard
|
||||||
*
|
var currentImage = getImageContent(element);
|
||||||
* @returns {Boolean}
|
if (currentImage) {
|
||||||
* true if the clipboard currently contains only text, false
|
|
||||||
* otherwise.
|
|
||||||
*/
|
|
||||||
$scope.isText = function isText() {
|
|
||||||
return !$scope.isImage();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// Convert the image's data URL into a blob
|
||||||
* Clears the current clipboard contents. If the clipboard currently
|
var blob = parseDataURL(currentImage);
|
||||||
* displays an image, this will also return to a text-based clipboard
|
if (blob) {
|
||||||
* display.
|
|
||||||
*/
|
|
||||||
$scope.resetClipboard = function resetClipboard() {
|
|
||||||
|
|
||||||
// Reset to blank
|
// Complete the assignment if conversion was successful
|
||||||
$scope.data = new ClipboardData({
|
$scope.$evalAsync(function assignClipboardData() {
|
||||||
type : 'text/plain',
|
$scope.data = new ClipboardData({
|
||||||
data : ''
|
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
|
// Update the internally-stored clipboard data when events are fired
|
||||||
$scope.$watch('content.text', function textChanged(text) {
|
// that indicate the clipboard field may have been changed
|
||||||
|
element.addEventListener('input', updateClipboardData);
|
||||||
if (text) {
|
element.addEventListener('DOMCharacterDataModified', updateClipboardData);
|
||||||
$scope.data = new ClipboardData({
|
element.addEventListener('DOMNodeInserted', updateClipboardData);
|
||||||
type : $scope.data.type,
|
element.addEventListener('DOMNodeRemoved', updateClipboardData);
|
||||||
data : text
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch clipboard for new data, associating it with any pressed keys
|
// 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
|
// 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;
|
||||||
|
|
||||||
// Stop any current read process
|
// 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 the clipboard data is a string, render it as text
|
||||||
if (typeof data.data === 'string') {
|
if (typeof data.data === 'string')
|
||||||
$scope.content.text = data.data;
|
setTextContent(element, data.data);
|
||||||
$scope.content.imageURL = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render Blob/File contents based on mimetype
|
// Render Blob/File contents based on mimetype
|
||||||
else if (data.data instanceof Blob) {
|
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 the copied data was an image, display it as such
|
||||||
if (/^image\//.exec(data.type)) {
|
if (/^image\//.exec(data.type)) {
|
||||||
reader.onload = function updateImageURL() {
|
reader.onload = function updateImageURL() {
|
||||||
$scope.$apply(function imageURLLoaded() {
|
setImageContent(element, reader.result);
|
||||||
$scope.content.text = null;
|
|
||||||
$scope.content.imageURL = reader.result;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(data.data);
|
reader.readAsDataURL(data.data);
|
||||||
}
|
}
|
||||||
|
@@ -17,8 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.clipboard .image-clipboard,
|
.clipboard {
|
||||||
.clipboard .text-clipboard textarea {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid #AAA;
|
border: 1px solid #AAA;
|
||||||
-moz-border-radius: 0.25em;
|
-moz-border-radius: 0.25em;
|
||||||
@@ -26,16 +25,11 @@
|
|||||||
-khtml-border-radius: 0.25em;
|
-khtml-border-radius: 0.25em;
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
background: white;
|
background: white;
|
||||||
}
|
|
||||||
|
|
||||||
.clipboard .text-clipboard textarea {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 2in;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
overflow: auto;
|
||||||
|
padding: 0.25em;
|
||||||
.clipboard .image-clipboard {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clipboard .image-clipboard .reset-button {
|
.clipboard .image-clipboard .reset-button {
|
||||||
@@ -46,9 +40,11 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clipboard .image-clipboard img {
|
.clipboard img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 480px;
|
max-height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
background: url('images/checker.png');
|
background: url('images/checker.png');
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1 @@
|
|||||||
<div class="clipboard">
|
<div class="clipboard" contenteditable="true"></div>
|
||||||
<div ng-if="isText()" class="text-clipboard">
|
|
||||||
<textarea ng-model="content.text" 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="{{content.imageURL}}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
Reference in New Issue
Block a user