@novnc/novnc 1.6.0-g71d0bfa → 1.6.0-g8e1ebdf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ /*
2
+ * noVNC: HTML5 VNC client
3
+ * Copyright (c) 2025 The noVNC authors
4
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
5
+ */
6
+
7
+ import * as Log from './util/logging.js';
8
+ import { browserAsyncClipboardSupport } from './util/browser.js';
9
+
10
+ export default class AsyncClipboard {
11
+ constructor(target) {
12
+ this._target = target || null;
13
+
14
+ this._isAvailable = null;
15
+
16
+ this._eventHandlers = {
17
+ 'focus': this._handleFocus.bind(this),
18
+ };
19
+
20
+ // ===== EVENT HANDLERS =====
21
+
22
+ this.onpaste = () => {};
23
+ }
24
+
25
+ // ===== PRIVATE METHODS =====
26
+
27
+ async _ensureAvailable() {
28
+ if (this._isAvailable !== null) return this._isAvailable;
29
+ try {
30
+ const status = await browserAsyncClipboardSupport();
31
+ this._isAvailable = (status === 'available');
32
+ } catch {
33
+ this._isAvailable = false;
34
+ }
35
+ return this._isAvailable;
36
+ }
37
+
38
+ async _handleFocus(event) {
39
+ if (!(await this._ensureAvailable())) return;
40
+ try {
41
+ const text = await navigator.clipboard.readText();
42
+ this.onpaste(text);
43
+ } catch (error) {
44
+ Log.Error("Clipboard read failed: ", error);
45
+ }
46
+ }
47
+
48
+ // ===== PUBLIC METHODS =====
49
+
50
+ writeClipboard(text) {
51
+ // Can lazily check cached availability
52
+ if (!this._isAvailable) return false;
53
+ navigator.clipboard.writeText(text)
54
+ .catch(error => Log.Error("Clipboard write failed: ", error));
55
+ return true;
56
+ }
57
+
58
+ grab() {
59
+ if (!this._target) return;
60
+ this._ensureAvailable()
61
+ .then((isAvailable) => {
62
+ if (isAvailable) {
63
+ this._target.addEventListener('focus', this._eventHandlers.focus);
64
+ }
65
+ });
66
+ }
67
+
68
+ ungrab() {
69
+ if (!this._target) return;
70
+ this._target.removeEventListener('focus', this._eventHandlers.focus);
71
+ }
72
+ }
@@ -181,11 +181,11 @@ class DES {
181
181
  // Encrypt 8 bytes of text
182
182
  enc8(text) {
183
183
  const b = text.slice();
184
- let i = 0, l, r, x; // left, right, accumulator
184
+ let l, r, x; // left, right, accumulator
185
185
 
186
186
  // Squash 8 bytes to 2 ints
187
- l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
188
- r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
187
+ l = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];
188
+ r = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];
189
189
 
190
190
  x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
191
191
  r ^= x;
