From db039252c7cfc5dd2b2dd91d2ff43815525c1c08 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Wed, 18 Dec 2019 15:24:03 +0200 Subject: [PATCH 1/4] Add automatic clipboard support --- core/clipboard.js | 46 +++++++++++++++++++++++++++++++++++ core/rfb.js | 24 ++++++++++++++---- docs/API-internal.md | 24 +++++++++++++++++- tests/test.clipboard.js | 54 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 core/clipboard.js create mode 100644 tests/test.clipboard.js diff --git a/core/clipboard.js b/core/clipboard.js new file mode 100644 index 000000000..a4a681322 --- /dev/null +++ b/core/clipboard.js @@ -0,0 +1,46 @@ +export default class Clipboard { + constructor(target) { + this._target = target; + + this._eventHandlers = { + 'copy': this._handleCopy.bind(this), + 'paste': this._handlePaste.bind(this) + }; + + // ===== EVENT HANDLERS ===== + + this.onpaste = () => {}; + } + + // ===== PRIVATE METHODS ===== + + _handleCopy(e) { + if (navigator.clipboard.writeText) { + navigator.clipboard.writeText(e.clipboardData.getData('text/plain')).catch(() => {/* Do nothing */}); + } + } + + _handlePaste(e) { + if (navigator.clipboard.readText) { + navigator.clipboard.readText().then(this.onpaste).catch(() => {/* Do nothing */}); + } else if (e.clipboardData) { + this.onpaste(e.clipboardData.getData('text/plain')); + } + } + + // ===== PUBLIC METHODS ===== + + grab() { + if (!Clipboard.isSupported) return; + this._target.addEventListener('copy', this._eventHandlers.copy); + this._target.addEventListener('paste', this._eventHandlers.paste); + } + + ungrab() { + if (!Clipboard.isSupported) return; + this._target.removeEventListener('copy', this._eventHandlers.copy); + this._target.removeEventListener('paste', this._eventHandlers.paste); + } +} + +Clipboard.isSupported = (navigator && navigator.clipboard) ? true : false; diff --git a/core/rfb.js b/core/rfb.js index 4a8483fdd..61bd45dd5 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -13,6 +13,7 @@ import { encodeUTF8, decodeUTF8 } from './util/strings.js'; import { dragThreshold } from './util/browser.js'; import EventTargetMixin from './util/eventtarget.js'; import Display from "./display.js"; +import Clipboard from "./clipboard.js"; import Inflator from "./inflator.js"; import Deflator from "./deflator.js"; import Keyboard from "./input/keyboard.js"; @@ -113,6 +114,7 @@ export default class RFB extends EventTargetMixin { this._sock = null; // Websock object this._display = null; // Display object this._flushing = false; // Display flushing state + this._clipboard = null; // Clipboard object this._keyboard = null; // Keyboard input handler object this._mouse = null; // Mouse input handler object @@ -198,6 +200,9 @@ export default class RFB extends EventTargetMixin { } this._display.onflush = this._onFlush.bind(this); + this._clipboard = new Clipboard(this._canvas); + this._clipboard.onpaste = this.clipboardPasteFrom.bind(this); + this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); @@ -292,9 +297,11 @@ export default class RFB extends EventTargetMixin { if (viewOnly) { this._keyboard.ungrab(); this._mouse.ungrab(); + this._clipboard.ungrab(); } else { this._keyboard.grab(); this._mouse.grab(); + this._clipboard.grab(); } } } @@ -1398,8 +1405,11 @@ export default class RFB extends EventTargetMixin { this._setDesktopName(name); this._resize(width, height); - if (!this._viewOnly) { this._keyboard.grab(); } - if (!this._viewOnly) { this._mouse.grab(); } + if (!this._viewOnly) { + this._keyboard.grab(); + this._mouse.grab(); + this._clipboard.grab(); + } this._fb_depth = 24; @@ -1516,9 +1526,13 @@ export default class RFB extends EventTargetMixin { return true; } - this.dispatchEvent(new CustomEvent( - "clipboard", - { detail: { text: text } })); + this.dispatchEvent(new CustomEvent("clipboard", { detail: { text: text } })); + + if (Clipboard.isSupported) { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + this._canvas.dispatchEvent(new ClipboardEvent('copy', { clipboardData })); + } } else { //Extended msg. diff --git a/docs/API-internal.md b/docs/API-internal.md index f1519422e..842fc5307 100644 --- a/docs/API-internal.md +++ b/docs/API-internal.md @@ -21,6 +21,8 @@ keysym values. * __Display__ (core/display.js): Efficient 2D rendering abstraction layered on the HTML5 canvas element. +* __Clipboard__ (core/clipboard.js): Clipboard event handler. + * __Websock__ (core/websock.js): Websock client from websockify with transparent binary data support. [Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page. @@ -28,7 +30,7 @@ with transparent binary data support. ## 1.2 Callbacks -For the Mouse, Keyboard and Display objects the callback functions are +For the Mouse, Keyboard, Display and Clipboard objects the callback functions are assigned to configuration attributes, just as for the RFB object. The WebSock module has a method named 'on' that takes two parameters: the callback event name, and the callback function. @@ -118,3 +120,23 @@ None | name | parameters | description | ------- | ---------- | ------------ | onflush | () | A display flush has been requested and we are now ready to resume FBU processing + + +## 2.4 Clipboard Module + +### 2.4.1 Configuration Attributes + +None + +### 2.4.2 Methods + +| name | parameters | description +| ------------------ | ----------------- | ------------ +| grab | () | Begin capturing clipboard events +| ungrab | () | Stop capturing clipboard events + +### 2.3.3 Callbacks + +| name | parameters | description +| ------- | ---------- | ------------ +| onpaste | (text) | Called with the text content of the clipboard when the user paste something diff --git a/tests/test.clipboard.js b/tests/test.clipboard.js new file mode 100644 index 000000000..ada9b8b63 --- /dev/null +++ b/tests/test.clipboard.js @@ -0,0 +1,54 @@ +const expect = chai.expect; + +import Clipboard from '../core/clipboard.js'; + +describe('Automatic Clipboard Sync', function () { + "use strict"; + + if (Clipboard.isSupported) { + beforeEach(function () { + if (navigator.clipboard.writeText) { + sinon.spy(navigator.clipboard, 'writeText'); + } + if (navigator.clipboard.readText) { + sinon.spy(navigator.clipboard, 'readText'); + } + }); + + afterEach(function () { + if (navigator.clipboard.writeText) { + navigator.clipboard.writeText.restore(); + } + if (navigator.clipboard.readText) { + navigator.clipboard.readText.restore(); + } + }); + } + + it('incoming clipboard data from the server is copied to the local clipboard', function () { + const text = 'Random string for testing'; + const clipboard = new Clipboard(); + if (Clipboard.isSupported) { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + clipboard._handleCopy(new ClipboardEvent('paste', { clipboardData })); + if (navigator.clipboard.writeText) { + expect(navigator.clipboard.writeText).to.have.been.calledWith(text); + } + } + }); + + it('should copy local pasted data to the server clipboard', function () { + const text = 'Another random string for testing'; + const clipboard = new Clipboard(); + clipboard.onpaste = pasterText => expect(pasterText).to.equal(text); + if (Clipboard.isSupported) { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + clipboard._handlePaste(new ClipboardEvent('paste', { clipboardData })); + if (navigator.clipboard.readText) { + expect(navigator.clipboard.readText).to.have.been.called; + } + } + }); +}); From a06661905d740898c3b0929d285fc756d252c2b8 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Tue, 7 Jan 2020 15:12:59 +0100 Subject: [PATCH 2/4] Fix constructor for Firefox --- core/rfb.js | 7 ++++++- tests/test.clipboard.js | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 61bd45dd5..f1f973505 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1531,7 +1531,12 @@ export default class RFB extends EventTargetMixin { if (Clipboard.isSupported) { const clipboardData = new DataTransfer(); clipboardData.setData("text/plain", text); - this._canvas.dispatchEvent(new ClipboardEvent('copy', { clipboardData })); + const clipboardEvent = new ClipboardEvent('paste', { clipboardData }); + // Force initialization since the constructor is broken in Firefox + if (!clipboardEvent.clipboardData.items.length) { + clipboardEvent.clipboardData.items.add(text, "text/plain"); + } + this._canvas.dispatchEvent(clipboardEvent); } } else { diff --git a/tests/test.clipboard.js b/tests/test.clipboard.js index ada9b8b63..2ef005748 100644 --- a/tests/test.clipboard.js +++ b/tests/test.clipboard.js @@ -31,7 +31,12 @@ describe('Automatic Clipboard Sync', function () { if (Clipboard.isSupported) { const clipboardData = new DataTransfer(); clipboardData.setData("text/plain", text); - clipboard._handleCopy(new ClipboardEvent('paste', { clipboardData })); + const clipboardEvent = new ClipboardEvent('paste', { clipboardData }); + // Force initialization since the constructor is broken in Firefox + if (!clipboardEvent.clipboardData.items.length) { + clipboardEvent.clipboardData.items.add(text, "text/plain"); + } + clipboard._handleCopy(clipboardEvent); if (navigator.clipboard.writeText) { expect(navigator.clipboard.writeText).to.have.been.calledWith(text); } @@ -45,7 +50,12 @@ describe('Automatic Clipboard Sync', function () { if (Clipboard.isSupported) { const clipboardData = new DataTransfer(); clipboardData.setData("text/plain", text); - clipboard._handlePaste(new ClipboardEvent('paste', { clipboardData })); + const clipboardEvent = new ClipboardEvent('paste', { clipboardData }); + // Force initialization since the constructor is broken in Firefox + if (!clipboardEvent.clipboardData.items.length) { + clipboardEvent.clipboardData.items.add(text, "text/plain"); + } + clipboard._handlePaste(clipboardEvent); if (navigator.clipboard.readText) { expect(navigator.clipboard.readText).to.have.been.called; } From 8a38fdde76f26cf9ed32bb3da4ae788572744510 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Mon, 4 May 2020 16:58:14 +0300 Subject: [PATCH 3/4] Fix event type fired on copy --- core/rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rfb.js b/core/rfb.js index f1f973505..5e14b5a63 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1531,7 +1531,7 @@ export default class RFB extends EventTargetMixin { if (Clipboard.isSupported) { const clipboardData = new DataTransfer(); clipboardData.setData("text/plain", text); - const clipboardEvent = new ClipboardEvent('paste', { clipboardData }); + const clipboardEvent = new ClipboardEvent('copy', { clipboardData }); // Force initialization since the constructor is broken in Firefox if (!clipboardEvent.clipboardData.items.length) { clipboardEvent.clipboardData.items.add(text, "text/plain"); From a8411789b3c1b827f24c23d24f0e7dd5b3dcaade Mon Sep 17 00:00:00 2001 From: Seth Nickell Date: Mon, 12 Jul 2021 16:25:06 -1000 Subject: [PATCH 4/4] expect(Clipboard.isSupported) to be true Clipboard needs to be supported on all target browsers. --- tests/test.clipboard.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test.clipboard.js b/tests/test.clipboard.js index 2ef005748..0c43d282b 100644 --- a/tests/test.clipboard.js +++ b/tests/test.clipboard.js @@ -25,6 +25,10 @@ describe('Automatic Clipboard Sync', function () { }); } + it('is supported on all target browsers', function () { + expect(Clipboard.isSupported).to.be.true; + }); + it('incoming clipboard data from the server is copied to the local clipboard', function () { const text = 'Random string for testing'; const clipboard = new Clipboard();