From db039252c7cfc5dd2b2dd91d2ff43815525c1c08 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Wed, 18 Dec 2019 15:24:03 +0200 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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(); From ed6ac63747174d2142c093498529199f94c59604 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 00:01:04 +0800 Subject: [PATCH 05/11] fix: fix paste not working bug --- core/clipboard.js | 20 ++++++++++++++------ core/input/keyboard.js | 4 ++++ core/rfb.js | 10 +++++++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/clipboard.js b/core/clipboard.js index a4a681322..921825915 100644 --- a/core/clipboard.js +++ b/core/clipboard.js @@ -19,27 +19,35 @@ export default class Clipboard { navigator.clipboard.writeText(e.clipboardData.getData('text/plain')).catch(() => {/* Do nothing */}); } } - + /** + * @param {ClipboardEvent} e + */ _handlePaste(e) { - if (navigator.clipboard.readText) { - navigator.clipboard.readText().then(this.onpaste).catch(() => {/* Do nothing */}); - } else if (e.clipboardData) { + if(!this._isVncEvent()){ + return; + } + if(e.clipboardData){ this.onpaste(e.clipboardData.getData('text/plain')); } } + _isVncEvent(){ + const isTargetFocused = document.activeElement === this._target; + return isTargetFocused; + } // ===== PUBLIC METHODS ===== grab() { if (!Clipboard.isSupported) return; this._target.addEventListener('copy', this._eventHandlers.copy); - this._target.addEventListener('paste', this._eventHandlers.paste); + // _target can not listen the paste event. + document.body.addEventListener('paste', this._eventHandlers.paste); } ungrab() { if (!Clipboard.isSupported) return; this._target.removeEventListener('copy', this._eventHandlers.copy); - this._target.removeEventListener('paste', this._eventHandlers.paste); + document.body.removeEventListener('paste', this._eventHandlers.paste); } } diff --git a/core/input/keyboard.js b/core/input/keyboard.js index 9068e9e9f..d96f477f9 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -85,6 +85,10 @@ export default class Keyboard { _handleKeyDown(e) { const code = this._getKeyCode(e); + const isCtrlVEvent = code === "KeyV" && e.ctrlKey; + if(isCtrlVEvent){ + return; + } let keysym = KeyboardUtil.getKeysym(e); let numlock = e.getModifierState('NumLock'); let capslock = e.getModifierState('CapsLock'); diff --git a/core/rfb.js b/core/rfb.js index 0bf6ff241..efae90e39 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -261,7 +261,7 @@ export default class RFB extends EventTargetMixin { } this._clipboard = new Clipboard(this._canvas); - this._clipboard.onpaste = this.clipboardPasteFrom.bind(this); + this._clipboard.onpaste = this._handlePasteEvent.bind(this); this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); @@ -500,6 +500,14 @@ export default class RFB extends EventTargetMixin { this._canvas.blur(); } + _handlePasteEvent(text){ + this.clipboardPasteFrom(text); + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true) + this.sendKey(KeyTable.XK_V, "KeyV", true) + this.sendKey(KeyTable.XK_V, "KeyV", false) + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false) + } + clipboardPasteFrom(text) { if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; } From e3f16f1dd672913cfad9c7e9467a9695ad252481 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 00:20:33 +0800 Subject: [PATCH 06/11] fix: fix pasting garbled text issue when server only supports ascii --- core/clipboard.js | 24 ++++++++++++++++++++++-- core/rfb.js | 6 ++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/core/clipboard.js b/core/clipboard.js index 921825915..5f67bd34f 100644 --- a/core/clipboard.js +++ b/core/clipboard.js @@ -1,4 +1,8 @@ export default class Clipboard { + /** + * @type {string} + */ + _remoteClipboard constructor(target) { this._target = target; @@ -15,8 +19,9 @@ export default class Clipboard { // ===== PRIVATE METHODS ===== _handleCopy(e) { + this._remoteClipboard = e.clipboardData.getData('text/plain'); if (navigator.clipboard.writeText) { - navigator.clipboard.writeText(e.clipboardData.getData('text/plain')).catch(() => {/* Do nothing */}); + navigator.clipboard.writeText(this._remoteClipboard).catch(() => {/* Do nothing */}); } } /** @@ -27,9 +32,24 @@ export default class Clipboard { return; } if(e.clipboardData){ - this.onpaste(e.clipboardData.getData('text/plain')); + const localClipboard = e.clipboardData.getData('text/plain'); + if(localClipboard === this._remoteClipboard){ + this._pasteVncServerInternalClipboard(); + return; + } + this.onpaste(localClipboard); } } + /** + * The vnc server clipboard can be non ascii text and server might only support ascii code. + * In that case, localClipboard received from the vnc server is garbled. + * For example, if you copied chinese text "你好" in the vnc server the local clipboard will be changed to "??". + * If you press Ctrl+V, the vnc server should paste "你好" instead of "??". + * So, we shouldn't send the local clipboard to the vnc server because the local clipboard is garbled in this case. + */ + _pasteVncServerInternalClipboard(){ + this.onpaste("", false); + } _isVncEvent(){ const isTargetFocused = document.activeElement === this._target; return isTargetFocused; diff --git a/core/rfb.js b/core/rfb.js index efae90e39..6d46e865a 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -500,8 +500,10 @@ export default class RFB extends EventTargetMixin { this._canvas.blur(); } - _handlePasteEvent(text){ - this.clipboardPasteFrom(text); + _handlePasteEvent(text, shouldUpdateRemoteClipboard = true){ + if(shouldUpdateRemoteClipboard){ + this.clipboardPasteFrom(text); + } this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true) this.sendKey(KeyTable.XK_V, "KeyV", true) this.sendKey(KeyTable.XK_V, "KeyV", false) From e0003064f15c91882483b72146ce0330ec4f7592 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 00:23:15 +0800 Subject: [PATCH 07/11] fix: change async clipboard api with execCommand --- core/clipboard.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/clipboard.js b/core/clipboard.js index 5f67bd34f..da2c46577 100644 --- a/core/clipboard.js +++ b/core/clipboard.js @@ -20,9 +20,19 @@ export default class Clipboard { _handleCopy(e) { this._remoteClipboard = e.clipboardData.getData('text/plain'); - if (navigator.clipboard.writeText) { - navigator.clipboard.writeText(this._remoteClipboard).catch(() => {/* Do nothing */}); - } + this._copy(this._remoteClipboard) + } + /** + * Has a better browser support compared with navigator.clipboard.writeText. + * Also, no permission required. + */ + _copy(text) { + const textarea = document.createElement('textarea'); + textarea.innerHTML = text; + document.body.appendChild(textarea); + textarea.select(); + const result = document.execCommand('copy'); + document.body.removeChild(textarea); } /** * @param {ClipboardEvent} e From 5daae308823c5ecead1436a732992e1152b712a4 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 00:35:10 +0800 Subject: [PATCH 08/11] chore: update API-internal --- docs/API-internal.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/API-internal.md b/docs/API-internal.md index 9c6adc8d9..c5671c2a0 100644 --- a/docs/API-internal.md +++ b/docs/API-internal.md @@ -83,3 +83,23 @@ None | blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display | drawImage | (img, x, y) | Draw image and track damage | autoscale | (containerWidth, containerHeight) | Scale the display + + +## 2.3 Clipboard Module + +### 2.3.1 Configuration Attributes + +None + +### 2.3.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 From 79ef0c342099a37253d5fb7cea3002ba7ffa5fc5 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 01:29:45 +0800 Subject: [PATCH 09/11] chore: fix lint --- core/clipboard.js | 27 +++++++++++----------- core/input/keyboard.js | 2 +- core/rfb.js | 12 +++++----- tests/test.clipboard.js | 50 ++++++++++++++++++++--------------------- 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/core/clipboard.js b/core/clipboard.js index da2c46577..d0740016f 100644 --- a/core/clipboard.js +++ b/core/clipboard.js @@ -1,8 +1,4 @@ export default class Clipboard { - /** - * @type {string} - */ - _remoteClipboard constructor(target) { this._target = target; @@ -10,7 +6,10 @@ export default class Clipboard { 'copy': this._handleCopy.bind(this), 'paste': this._handlePaste.bind(this) }; - + /** + * @type {string} + */ + this._remoteClipboard = null; // ===== EVENT HANDLERS ===== this.onpaste = () => {}; @@ -20,7 +19,7 @@ export default class Clipboard { _handleCopy(e) { this._remoteClipboard = e.clipboardData.getData('text/plain'); - this._copy(this._remoteClipboard) + this._copy(this._remoteClipboard); } /** * Has a better browser support compared with navigator.clipboard.writeText. @@ -31,19 +30,19 @@ export default class Clipboard { textarea.innerHTML = text; document.body.appendChild(textarea); textarea.select(); - const result = document.execCommand('copy'); + document.execCommand('copy'); document.body.removeChild(textarea); } /** - * @param {ClipboardEvent} e + * @param {ClipboardEvent} e */ _handlePaste(e) { - if(!this._isVncEvent()){ + if (!this._isVncEvent()) { return; } - if(e.clipboardData){ + if (e.clipboardData) { const localClipboard = e.clipboardData.getData('text/plain'); - if(localClipboard === this._remoteClipboard){ + if (localClipboard === this._remoteClipboard) { this._pasteVncServerInternalClipboard(); return; } @@ -53,14 +52,14 @@ export default class Clipboard { /** * The vnc server clipboard can be non ascii text and server might only support ascii code. * In that case, localClipboard received from the vnc server is garbled. - * For example, if you copied chinese text "你好" in the vnc server the local clipboard will be changed to "??". + * For example, if you copied chinese text "你好" in the vnc server the local clipboard will be changed to "??". * If you press Ctrl+V, the vnc server should paste "你好" instead of "??". * So, we shouldn't send the local clipboard to the vnc server because the local clipboard is garbled in this case. */ - _pasteVncServerInternalClipboard(){ + _pasteVncServerInternalClipboard() { this.onpaste("", false); } - _isVncEvent(){ + _isVncEvent() { const isTargetFocused = document.activeElement === this._target; return isTargetFocused; } diff --git a/core/input/keyboard.js b/core/input/keyboard.js index d96f477f9..8030bc9a2 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -86,7 +86,7 @@ export default class Keyboard { _handleKeyDown(e) { const code = this._getKeyCode(e); const isCtrlVEvent = code === "KeyV" && e.ctrlKey; - if(isCtrlVEvent){ + if (isCtrlVEvent) { return; } let keysym = KeyboardUtil.getKeysym(e); diff --git a/core/rfb.js b/core/rfb.js index 6d46e865a..4ec3870ea 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -500,14 +500,14 @@ export default class RFB extends EventTargetMixin { this._canvas.blur(); } - _handlePasteEvent(text, shouldUpdateRemoteClipboard = true){ - if(shouldUpdateRemoteClipboard){ + _handlePasteEvent(text, shouldUpdateRemoteClipboard = true) { + if (shouldUpdateRemoteClipboard) { this.clipboardPasteFrom(text); } - this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true) - this.sendKey(KeyTable.XK_V, "KeyV", true) - this.sendKey(KeyTable.XK_V, "KeyV", false) - this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false) + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); + this.sendKey(KeyTable.XK_V, "KeyV", true); + this.sendKey(KeyTable.XK_V, "KeyV", false); + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); } clipboardPasteFrom(text) { diff --git a/tests/test.clipboard.js b/tests/test.clipboard.js index 0c43d282b..e2c9275f0 100644 --- a/tests/test.clipboard.js +++ b/tests/test.clipboard.js @@ -5,26 +5,6 @@ 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('is supported on all target browsers', function () { expect(Clipboard.isSupported).to.be.true; }); @@ -40,17 +20,16 @@ describe('Automatic Clipboard Sync', function () { if (!clipboardEvent.clipboardData.items.length) { clipboardEvent.clipboardData.items.add(text, "text/plain"); } + sinon.spy(clipboard, '_copy'); clipboard._handleCopy(clipboardEvent); - if (navigator.clipboard.writeText) { - expect(navigator.clipboard.writeText).to.have.been.calledWith(text); - } + expect(clipboard._copy).to.have.been.calledWith(text); + expect(clipboard._remoteClipboard).to.eq(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); @@ -59,10 +38,29 @@ describe('Automatic Clipboard Sync', function () { if (!clipboardEvent.clipboardData.items.length) { clipboardEvent.clipboardData.items.add(text, "text/plain"); } + sinon.stub(clipboard, '_isVncEvent').returns(true); + sinon.spy(clipboard, 'onpaste'); clipboard._handlePaste(clipboardEvent); - if (navigator.clipboard.readText) { - expect(navigator.clipboard.readText).to.have.been.called; + expect(clipboard.onpaste).to.have.been.calledWith(text); + } + }); + + it('should not copy local pasted data to the server clipboard', function () { + const text = 'Another random string for testing'; + const clipboard = new Clipboard(); + clipboard._remoteClipboard = text; + if (Clipboard.isSupported) { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + 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"); } + sinon.stub(clipboard, '_isVncEvent').returns(true); + sinon.spy(clipboard, 'onpaste'); + clipboard._handlePaste(clipboardEvent); + expect(clipboard.onpaste).to.have.been.calledWith("", false); } }); }); From 8413040f0d4779d3bf267fc5514cf071bc0e1009 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 01:30:08 +0800 Subject: [PATCH 10/11] tests: fix tests --- tests/test.clipboard.js | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/test.clipboard.js b/tests/test.clipboard.js index e2c9275f0..e2382bcaa 100644 --- a/tests/test.clipboard.js +++ b/tests/test.clipboard.js @@ -13,13 +13,7 @@ describe('Automatic Clipboard Sync', function () { const text = 'Random string for testing'; const clipboard = new Clipboard(); if (Clipboard.isSupported) { - const clipboardData = new DataTransfer(); - clipboardData.setData("text/plain", text); - 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"); - } + const clipboardEvent = getClipboardEvent(text); sinon.spy(clipboard, '_copy'); clipboard._handleCopy(clipboardEvent); expect(clipboard._copy).to.have.been.calledWith(text); @@ -31,13 +25,7 @@ describe('Automatic Clipboard Sync', function () { const text = 'Another random string for testing'; const clipboard = new Clipboard(); if (Clipboard.isSupported) { - const clipboardData = new DataTransfer(); - clipboardData.setData("text/plain", text); - 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"); - } + const clipboardEvent = getClipboardEvent(text); sinon.stub(clipboard, '_isVncEvent').returns(true); sinon.spy(clipboard, 'onpaste'); clipboard._handlePaste(clipboardEvent); @@ -50,13 +38,7 @@ describe('Automatic Clipboard Sync', function () { const clipboard = new Clipboard(); clipboard._remoteClipboard = text; if (Clipboard.isSupported) { - const clipboardData = new DataTransfer(); - clipboardData.setData("text/plain", text); - 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"); - } + const clipboardEvent = getClipboardEvent(text); sinon.stub(clipboard, '_isVncEvent').returns(true); sinon.spy(clipboard, 'onpaste'); clipboard._handlePaste(clipboardEvent); @@ -64,3 +46,14 @@ describe('Automatic Clipboard Sync', function () { } }); }); + +function getClipboardEvent(text) { + const clipboardData = new DataTransfer(); + clipboardData.setData("text/plain", text); + 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"); + } + return clipboardEvent; +} \ No newline at end of file From df5e09f5793740037699238cbe48053818874489 Mon Sep 17 00:00:00 2001 From: xianshenglu Date: Sun, 12 Nov 2023 22:53:13 +0800 Subject: [PATCH 11/11] feat: support Mac copy/paste --- core/input/keyboard.js | 5 +++-- core/rfb.js | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/input/keyboard.js b/core/input/keyboard.js index 8030bc9a2..d24dedf5d 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -85,8 +85,9 @@ export default class Keyboard { _handleKeyDown(e) { const code = this._getKeyCode(e); - const isCtrlVEvent = code === "KeyV" && e.ctrlKey; - if (isCtrlVEvent) { + const isPasteKeydownEvent = (browser.isWindows() && code === "KeyV" && e.ctrlKey) + || (browser.isMac() && code === "KeyV" && e.metaKey); + if (isPasteKeydownEvent) { return; } let keysym = KeyboardUtil.getKeysym(e); diff --git a/core/rfb.js b/core/rfb.js index 4ec3870ea..57bd019bf 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -36,6 +36,7 @@ import TightDecoder from "./decoders/tight.js"; import TightPNGDecoder from "./decoders/tightpng.js"; import ZRLEDecoder from "./decoders/zrle.js"; import JPEGDecoder from "./decoders/jpeg.js"; +import * as browser from "./util/browser.js"; // How many seconds to wait for a disconnect to finish const DISCONNECT_TIMEOUT = 3; @@ -504,10 +505,19 @@ export default class RFB extends EventTargetMixin { if (shouldUpdateRemoteClipboard) { this.clipboardPasteFrom(text); } - this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); - this.sendKey(KeyTable.XK_V, "KeyV", true); - this.sendKey(KeyTable.XK_V, "KeyV", false); - this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); + if (browser.isMac()) { + this.sendKey(KeyTable.XK_Meta_L, "Meta", true); + this.sendKey(KeyTable.XK_V, "KeyV", true); + this.sendKey(KeyTable.XK_V, "KeyV", false); + this.sendKey(KeyTable.XK_Meta_L, "Meta", false); + return; + } + if (browser.isWindows()) { + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); + this.sendKey(KeyTable.XK_V, "KeyV", true); + this.sendKey(KeyTable.XK_V, "KeyV", false); + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); + } } clipboardPasteFrom(text) {