@@ -252,7 +252,7 @@ class DES {
252
252
 
253
253
  // Spread ints to bytes
254
254
  x = [r, l];
255
- for (i = 0; i < 8; i++) {
255
+ for (let i = 0; i < 8; i++) {
256
256
  b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
257
257
  if (b[i] < 0) { b[i] += 256; } // unsigned
258
258
  }
@@ -175,7 +175,7 @@ export default class ZRLEDecoder {
175
175
 
176
176
  _readRLELength() {
177
177
  let length = 0;
178
- let current = 0;
178
+ let current;
179
179
  do {
180
180
  current = this._inflator.inflate(1)[0];
181
181
  length += current;
package/core/display.js CHANGED
@@ -14,7 +14,7 @@ export default class Display {
14
14
  constructor(target) {
15
15
  this._drawCtx = null;
16
16
 
17
- this._renderQ = []; // queue drawing actions for in-oder rendering
17
+ this._renderQ = []; // queue drawing actions for in-order rendering
18
18
  this._flushPromise = null;
19
19
 
20
20
  // the full frame buffer (logical canvas) size
package/core/rfb.js CHANGED
@@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
15
15
  import { setCapture } from './util/events.js';
16
16
  import EventTargetMixin from './util/eventtarget.js';
17
17
  import Display from "./display.js";
18
+ import AsyncClipboard from "./clipboard.js";
18
19
  import Inflator from "./inflator.js";
19
20
  import Deflator from "./deflator.js";
20
21
  import Keyboard from "./input/keyboard.js";
@@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
164
165
  this._sock = null; // Websock object
165
166
  this._display = null; // Display object
166
167
  this._flushing = false; // Display flushing state
168
+ this._asyncClipboard = null; // Async clipboard object
167
169
  this._keyboard = null; // Keyboard input handler object
168
170
  this._gestures = null; // Gesture input handler object
169
171
  this._resizeObserver = null; // Resize observer object
@@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
266
268
  throw exc;
267
269
  }
268
270
 
271
+ this._asyncClipboard = new AsyncClipboard(this._canvas);
272
+ this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
273
+
269
274
  this._keyboard = new Keyboard(this._canvas);
270
275
  this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
271
276
  this._remoteCapsLock = null; // Null indicates unknown or irrelevant
@@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
315
320
  this._rfbConnectionState === "connected") {
316
321
  if (viewOnly) {
317
322
  this._keyboard.ungrab();
323
+ this._asyncClipboard.ungrab();
318
324
  } else {
319
325
  this._keyboard.grab();
326
+ this._asyncClipboard.grab();
320
327
  }
321
328
  }
322
329
  }
@@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
2208
2215
  this._setDesktopName(name);
2209
2216
  this._resize(width, height);
2210
2217
 
2211
- if (!this._viewOnly) { this._keyboard.grab(); }
2218
+ if (!this._viewOnly) {
2219
+ this._keyboard.grab();
2220
+ this._asyncClipboard.grab();
2221
+ }
2212
2222
 
2213
2223
  this._fbDepth = 24;
2214
2224
 
@@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
2323
2333
  return this._fail("Unexpected SetColorMapEntries message");
2324
2334
  }
2325
2335
 
2336
+ _writeClipboard(text) {
2337
+ if (this._viewOnly) return;
2338
+ if (this._asyncClipboard.writeClipboard(text)) return;
2339
+ // Fallback clipboard
2340
+ this.dispatchEvent(
2341
+ new CustomEvent("clipboard", {detail: {text: text}})
2342
+ );
2343
+ }
2344
+
2326
2345
  _handleServerCutText() {
2327
2346
  Log.Debug("ServerCutText");
2328
2347
 
@@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
2342
2361
  return true;
2343
2362
  }
2344
2363
 
2345
- this.dispatchEvent(new CustomEvent(
2346
- "clipboard",
2347
- { detail: { text: text } }));
2364
+ this._writeClipboard(text);
2348
2365
 
2349
2366
  } else {
2350
2367
  //Extended msg.
@@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
2480
2497
 
2481
2498
  textData = textData.replaceAll("\r\n", "\n");
2482
2499
 
2483
- this.dispatchEvent(new CustomEvent(
2484
- "clipboard",
2485
- { detail: { text: textData } }));
2500
+ this._writeClipboard(textData);
2486
2501
  }
2487
2502
  } else {
2488
2503
  return this._fail("Unexpected action in extended clipboard message: " + actions);
@@ -2934,7 +2949,7 @@ export default class RFB extends EventTargetMixin {
2934
2949
 
2935
2950
  // We need to handle errors when we requested the resize.
2936
2951
  if (this._FBU.x === 1 && this._FBU.y !== 0) {
2937
- let msg = "";
2952
+ let msg;
2938
2953
  // The y-position indicates the status code from the server
2939
2954
  switch (this._FBU.y) {
2940
2955
  case 1:
@@ -11,6 +11,39 @@
11
11
  import * as Log from './logging.js';
12
12
  import Base64 from '../base64.js';
13
13
 
14
+ // Async clipboard detection
15
+
16
+ /* Evaluates if there is browser support for the async clipboard API and
17
+ * relevant clipboard permissions. Returns 'unsupported' if permission states
18
+ * cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
19
+ * permission states for both read and write indicates full API support with no
20
+ * imposed native browser paste prompt. Conversely, detecting 'denied' indicates
21
+ * the user elected to disable clipboard.
22
+ */
23
+ export async function browserAsyncClipboardSupport() {
24
+ if (!(navigator?.permissions?.query &&
25
+ navigator?.clipboard?.writeText &&
26
+ navigator?.clipboard?.readText)) {
27
+ return 'unsupported';
28
+ }
29
+ try {
30
+ const writePerm = await navigator.permissions.query(
31
+ {name: "clipboard-write", allowWithoutGesture: true});
32
+ const readPerm = await navigator.permissions.query(
33
+ {name: "clipboard-read", allowWithoutGesture: false});
34
+ if (writePerm.state === "denied" || readPerm.state === "denied") {
35
+ return 'denied';
36
+ }
37
+ if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
38
+ (readPerm.state === "granted" || readPerm.state === "prompt")) {
39
+ return 'available';
40
+ }
41
+ } catch {
42
+ return 'unsupported';
43
+ }
44
+ return 'unsupported';
45
+ }
46
+
14
47
  // Touch detection
15
48
  export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
16
49
  // required for Chrome debugger
package/docs/API.md CHANGED
@@ -77,11 +77,6 @@ protocol stream.
77
77
  if the remote session is smaller than its container, or handled
78
78
  according to `clipViewport` if it is larger. Disabled by default.
79
79
 
80
- `showDotCursor`
81
- - Is a `boolean` indicating whether a dot cursor should be shown
82
- instead of a zero-sized or fully-transparent cursor if the server
83
- sets such invisible cursor. Disabled by default.
84
-
85
80
  `viewOnly`
86
81
  - Is a `boolean` indicating if any events (e.g. key presses or mouse
87
82
  movement) should be prevented from being sent to the server.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novnc/novnc",
3
- "version": "1.6.0-g71d0bfa",
3
+ "version": "1.6.0-g8e1ebdf",
4
4
  "description": "An HTML5 VNC client",
5
5
  "type": "module",
6
6
  "files": [
@@ -39,6 +39,7 @@
39
39
  "chai": "latest",
40
40
  "commander": "latest",
41
41
  "eslint": "latest",
42
+ "@eslint/js": "latest",
42
43
  "fs-extra": "latest",
43
44
  "globals": "latest",
44
45
  "jsdom": "latest",