From 303da371c62a05da1f854b0dbe2342ccc1c27d81 Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 18 Nov 2019 13:35:09 -0600 Subject: [PATCH] simple implementation in vue --- package-lock.json | 5 + package.json | 1 + src/App.vue | 215 +++++++++++++++++-- src/components/GuacClient.vue | 286 ++++++++++++++++++++++++++ src/components/HelloWorld.vue | 58 ------ src/components/Modal.vue | 65 ++++++ src/lib/GuacMouse.js | 375 ++++++++++++++++++++++++++++++++++ src/lib/clipboard.js | 97 +++++++++ src/lib/states.js | 55 +++++ 9 files changed, 1081 insertions(+), 76 deletions(-) create mode 100644 src/components/GuacClient.vue delete mode 100644 src/components/HelloWorld.vue create mode 100644 src/components/Modal.vue create mode 100644 src/lib/GuacMouse.js create mode 100644 src/lib/clipboard.js create mode 100644 src/lib/states.js diff --git a/package-lock.json b/package-lock.json index dddbaa1..6b99913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5630,6 +5630,11 @@ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, + "guacamole-common-js": { + "version": "1.1.0-b", + "resolved": "https://registry.npmjs.org/guacamole-common-js/-/guacamole-common-js-1.1.0-b.tgz", + "integrity": "sha512-aU0AilOowlsw2QEb7F7a+L6+cdc40D38Pe7rRnbuKZ5KTqcwZK6dworQNrCbrSavKNH/+qvAcQkskMntVBmMaQ==" + }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", diff --git a/package.json b/package.json index 63d9b17..5a4fee9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "core-js": "^3.3.2", + "guacamole-common-js": "^1.1.0-b", "vue": "^2.6.10" }, "devDependencies": { diff --git a/src/App.vue b/src/App.vue index fcc5662..8f35553 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,28 +1,207 @@ diff --git a/src/components/GuacClient.vue b/src/components/GuacClient.vue new file mode 100644 index 0000000..3d6c8d3 --- /dev/null +++ b/src/components/GuacClient.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index 879051a..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - diff --git a/src/components/Modal.vue b/src/components/Modal.vue new file mode 100644 index 0000000..ced0c17 --- /dev/null +++ b/src/components/Modal.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/lib/GuacMouse.js b/src/lib/GuacMouse.js new file mode 100644 index 0000000..42797b8 --- /dev/null +++ b/src/lib/GuacMouse.js @@ -0,0 +1,375 @@ +import Guacamole from 'guacamole-common-js' + +const mouse = function(element) { + + /** + * Reference to this Guacamole.Mouse. + * @private + */ + var guac_mouse = this; + + /** + * The number of mousemove events to require before re-enabling mouse + * event handling after receiving a touch event. + */ + this.touchMouseThreshold = 3; + + /** + * The minimum amount of pixels scrolled required for a single scroll button + * click. + */ + this.scrollThreshold = 53; + + /** + * The number of pixels to scroll per line. + */ + this.PIXELS_PER_LINE = 18; + + /** + * The number of pixels to scroll per page. + */ + this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16; + + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type {Guacamole.Mouse.State} + */ + this.currentState = new Guacamole.Mouse.State( + 0, 0, + false, false, false, false, false + ); + + /** + * Fired whenever the user presses a mouse button down over the element + * associated with this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ +this.onmousedown = null; + + /** + * Fired whenever the user releases a mouse button down over the element + * associated with this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ +this.onmouseup = null; + + /** + * Fired whenever the user moves the mouse over the element associated with + * this Guacamole.Mouse. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ +this.onmousemove = null; + + /** + * Fired whenever the mouse leaves the boundaries of the element associated + * with this Guacamole.Mouse. + * + * @event + */ +this.onmouseout = null; + + /** + * Counter of mouse events to ignore. This decremented by mousemove, and + * while non-zero, mouse events will have no effect. + * @private + */ + var ignore_mouse = 0; + + /** + * Cumulative scroll delta amount. This value is accumulated through scroll + * events and results in scroll button clicks if it exceeds a certain + * threshold. + * + * @private + */ + var scroll_delta = 0; + + function cancelEvent(e) { + e.stopPropagation(); + if (e.preventDefault) e.preventDefault(); + e.returnValue = false; + } + + // Block context menu so right-click gets sent properly + element.addEventListener("contextmenu", function(e) { + cancelEvent(e); + }, false); + + element.addEventListener("mousemove", function(e) { + + // If ignoring events, decrement counter + if (ignore_mouse) { + ignore_mouse--; + return; + } + + guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY); + + if (guac_mouse.onmousemove) + guac_mouse.onmousemove(guac_mouse.currentState); + + }, false); + + element.addEventListener("mousedown", function(e) { + + cancelEvent(e); + + // Do not handle if ignoring events + if (ignore_mouse) + return; + + switch (e.button) { + case 0: + guac_mouse.currentState.left = true; + break; + case 1: + guac_mouse.currentState.middle = true; + break; + case 2: + guac_mouse.currentState.right = true; + break; + } + + if (guac_mouse.onmousedown) + guac_mouse.onmousedown(guac_mouse.currentState); + + }, false); + + element.addEventListener("mouseup", function(e) { + + cancelEvent(e); + + // Do not handle if ignoring events + if (ignore_mouse) + return; + + switch (e.button) { + case 0: + guac_mouse.currentState.left = false; + break; + case 1: + guac_mouse.currentState.middle = false; + break; + case 2: + guac_mouse.currentState.right = false; + break; + } + + if (guac_mouse.onmouseup) + guac_mouse.onmouseup(guac_mouse.currentState); + + }, false); + + element.addEventListener("mouseout", function(e) { + + // Get parent of the element the mouse pointer is leaving + if (!e) e = window.event; + + // Check that mouseout is due to actually LEAVING the element + var target = e.relatedTarget || e.toElement; + while (target) { + if (target === element) + return; + target = target.parentNode; + } + + cancelEvent(e); + + // Release all buttons + if (guac_mouse.currentState.left + || guac_mouse.currentState.middle + || guac_mouse.currentState.right) { + + guac_mouse.currentState.left = false; + guac_mouse.currentState.middle = false; + guac_mouse.currentState.right = false; + + if (guac_mouse.onmouseup) + guac_mouse.onmouseup(guac_mouse.currentState); + } + + // Fire onmouseout event + if (guac_mouse.onmouseout) + guac_mouse.onmouseout(); + + }, false); + + // Override selection on mouse event element. + element.addEventListener("selectstart", function(e) { + cancelEvent(e); + }, false); + + // Ignore all pending mouse events when touch events are the apparent source + function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; } + + element.addEventListener("touchmove", ignorePendingMouseEvents, false); + element.addEventListener("touchstart", ignorePendingMouseEvents, false); + element.addEventListener("touchend", ignorePendingMouseEvents, false); + + // Scroll wheel support + function mousewheel_handler(e) { + + // Determine approximate scroll amount (in pixels) + var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta; + + // If successfully retrieved scroll amount, convert to pixels if not + // already in pixels + if (delta) { + + // Convert to pixels if delta was lines + if (e.deltaMode === 1) + delta = e.deltaY * guac_mouse.PIXELS_PER_LINE; + + // Convert to pixels if delta was pages + else if (e.deltaMode === 2) + delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE; + + } + + // Otherwise, assume legacy mousewheel event and line scrolling + else + delta = e.detail * guac_mouse.PIXELS_PER_LINE; + + // Update overall delta + scroll_delta += delta; + + // Up + if (scroll_delta <= -guac_mouse.scrollThreshold) { + + // Repeatedly click the up button until insufficient delta remains + do { + + if (guac_mouse.onmousedown) { + guac_mouse.currentState.up = true; + guac_mouse.onmousedown(guac_mouse.currentState); + } + + if (guac_mouse.onmouseup) { + guac_mouse.currentState.up = false; + guac_mouse.onmouseup(guac_mouse.currentState); + } + + scroll_delta += guac_mouse.scrollThreshold; + + } while (scroll_delta <= -guac_mouse.scrollThreshold); + + // Reset delta + scroll_delta = 0; + + } + + // Down + if (scroll_delta >= guac_mouse.scrollThreshold) { + + // Repeatedly click the down button until insufficient delta remains + do { + + if (guac_mouse.onmousedown) { + guac_mouse.currentState.down = true; + guac_mouse.onmousedown(guac_mouse.currentState); + } + + if (guac_mouse.onmouseup) { + guac_mouse.currentState.down = false; + guac_mouse.onmouseup(guac_mouse.currentState); + } + + scroll_delta -= guac_mouse.scrollThreshold; + + } while (scroll_delta >= guac_mouse.scrollThreshold); + + // Reset delta + scroll_delta = 0; + + } + + cancelEvent(e); + + } + + element.addEventListener('DOMMouseScroll', mousewheel_handler, false); + element.addEventListener('mousewheel', mousewheel_handler, false); + element.addEventListener('wheel', mousewheel_handler, false); + + /** + * Whether the browser supports CSS3 cursor styling, including hotspot + * coordinates. + * + * @private + * @type {Boolean} + */ + var CSS3_CURSOR_SUPPORTED = (function() { + + var div = document.createElement("div"); + + // If no cursor property at all, then no support + if (!("cursor" in div.style)) + return false; + + try { + // Apply simple 1x1 PNG + div.style.cursor = "url(data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" + + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI" + + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA" + + "AABJRU5ErkJggg==) 0 0, auto"; + } + catch (e) { + return false; + } + + // Verify cursor property is set to URL with hotspot + return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || ""); + + })(); + + /** + * Changes the local mouse cursor to the given canvas, having the given + * hotspot coordinates. This affects styling of the element backing this + * Guacamole.Mouse only, and may fail depending on browser support for + * setting the mouse cursor. + * + * If setting the local cursor is desired, it is up to the implementation + * to do something else, such as use the software cursor built into + * Guacamole.Display, if the local cursor cannot be set. + * + * @param {HTMLCanvasElement} canvas The cursor image. + * @param {Number} x The X-coordinate of the cursor hotspot. + * @param {Number} y The Y-coordinate of the cursor hotspot. + * @return {Boolean} true if the cursor was successfully set, false if the + * cursor could not be set for any reason. + */ + this.setCursor = function(canvas, x, y) { + + // Attempt to set via CSS3 cursor styling + if (CSS3_CURSOR_SUPPORTED) { + var dataURL = canvas.toDataURL('image/png'); + element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto"; + return true; + } + + // Otherwise, setting cursor failed + return false; + + }; + +}; + +//attach supporting classes +mouse.State = Guacamole.Mouse.State +mouse.Touchpad = Guacamole.Mouse.Touchpad +mouse.Touchscreen = Guacamole.Mouse.Touchscreen + + +export default { + mouse +} \ No newline at end of file diff --git a/src/lib/clipboard.js b/src/lib/clipboard.js new file mode 100644 index 0000000..b0f1e78 --- /dev/null +++ b/src/lib/clipboard.js @@ -0,0 +1,97 @@ +import Guacamole from 'guacamole-common-js' + +const clipboard = {} + +clipboard.install = (client) => { + clipboard.getLocalClipboard().then(data => clipboard.cache = data) + + window.addEventListener('load', clipboard.update(client), true) + window.addEventListener('copy', clipboard.update(client)) + window.addEventListener('cut', clipboard.update(client)) + window.addEventListener('focus', e => { + if (e.target === window) { + clipboard.update(client)() + } + }, true) +} + +clipboard.update = client => { + return () => { + clipboard.getLocalClipboard().then(data => { + clipboard.cache = data + clipboard.setRemoteClipboard(client) + }) + } +} + +clipboard.setRemoteClipboard = (client) => { + if (!clipboard.cache) { + return + } + + let writer + + const stream = client.createClipboardStream(clipboard.cache.type) + + if (typeof clipboard.cache.data === 'string') { + writer = new Guacamole.StringWriter(stream) + writer.sendText(clipboard.cache.data) + writer.sendEnd() + } else { + writer = new Guacamole.BlobWriter(stream) + writer.oncomplete = function clipboardSent() { + writer.sendEnd() + }; + writer.sendBlob(clipboard.cache.data) + } +} + +clipboard.getLocalClipboard = async () => { + if (navigator.clipboard && navigator.clipboard.readText) { + const text = await navigator.clipboard.readText() + return { + type: 'text/plain', + data: text + } + } +} + +clipboard.setLocalClipboard = async (data) => { + if (navigator.clipboard && navigator.clipboard.writeText) { + if (data.type === 'text/plain') { + await navigator.clipboard.writeText(data.data) + } + } +} + +clipboard.onClipboard = (stream, mimetype) => { + let reader + + if (/^text\//.exec(mimetype)) { + reader = new Guacamole.StringReader(stream); + + // Assemble received data into a single string + let data = ''; + reader.ontext = text => { + data += text; + } + + // Set clipboard contents once stream is finished + reader.onend = () => { + clipboard.setLocalClipboard({ + type: mimetype, + data: data + }) + } + } else { + reader = new Guacamole.BlobReader(stream, mimetype); + reader.onend = () => { + clipboard.setLocalClipboard({ + type: mimetype, + data: reader.getBlob() + }) + } + } +} + +export default clipboard diff --git a/src/lib/states.js b/src/lib/states.js new file mode 100644 index 0000000..5fa2d99 --- /dev/null +++ b/src/lib/states.js @@ -0,0 +1,55 @@ +export default { + /** + * The Guacamole connection has not yet been attempted. + * + * @type String + */ + IDLE : "IDLE", + + /** + * The Guacamole connection is being established. + * + * @type String + */ + CONNECTING : "CONNECTING", + + /** + * The Guacamole connection has been successfully established, and the + * client is now waiting for receipt of initial graphical data. + * + * @type String + */ + WAITING : "WAITING", + + /** + * The Guacamole connection has been successfully established, and + * initial graphical data has been received. + * + * @type String + */ + CONNECTED : "CONNECTED", + + /** + * The Guacamole connection has terminated successfully. No errors are + * indicated. + * + * @type String + */ + DISCONNECTED : "DISCONNECTED", + + /** + * The Guacamole connection has terminated due to an error reported by + * the client. The associated error code is stored in statusCode. + * + * @type String + */ + CLIENT_ERROR : "CLIENT_ERROR", + + /** + * The Guacamole connection has terminated due to an error reported by + * the tunnel. The associated error code is stored in statusCode. + * + * @type String + */ + TUNNEL_ERROR : "TUNNEL_ERROR" +}