Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clipboard support, continued #1562

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions core/clipboard.js
Original file line number Diff line number Diff line change
@@ -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;
23 changes: 20 additions & 3 deletions core/rfb.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.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";
Expand Down Expand Up @@ -132,6 +133,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._gestures = null; // Gesture input handler object

Expand Down Expand Up @@ -228,6 +230,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);

Expand Down Expand Up @@ -274,8 +279,10 @@ export default class RFB extends EventTargetMixin {
this._rfbConnectionState === "connected") {
if (viewOnly) {
this._keyboard.ungrab();
this._clipboard.ungrab();
} else {
this._keyboard.grab();
this._clipboard.grab();
}
}
}
Expand Down Expand Up @@ -1745,6 +1752,7 @@ export default class RFB extends EventTargetMixin {
this._resize(width, height);

if (!this._viewOnly) { this._keyboard.grab(); }
if (!this._viewOnly) { this._clipboard.grab(); }

this._fbDepth = 24;

Expand Down Expand Up @@ -1861,9 +1869,18 @@ 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);
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");
}
this._canvas.dispatchEvent(clipboardEvent);
}

} else {
//Extended msg.
Expand Down
24 changes: 23 additions & 1 deletion docs/API-internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ 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.


## 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.
Expand Down Expand Up @@ -87,3 +89,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
68 changes: 68 additions & 0 deletions tests/test.clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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('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();
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");
}
clipboard._handleCopy(clipboardEvent);
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);
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;
}
}
});
});