guacamole-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jmuehl...@apache.org
Subject [12/32] incubator-guacamole-client git commit: GUACAMOLE-55: Update clipboardService to support non-text contents.
Date Thu, 30 Jun 2016 05:37:06 GMT
GUACAMOLE-55: Update clipboardService to support non-text contents.

Project: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/commit/ea5ee182
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/ea5ee182
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/ea5ee182

Branch: refs/heads/master
Commit: ea5ee1825b2190d2c62b3873d20e275377b059ab
Parents: 6e4e645
Author: Michael Jumper <mjumper@apache.org>
Authored: Tue Jun 28 13:43:31 2016 -0700
Committer: Michael Jumper <mjumper@apache.org>
Committed: Tue Jun 28 13:51:24 2016 -0700

----------------------------------------------------------------------
 .../app/clipboard/directives/guacClipboard.js   | 162 +--------
 .../app/clipboard/services/clipboardService.js  | 329 ++++++++++++++++---
 .../webapp/app/clipboard/styles/clipboard.css   |   9 +
 3 files changed, 294 insertions(+), 206 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/ea5ee182/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js
index a55f398..3a44d38 100644
--- a/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js
+++ b/guacamole/src/main/webapp/app/clipboard/directives/guacClipboard.js
@@ -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);
                 }

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/ea5ee182/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js
index ac06400..0dc2b7a 100644
--- a/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js
+++ b/guacamole/src/main/webapp/app/clipboard/services/clipboardService.js
@@ -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 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);
+
+    /**
+     * 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) {
 
-    // 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';
+        // For each child of the given element
+        var current = element.firstChild;
+        while (current) {
 
-    // Add textarea to DOM
-    document.body.appendChild(clipElement);
+            // 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();
-
-        // 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 }));
-
-        };
+        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'))
@@ -106,11 +194,129 @@ 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.
      *
      * @return {Promise.<ClipboardData>}
@@ -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);
 

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/ea5ee182/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css
index 10e3eaf..ab02af0 100644
--- a/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css
+++ b/guacamole/src/main/webapp/app/clipboard/styles/clipboard.css
@@ -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;
+}


Mime
View raw message