mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 00:53:21 +00:00 
			
		
		
		
	GUACAMOLE-55: Update clipboardService to support non-text contents.
This commit is contained in:
		| @@ -113,160 +113,6 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', | ||||
|          */ | ||||
|         var element = $element[0]; | ||||
|  | ||||
|         /** | ||||
|          * Modifies the contents of the given element such that it contains | ||||
|          * only plain text. All non-text child elements will be stripped and | ||||
|          * replaced with their text equivalents. As this function performs the | ||||
|          * conversion through incremental changes only, cursor position within | ||||
|          * the given element is preserved. | ||||
|          * | ||||
|          * @param {Element} element | ||||
|          *     The elements whose contents should be converted to plain text. | ||||
|          */ | ||||
|         var convertToText = function convertToText(element) { | ||||
|  | ||||
|             // For each child of the given element | ||||
|             var current = element.firstChild; | ||||
|             while (current) { | ||||
|  | ||||
|                 // Preserve the next child in the list, in case the current | ||||
|                 // node is replaced | ||||
|                 var next = current.nextSibling; | ||||
|  | ||||
|                 // If the child is not already a text node, replace it with its | ||||
|                 // own text contents | ||||
|                 if (current.nodeType !== Node.TEXT_NODE) { | ||||
|                     var textNode = document.createTextNode(current.textContent); | ||||
|                     current.parentElement.replaceChild(textNode, current); | ||||
|                 } | ||||
|  | ||||
|                 // Advance to next child | ||||
|                 current = next; | ||||
|  | ||||
|             } | ||||
|  | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Parses the given data URL, returning its decoded contents as a new | ||||
|          * Blob. If the URL is not a valid data URL, null will be returned | ||||
|          * instead. | ||||
|          * | ||||
|          * @param {String} url | ||||
|          *     The data URL to parse. | ||||
|          * | ||||
|          * @returns {Blob} | ||||
|          *     A new Blob containing the decoded contents of the data URL, or | ||||
|          *     null if the URL is not a valid data URL. | ||||
|          */ | ||||
|         var parseDataURL = function parseDataURL(url) { | ||||
|  | ||||
|             // Parse given string as a data URL | ||||
|             var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url); | ||||
|             if (!result) | ||||
|                 return null; | ||||
|  | ||||
|             // Pull the mimetype and base64 contents of the data URL | ||||
|             var type = result[1]; | ||||
|             var data = $window.atob(result[2]); | ||||
|  | ||||
|             // Convert the decoded binary string into a typed array | ||||
|             var buffer = new Uint8Array(data.length); | ||||
|             for (var i = 0; i < data.length; i++) | ||||
|                 buffer[i] = data.charCodeAt(i); | ||||
|  | ||||
|             // Produce a proper blob containing the data and type provided in | ||||
|             // the data URL | ||||
|             return new Blob([buffer], { type : type }); | ||||
|  | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Replaces the current text content of the given element with the | ||||
|          * given text. To avoid affecting the position of the cursor within an | ||||
|          * editable element, or firing unnecessary DOM modification events, the | ||||
|          * underlying <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 | ||||
|         element.addEventListener('paste', function dataPasted(e) { | ||||
|  | ||||
| @@ -320,11 +166,11 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', | ||||
|  | ||||
|             // If the clipboard contains a single image, parse and assign the | ||||
|             // image data to the internal clipboard | ||||
|             var currentImage = getImageContent(element); | ||||
|             var currentImage = clipboardService.getImageContent(element); | ||||
|             if (currentImage) { | ||||
|  | ||||
|                 // Convert the image's data URL into a blob | ||||
|                 var blob = parseDataURL(currentImage); | ||||
|                 var blob = clipboardService.parseDataURL(currentImage); | ||||
|                 if (blob) { | ||||
|  | ||||
|                     // Complete the assignment if conversion was successful | ||||
| @@ -372,7 +218,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', | ||||
|  | ||||
|             // If the clipboard data is a string, render it as text | ||||
|             if (typeof data.data === 'string') | ||||
|                 setTextContent(element, data.data); | ||||
|                 clipboardService.setTextContent(element, data.data); | ||||
|  | ||||
|             // Render Blob/File contents based on mimetype | ||||
|             else if (data.data instanceof Blob) { | ||||
| @@ -380,7 +226,7 @@ angular.module('clipboard').directive('guacClipboard', ['$injector', | ||||
|                 // If the copied data was an image, display it as such | ||||
|                 if (/^image\//.exec(data.type)) { | ||||
|                     reader.onload = function updateImageURL() { | ||||
|                         setImageContent(element, reader.result); | ||||
|                         clipboardService.setImageContent(element, reader.result); | ||||
|                     }; | ||||
|                     reader.readAsDataURL(data.data); | ||||
|                 } | ||||
|   | ||||
| @@ -24,36 +24,134 @@ angular.module('clipboard').factory('clipboardService', ['$injector', | ||||
|         function clipboardService($injector) { | ||||
|  | ||||
|     // Get required services | ||||
|     var $q = $injector.get('$q'); | ||||
|     var $q      = $injector.get('$q'); | ||||
|     var $window = $injector.get('$window'); | ||||
|  | ||||
|     // Required types | ||||
|     var ClipboardData = $injector.get('ClipboardData'); | ||||
|  | ||||
|     var service = {}; | ||||
|  | ||||
|     /** | ||||
|      * A div which is used to hide the clipboard textarea and remove it from | ||||
|      * document flow. | ||||
|      * Reference to the window.document object. | ||||
|      * | ||||
|      * @type Element | ||||
|      * @private | ||||
|      * @type HTMLDocument | ||||
|      */ | ||||
|     var clipElement = document.createElement('div'); | ||||
|     var document = $window.document; | ||||
|  | ||||
|     /** | ||||
|      * The textarea that will be used to hold the local clipboard contents. | ||||
|      * | ||||
|      * @type Element | ||||
|      */ | ||||
|     var clipboardContent = document.createElement('textarea'); | ||||
|     var clipboardContent = document.createElement('div'); | ||||
|  | ||||
|     // Ensure textarea is selectable but not visible | ||||
|     clipElement.appendChild(clipboardContent); | ||||
|     clipElement.style.position = 'fixed'; | ||||
|     clipElement.style.width    = '1px'; | ||||
|     clipElement.style.height   = '1px'; | ||||
|     clipElement.style.left     = '-1px'; | ||||
|     clipElement.style.top      = '-1px'; | ||||
|     clipElement.style.overflow = 'hidden'; | ||||
|     // Ensure clipboard target is selectable but not visible | ||||
|     clipboardContent.setAttribute('contenteditable', 'true'); | ||||
|     clipboardContent.className = 'clipboard-service-target'; | ||||
|  | ||||
|     // Add textarea to DOM | ||||
|     document.body.appendChild(clipElement); | ||||
|     // Add clipboard target to DOM | ||||
|     document.body.appendChild(clipboardContent); | ||||
|  | ||||
|     /** | ||||
|      * A stack of past node selection ranges. A range convering the nodes | ||||
|      * currently selected within the document can be pushed onto this stack | ||||
|      * with pushSelection(), and the most recently pushed selection can be | ||||
|      * popped off the stack (and thus re-selected) with popSelection(). | ||||
|      * | ||||
|      * @type Range[] | ||||
|      */ | ||||
|     var selectionStack = []; | ||||
|  | ||||
|     /** | ||||
|      * Pushes the current selection range to the selection stack such that it | ||||
|      * can later be restored with popSelection(). | ||||
|      */ | ||||
|     var pushSelection = function pushSelection() { | ||||
|  | ||||
|         // Add a range representing the current selection to the stack | ||||
|         var selection = $window.getSelection(); | ||||
|         if (selection.getRangeAt && selection.rangeCount) | ||||
|             selectionStack.push(selection.getRangeAt(0)); | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Pops a selection range off the selection stack restoring the document's | ||||
|      * previous selection state. The selection range will be the most recent | ||||
|      * selection range pushed by pushSelection(). If there are no selection | ||||
|      * ranges currently on the stack, this function has no effect. | ||||
|      */ | ||||
|     var popSelection = function popSelection() { | ||||
|  | ||||
|         // Pull one selection range from the stack | ||||
|         var range = selectionStack.pop(); | ||||
|         if (!range) | ||||
|             return; | ||||
|  | ||||
|         // Replace any current selection with the retrieved selection | ||||
|         var selection = $window.getSelection(); | ||||
|         selection.removeAllRanges(); | ||||
|         selection.addRange(range); | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Selects all nodes within the given element. This will replace the | ||||
|      * current selection with a new selection range that covers the element's | ||||
|      * contents. If the original selection should be preserved, use | ||||
|      * pushSelection() and popSelection(). | ||||
|      * | ||||
|      * @param {Element} element | ||||
|      *     The element whose contents should be selected. | ||||
|      */ | ||||
|     var selectAll = function selectAll(element) { | ||||
|  | ||||
|         // Generate a range which selects all nodes within the given element | ||||
|         var range = document.createRange(); | ||||
|         range.selectNodeContents(element); | ||||
|  | ||||
|         // Replace any current selection with the generated range | ||||
|         var selection = $window.getSelection(); | ||||
|         selection.removeAllRanges(); | ||||
|         selection.addRange(range); | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Modifies the contents of the given element such that it contains only | ||||
|      * plain text. All non-text child elements will be stripped and replaced | ||||
|      * with their text equivalents. As this function performs the conversion | ||||
|      * through incremental changes only, cursor position within the given | ||||
|      * element is preserved. | ||||
|      * | ||||
|      * @param {Element} element | ||||
|      *     The elements whose contents should be converted to plain text. | ||||
|      */ | ||||
|     var convertToText = function convertToText(element) { | ||||
|  | ||||
|         // For each child of the given element | ||||
|         var current = element.firstChild; | ||||
|         while (current) { | ||||
|  | ||||
|             // Preserve the next child in the list, in case the current | ||||
|             // node is replaced | ||||
|             var next = current.nextSibling; | ||||
|  | ||||
|             // If the child is not already a text node, replace it with its | ||||
|             // own text contents | ||||
|             if (current.nodeType !== Node.TEXT_NODE) { | ||||
|                 var textNode = document.createTextNode(current.textContent); | ||||
|                 current.parentElement.replaceChild(textNode, current); | ||||
|             } | ||||
|  | ||||
|             // Advance to next child | ||||
|             current = next; | ||||
|  | ||||
|         } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Sets the local clipboard, if possible, to the given text. | ||||
| @@ -71,30 +169,20 @@ angular.module('clipboard').factory('clipboardService', ['$injector', | ||||
|  | ||||
|         // Track the originally-focused element prior to changing focus | ||||
|         var originalElement = document.activeElement; | ||||
|         pushSelection(); | ||||
|  | ||||
|         // Copy the given value into the clipboard DOM element | ||||
|         clipboardContent.value = 'X'; | ||||
|         clipboardContent.select(); | ||||
|         if (typeof data.data === 'string') | ||||
|             clipboardContent.textContent = data.data; | ||||
|         else { | ||||
|             clipboardContent.innerHTML = ''; | ||||
|             var img = document.createElement('img'); | ||||
|             img.src = URL.createObjectURL(data.data); | ||||
|             clipboardContent.appendChild(img); | ||||
|         } | ||||
|  | ||||
|         // Override copied contents of clipboard with the provided value | ||||
|         clipboardContent.oncopy = function overrideContent(e) { | ||||
|  | ||||
|             // Override the contents of the clipboard | ||||
|             e.preventDefault(); | ||||
|  | ||||
|             // Remove anything already present within the clipboard | ||||
|             var items = e.clipboardData.items; | ||||
|             items.clear(); | ||||
|  | ||||
|             // If the provided data is a string, add it as such | ||||
|             if (typeof data.data === 'string') | ||||
|                 items.add(data.data, data.type); | ||||
|  | ||||
|             // Otherwise, add as a File | ||||
|             else | ||||
|                 items.add(new File([data.data], 'data', { type : data.type })); | ||||
|  | ||||
|         }; | ||||
|         // Select all data within the clipboard target | ||||
|         selectAll(clipboardContent); | ||||
|  | ||||
|         // Attempt to copy data from clipboard element into local clipboard | ||||
|         if (document.execCommand('copy')) | ||||
| @@ -106,10 +194,128 @@ angular.module('clipboard').factory('clipboardService', ['$injector', | ||||
|         // restoring whichever element was originally focused | ||||
|         clipboardContent.blur(); | ||||
|         originalElement.focus(); | ||||
|         popSelection(); | ||||
|  | ||||
|         return deferred.promise; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Parses the given data URL, returning its decoded contents as a new Blob. | ||||
|      * If the URL is not a valid data URL, null will be returned instead. | ||||
|      * | ||||
|      * @param {String} url | ||||
|      *     The data URL to parse. | ||||
|      * | ||||
|      * @returns {Blob} | ||||
|      *     A new Blob containing the decoded contents of the data URL, or null | ||||
|      *     if the URL is not a valid data URL. | ||||
|      */ | ||||
|     service.parseDataURL = function parseDataURL(url) { | ||||
|  | ||||
|         // Parse given string as a data URL | ||||
|         var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url); | ||||
|         if (!result) | ||||
|             return null; | ||||
|  | ||||
|         // Pull the mimetype and base64 contents of the data URL | ||||
|         var type = result[1]; | ||||
|         var data = $window.atob(result[2]); | ||||
|  | ||||
|         // Convert the decoded binary string into a typed array | ||||
|         var buffer = new Uint8Array(data.length); | ||||
|         for (var i = 0; i < data.length; i++) | ||||
|             buffer[i] = data.charCodeAt(i); | ||||
|  | ||||
|         // Produce a proper blob containing the data and type provided in | ||||
|         // the data URL | ||||
|         return new Blob([buffer], { type : type }); | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Replaces the current text content of the given element with the given | ||||
|      * text. To avoid affecting the position of the cursor within an editable | ||||
|      * element, or firing unnecessary DOM modification events, the underlying | ||||
|      * <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 non-text content while preserving cursor position | ||||
|         convertToText(element); | ||||
|  | ||||
|         // Reset text content only if doing so will actually change the content | ||||
|         if (element.textContent !== text) | ||||
|             element.textContent = text; | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Returns the URL of the single image within the given element, if the | ||||
|      * element truly contains only one child and that child is an image. If the | ||||
|      * content of the element is mixed or not an image, null is returned. | ||||
|      * | ||||
|      * @param {Element} element | ||||
|      *     The element whose image content should be retrieved. | ||||
|      * | ||||
|      * @returns {String} | ||||
|      *     The URL of the image contained within the given element, if that | ||||
|      *     element contains only a single child element which happens to be an | ||||
|      *     image, or null if the content of the element is not purely an image. | ||||
|      */ | ||||
|     service.getImageContent = function getImageContent(element) { | ||||
|  | ||||
|         // Return the source of the single child element, if it is an image | ||||
|         var firstChild = element.firstChild; | ||||
|         if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling) | ||||
|             return firstChild.getAttribute('src'); | ||||
|  | ||||
|         // Otherwise, the content of this element is not simply an image | ||||
|         return null; | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Replaces the current contents of the given element with a single image | ||||
|      * having the given URL. To avoid affecting the position of the cursor | ||||
|      * within an editable element, or firing unnecessary DOM modification | ||||
|      * events, the content of the element is only touched if doing so would | ||||
|      * actually change content. | ||||
|      * | ||||
|      * @param {Element} element | ||||
|      *     The element whose image content should be changed. | ||||
|      * | ||||
|      * @param {String} url | ||||
|      *     The URL of the image which should be assigned as the contents of the | ||||
|      *     given element. | ||||
|      */ | ||||
|     service.setImageContent = function setImageContent(element, url) { | ||||
|  | ||||
|         // Retrieve the URL of the current image contents, if any | ||||
|         var currentImage = service.getImageContent(element); | ||||
|  | ||||
|         // If the current contents are not the given image (or not an image | ||||
|         // at all), reassign the contents | ||||
|         if (currentImage !== url) { | ||||
|  | ||||
|             // Clear current contents | ||||
|             element.innerHTML = ''; | ||||
|  | ||||
|             // Add a new image as the sole contents of the element | ||||
|             var img = document.createElement('img'); | ||||
|             img.src = url; | ||||
|             element.appendChild(img); | ||||
|  | ||||
|         } | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get the current value of the local clipboard. | ||||
|      * | ||||
| @@ -124,24 +330,50 @@ angular.module('clipboard').factory('clipboardService', ['$injector', | ||||
|  | ||||
|         // Wait for the next event queue run before attempting to read | ||||
|         // clipboard data (in case the copy/cut has not yet completed) | ||||
|         window.setTimeout(function deferredClipboardRead() { | ||||
|         $window.setTimeout(function deferredClipboardRead() { | ||||
|  | ||||
|             // Track the originally-focused element prior to changing focus | ||||
|             var originalElement = document.activeElement; | ||||
|             pushSelection(); | ||||
|  | ||||
|             // Clear and select the clipboard DOM element | ||||
|             clipboardContent.value = ''; | ||||
|             clipboardContent.innerHTML = ''; | ||||
|             clipboardContent.focus(); | ||||
|             clipboardContent.select(); | ||||
|  | ||||
|             // FIXME: Only handling text data | ||||
|             selectAll(clipboardContent); | ||||
|  | ||||
|             // Attempt paste local clipboard into clipboard DOM element | ||||
|             if (document.activeElement === clipboardContent && document.execCommand('paste')) | ||||
|                 deferred.resolve(new ClipboardData({ | ||||
|                     type : 'text/plain', | ||||
|                     data : clipboardContent.value | ||||
|                 })); | ||||
|             if (document.activeElement === clipboardContent && document.execCommand('paste')) { | ||||
|  | ||||
|                 // If the pasted data is a single image, resolve with a blob | ||||
|                 // containing that image | ||||
|                 var currentImage = service.getImageContent(clipboardContent); | ||||
|                 if (currentImage) { | ||||
|  | ||||
|                     // Convert the image's data URL into a blob | ||||
|                     var blob = service.parseDataURL(currentImage); | ||||
|                     if (blob) { | ||||
|                         deferred.resolve(new ClipboardData({ | ||||
|                             type : blob.type, | ||||
|                             data : blob | ||||
|                         })); | ||||
|                     } | ||||
|  | ||||
|                     // Reject if conversion fails | ||||
|                     else | ||||
|                         deferred.reject(); | ||||
|  | ||||
|                 } // end if clipboard is an image | ||||
|  | ||||
|                 // Otherwise, assume the clipboard contains plain text | ||||
|                 else | ||||
|                     deferred.resolve(new ClipboardData({ | ||||
|                         type : 'text/plain', | ||||
|                         data : clipboardContent.textContent | ||||
|                     })); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             // Otherwise, reading from the clipboard has failed | ||||
|             else | ||||
|                 deferred.reject(); | ||||
|  | ||||
| @@ -149,6 +381,7 @@ angular.module('clipboard').factory('clipboardService', ['$injector', | ||||
|             // restoring whichever element was originally focused | ||||
|             clipboardContent.blur(); | ||||
|             originalElement.focus(); | ||||
|             popSelection(); | ||||
|  | ||||
|         }, 100); | ||||
|  | ||||
|   | ||||
| @@ -48,3 +48,12 @@ | ||||
|     border: 1px solid black; | ||||
|     background: url('images/checker.png'); | ||||
| } | ||||
|  | ||||
| .clipboard-service-target { | ||||
|     position: fixed; | ||||
|     left: -1px; | ||||
|     right: -1px; | ||||
|     width: 1px; | ||||
|     height: 1px; | ||||
|     overflow: hidden; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user