Updated to noVNC 1.3.0, #2603

This commit is contained in:
Ylian Saint-Hilaire
2021-11-29 12:08:15 -08:00
parent a2ce9b8573
commit 112ebde4f2
75 changed files with 3712 additions and 60022 deletions

View File

@@ -1,9 +1,11 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
* Browser feature support detection
*/
import * as Log from './logging.js';
@@ -31,7 +33,7 @@ try {
const target = document.createElement('canvas');
target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
if (target.style.cursor) {
if (target.style.cursor.indexOf("url") === 0) {
Log.Info("Data URI scheme cursor supported");
_supportsCursorURIs = true;
} else {
@@ -43,14 +45,37 @@ try {
export const supportsCursorURIs = _supportsCursorURIs;
let _supportsImageMetadata = false;
let _hasScrollbarGutter = true;
try {
new ImageData(new Uint8ClampedArray(4), 1, 1);
_supportsImageMetadata = true;
} catch (ex) {
// ignore failure
// Create invisible container
const container = document.createElement('div');
container.style.visibility = 'hidden';
container.style.overflow = 'scroll'; // forcing scrollbars
document.body.appendChild(container);
// Create a div and place it in the container
const child = document.createElement('div');
container.appendChild(child);
// Calculate the difference between the container's full width
// and the child's width - the difference is the scrollbars
const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
// Clean up
container.parentNode.removeChild(container);
_hasScrollbarGutter = scrollbarWidth != 0;
} catch (exc) {
Log.Error("Scrollbar test exception: " + exc);
}
export const supportsImageMetadata = _supportsImageMetadata;
export const hasScrollbarGutter = _hasScrollbarGutter;
/*
* The functions for detection of platforms and browsers below are exported
* but the use of these should be minimized as much as possible.
*
* It's better to use feature detection than platform detection.
*/
export function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform);
@@ -67,23 +92,11 @@ export function isIOS() {
!!(/ipod/i).exec(navigator.platform));
}
export function isAndroid() {
return navigator && !!(/android/i).exec(navigator.userAgent);
}
export function isSafari() {
return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1);
}
export function isIE() {
return navigator && !!(/trident/i).exec(navigator.userAgent);
}
export function isEdge() {
return navigator && !!(/edge/i).exec(navigator.userAgent);
}
export function isFirefox() {
return navigator && !!(/firefox/i).exec(navigator.userAgent);
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
@@ -20,7 +20,6 @@ export default class Cursor {
this._canvas.style.pointerEvents = 'none';
// Can't use "display" because of Firefox bug #1445997
this._canvas.style.visibility = 'hidden';
document.body.appendChild(this._canvas);
}
this._position = { x: 0, y: 0 };
@@ -31,9 +30,6 @@ export default class Cursor {
'mouseleave': this._handleMouseLeave.bind(this),
'mousemove': this._handleMouseMove.bind(this),
'mouseup': this._handleMouseUp.bind(this),
'touchstart': this._handleTouchStart.bind(this),
'touchmove': this._handleTouchMove.bind(this),
'touchend': this._handleTouchEnd.bind(this),
};
}
@@ -45,25 +41,23 @@ export default class Cursor {
this._target = target;
if (useFallback) {
// FIXME: These don't fire properly except for mouse
/// movement in IE. We want to also capture element
// movement, size changes, visibility, etc.
document.body.appendChild(this._canvas);
const options = { capture: true, passive: true };
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
// There is no "touchleave" so we monitor touchstart globally
window.addEventListener('touchstart', this._eventHandlers.touchstart, options);
this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options);
this._target.addEventListener('touchend', this._eventHandlers.touchend, options);
}
this.clear();
}
detach() {
if (!this._target) {
return;
}
if (useFallback) {
const options = { capture: true, passive: true };
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
@@ -71,9 +65,7 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
window.removeEventListener('touchstart', this._eventHandlers.touchstart, options);
this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options);
this._target.removeEventListener('touchend', this._eventHandlers.touchend, options);
document.body.removeChild(this._canvas);
}
this._target = null;
@@ -95,14 +87,7 @@ export default class Cursor {
this._canvas.width = w;
this._canvas.height = h;
let img;
try {
// IE doesn't support this
img = new ImageData(new Uint8ClampedArray(rgba), w, h);
} catch (ex) {
img = ctx.createImageData(w, h);
img.data.set(new Uint8ClampedArray(rgba));
}
let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
ctx.clearRect(0, 0, w, h);
ctx.putImageData(img, 0, 0);
@@ -124,6 +109,27 @@ export default class Cursor {
this._hotSpot.y = 0;
}
// Mouse events might be emulated, this allows
// moving the cursor in such cases
move(clientX, clientY) {
if (!useFallback) {
return;
}
// clientX/clientY are relative the _visual viewport_,
// but our position is relative the _layout viewport_,
// so try to compensate when we can
if (window.visualViewport) {
this._position.x = clientX + window.visualViewport.offsetLeft;
this._position.y = clientY + window.visualViewport.offsetTop;
} else {
this._position.x = clientX;
this._position.y = clientY;
}
this._updatePosition();
let target = document.elementFromPoint(clientX, clientY);
this._updateVisibility(target);
}
_handleMouseOver(event) {
// This event could be because we're entering the target, or
// moving around amongst its sub elements. Let the move handler
@@ -132,7 +138,8 @@ export default class Cursor {
}
_handleMouseLeave(event) {
this._hideCursor();
// Check if we should show the cursor on the element we are leaving to
this._updateVisibility(event.relatedTarget);
}
_handleMouseMove(event) {
@@ -150,27 +157,29 @@ export default class Cursor {
// now and adjust visibility based on that.
let target = document.elementFromPoint(event.clientX, event.clientY);
this._updateVisibility(target);
}
_handleTouchStart(event) {
// Just as for mouseover, we let the move handler deal with it
this._handleTouchMove(event);
}
_handleTouchMove(event) {
this._updateVisibility(event.target);
this._position.x = event.changedTouches[0].clientX - this._hotSpot.x;
this._position.y = event.changedTouches[0].clientY - this._hotSpot.y;
this._updatePosition();
}
_handleTouchEnd(event) {
// Same principle as for mouseup
let target = document.elementFromPoint(event.changedTouches[0].clientX,
event.changedTouches[0].clientY);
this._updateVisibility(target);
// Captures end with a mouseup but we can't know the event order of
// mouseup vs releaseCapture.
//
// In the cases when releaseCapture comes first, the code above is
// enough.
//
// In the cases when the mouseup comes first, we need wait for the
// browser to flush all events and then check again if the cursor
// should be visible.
if (this._captureIsActive()) {
window.setTimeout(() => {
// We might have detached at this point
if (!this._target) {
return;
}
// Refresh the target from elementFromPoint since queued events
// might have altered the DOM
target = document.elementFromPoint(event.clientX,
event.clientY);
this._updateVisibility(target);
}, 0);
}
}
_showCursor() {
@@ -189,6 +198,9 @@ export default class Cursor {
// (i.e. are we over the target, or a child of the target without a
// different cursor set)
_shouldShowCursor(target) {
if (!target) {
return false;
}
// Easy case
if (target === this._target) {
return true;
@@ -207,6 +219,11 @@ export default class Cursor {
}
_updateVisibility(target) {
// When the cursor target has capture we want to show the cursor.
// So, if a capture is active - look at the captured element instead.
if (this._captureIsActive()) {
target = document.captureElement;
}
if (this._shouldShowCursor(target)) {
this._showCursor();
} else {
@@ -218,4 +235,9 @@ export default class Cursor {
this._canvas.style.left = this._position.x + "px";
this._canvas.style.top = this._position.y + "px";
}
_captureIsActive() {
return document.captureElement &&
document.documentElement.contains(document.captureElement);
}
}

View File

@@ -0,0 +1,32 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* HTML element utility functions
*/
export function clientToElement(x, y, elem) {
const bounds = elem.getBoundingClientRect();
let pos = { x: 0, y: 0 };
// Clip to target bounds
if (x < bounds.left) {
pos.x = 0;
} else if (x >= bounds.right) {
pos.x = bounds.width - 1;
} else {
pos.x = x - bounds.left;
}
if (y < bounds.top) {
pos.y = 0;
} else if (y >= bounds.bottom) {
pos.y = bounds.height - 1;
} else {
pos.y = y - bounds.top;
}
return pos;
}

View File

@@ -21,7 +21,8 @@ export function stopEvent(e) {
// Emulate Element.setCapture() when not supported
let _captureRecursion = false;
let _captureElem = null;
let _elementForUnflushedEvents = null;
document.captureElement = null;
function _captureProxy(e) {
// Recursion protection as we'll see our own event
if (_captureRecursion) return;
@@ -30,7 +31,11 @@ function _captureProxy(e) {
const newEv = new e.constructor(e.type, e);
_captureRecursion = true;
_captureElem.dispatchEvent(newEv);
if (document.captureElement) {
document.captureElement.dispatchEvent(newEv);
} else {
_elementForUnflushedEvents.dispatchEvent(newEv);
}
_captureRecursion = false;
// Avoid double events
@@ -48,58 +53,52 @@ function _captureProxy(e) {
}
// Follow cursor style of target element
function _captureElemChanged() {
const captureElem = document.getElementById("noVNC_mouse_capture_elem");
captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor;
function _capturedElemChanged() {
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
}
const _captureObserver = new MutationObserver(_captureElemChanged);
const _captureObserver = new MutationObserver(_capturedElemChanged);
let _captureIndex = 0;
export function setCapture(elem) {
if (elem.setCapture) {
elem.setCapture();
// IE releases capture on 'click' events which might not trigger
elem.addEventListener('mouseup', releaseCapture);
export function setCapture(target) {
if (target.setCapture) {
target.setCapture();
document.captureElement = target;
} else {
// Release any existing capture in case this method is
// called multiple times without coordination
releaseCapture();
let captureElem = document.getElementById("noVNC_mouse_capture_elem");
let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
if (captureElem === null) {
captureElem = document.createElement("div");
captureElem.id = "noVNC_mouse_capture_elem";
captureElem.style.position = "fixed";
captureElem.style.top = "0px";
captureElem.style.left = "0px";
captureElem.style.width = "100%";
captureElem.style.height = "100%";
captureElem.style.zIndex = 10000;
captureElem.style.display = "none";
document.body.appendChild(captureElem);
if (proxyElem === null) {
proxyElem = document.createElement("div");
proxyElem.id = "noVNC_mouse_capture_elem";
proxyElem.style.position = "fixed";
proxyElem.style.top = "0px";
proxyElem.style.left = "0px";
proxyElem.style.width = "100%";
proxyElem.style.height = "100%";
proxyElem.style.zIndex = 10000;
proxyElem.style.display = "none";
document.body.appendChild(proxyElem);
// This is to make sure callers don't get confused by having
// our blocking element as the target
captureElem.addEventListener('contextmenu', _captureProxy);
proxyElem.addEventListener('contextmenu', _captureProxy);
captureElem.addEventListener('mousemove', _captureProxy);
captureElem.addEventListener('mouseup', _captureProxy);
proxyElem.addEventListener('mousemove', _captureProxy);
proxyElem.addEventListener('mouseup', _captureProxy);
}
_captureElem = elem;
_captureIndex++;
document.captureElement = target;
// Track cursor and get initial cursor
_captureObserver.observe(elem, {attributes: true});
_captureElemChanged();
_captureObserver.observe(target, {attributes: true});
_capturedElemChanged();
captureElem.style.display = "";
proxyElem.style.display = "";
// We listen to events on window in order to keep tracking if it
// happens to leave the viewport
@@ -112,26 +111,26 @@ export function releaseCapture() {
if (document.releaseCapture) {
document.releaseCapture();
document.captureElement = null;
} else {
if (!_captureElem) {
if (!document.captureElement) {
return;
}
// There might be events already queued, so we need to wait for
// them to flush. E.g. contextmenu in Microsoft Edge
window.setTimeout((expected) => {
// Only clear it if it's the expected grab (i.e. no one
// else has initiated a new grab)
if (_captureIndex === expected) {
_captureElem = null;
}
}, 0, _captureIndex);
// There might be events already queued. The event proxy needs
// access to the captured element for these queued events.
// E.g. contextmenu (right-click) in Microsoft Edge
//
// Before removing the capturedElem pointer we save it to a
// temporary variable that the unflushed events can use.
_elementForUnflushedEvents = document.captureElement;
document.captureElement = null;
_captureObserver.disconnect();
const captureElem = document.getElementById("noVNC_mouse_capture_elem");
captureElem.style.display = "none";
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
proxyElem.style.display = "none";
window.removeEventListener('mousemove', _captureProxy);
window.removeEventListener('mouseup', _captureProxy);

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -0,0 +1,15 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
export function toUnsigned32bit(toConvert) {
return toConvert >>> 0;
}
export function toSigned32bit(toConvert) {
return toConvert | 0;
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -10,18 +10,18 @@
* Logging/debug routines
*/
let _log_level = 'warn';
let _logLevel = 'warn';
let Debug = () => {};
let Info = () => {};
let Warn = () => {};
let Error = () => {};
export function init_logging(level) {
export function initLogging(level) {
if (typeof level === 'undefined') {
level = _log_level;
level = _logLevel;
} else {
_log_level = level;
_logLevel = level;
}
Debug = Info = Warn = Error = () => {};
@@ -46,11 +46,11 @@ export function init_logging(level) {
}
}
export function get_logging() {
return _log_level;
export function getLogging() {
return _logLevel;
}
export { Debug, Info, Warn, Error };
// Initialize logging level
init_logging();
initLogging();

View File

@@ -1,54 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
/* Polyfills to provide new APIs in old browsers */
/* Object.assign() (taken from MDN) */
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
for (let index = 1; index < arguments.length; index++) {
const nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (let nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
/* CustomEvent constructor (taken from MDN) */
(() => {
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
const evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
if (typeof window.CustomEvent !== "function") {
window.CustomEvent = CustomEvent;
}
})();

View File

@@ -1,14 +1,28 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Decode from UTF-8
*/
export function decodeUTF8(utf8string) {
return decodeURIComponent(escape(utf8string));
// Decode from UTF-8
export function decodeUTF8(utf8string, allowLatin1=false) {
try {
return decodeURIComponent(escape(utf8string));
} catch (e) {
if (e instanceof URIError) {
if (allowLatin1) {
// If we allow Latin1 we can ignore any decoding fails
// and in these cases return the original string
return utf8string;
}
}
throw e;
}
}
// Encode to UTF-8
export function encodeUTF8(DOMString) {
return unescape(encodeURIComponent(DOMString));
}