This commit is contained in:
matt 2022-07-29 15:53:29 +00:00
commit e5a62ea66b
17 changed files with 1649 additions and 800 deletions

View File

@ -26,6 +26,12 @@
return false;
}
// Skip allowed errors
let allowedErrors = [ "The user has exited the lock before this request was completed." ];
if (event.message && allowedErrors.includes(event.message)) {
return false;
}
let div = document.createElement("div");
div.classList.add('noVNC_message');
div.appendChild(document.createTextNode(event.message));

BIN
app/images/gamepad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

78
app/images/pointer.svg Normal file
View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="25"
height="25"
viewBox="0 0 25 25"
id="svg2"
version="1.1"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="pointer.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#717171"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="6.9841519"
inkscape:cy="18.584699"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="false"
inkscape:window-width="2560"
inkscape:window-height="1403"
inkscape:window-x="2560"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-smooth-nodes="true"
inkscape:document-rotation="0">
<inkscape:grid
type="xygrid"
id="grid4136" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1027.3622)">
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 6.3910823,1030.3965 v 17.497 l 3.5465954,-2.6671 1.5862113,4.1015 4.336661,-1.5752 -1.59331,-4.2624 4.341678,-0.3562 z"
id="path879" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
app/images/splash.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -22,7 +22,8 @@
body {
margin:0;
padding:0;
font-family: Helvetica;
font-family: "Poppins", "Helvetica";
letter-spacing: 0.05em;
background: white url('../images/icons/kasm_logo.png') no-repeat fixed center;
height:100%;
touch-action: none;
@ -37,7 +38,7 @@ html {
}
.noVNC_disabled {
color: rgb(128, 128, 128);
color: rgb(128, 128, 128) !important;
}
/* ----------------------------------------
@ -351,8 +352,9 @@ select:active {
/* Edge misrenders animations wihthout this */
transform: translateX(0);
}
:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
opacity: 0.8;
/* opacity: 0.8; */
}
#noVNC_control_bar_anchor.noVNC_right {
left: auto;
@ -365,12 +367,12 @@ select:active {
transition: 0.5s ease-in-out;
background-color: rgb(80, 89, 101);
background-color: rgb(9 2 2 / 0.6);
border-radius: 0 10px 10px 0;
border-style: inset;
border-color: rgb(255 255 255 / 0.6);
}
#noVNC_control_bar.noVNC_open {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
left: 0;
}
#noVNC_control_bar::before {
@ -400,28 +402,31 @@ select:active {
#noVNC_control_bar_handle {
position: absolute;
left: -15px;
top: 0;
transform: translateY(35px);
width: calc(100% + 30px);
height: 50px;
z-index: -1;
cursor: pointer;
border-radius: 5px;
background-color: rgb(83, 99, 122);
background-image: url("../images/handle_bg.svg");
background-repeat: no-repeat;
background-position: right;
box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
}
#noVNC_control_bar_handle:after {
content: "";
transition: transform 0.5s ease-in-out;
background: url("../images/handle.svg");
background-repeat: no-repeat;
background-position: center;
position: absolute;
top: 22px; /* (50px-6px)/2 */
right: 5px;
width: 5px;
height: 6px;
right: 0px;
width: 15px;
height: 60px;
background-color: rgb(9 2 2 / 0.6);
border-bottom-right-radius: 10px;
border-top-right-radius: 10px;
border-color: rgb(255 255 255 / 0.6);
border-style: inset;
}
#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateX(1px) rotate(180deg);
@ -431,6 +436,7 @@ select:active {
}
.noVNC_right #noVNC_control_bar_handle {
background-position: left;
}
.noVNC_right #noVNC_control_bar_handle:after {
left: 5px;
@ -495,18 +501,23 @@ select:active {
transform: translateY(-50%) scale(1);
}
.noVNC_button_div {
display: block;
color: #fff;
font-size: 13px;
}
/* General button style */
.noVNC_button {
display: block;
display: inline;
padding: 4px 4px;
margin: 10px 0;
vertical-align: middle;
border:1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
}
.noVNC_button.noVNC_selected {
border-color: rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.5);
background: rgba(153, 151, 157, 0.68);
}
.noVNC_button:disabled {
opacity: 0.4;
@ -523,10 +534,10 @@ select:active {
:root:not(.noVNC_touch) .noVNC_button.noVNC_selected:hover,
.noVNC_button.noVNC_selected:focus {
border-color: rgba(0, 0, 0, 0.4);
background: rgba(0, 0, 0, 0.2);
background: rgba(153, 151, 157, 0.68);
}
:root:not(.noVNC_touch) .noVNC_button:hover,
.noVNC_button:focus {
:root:not(.noVNC_touch) .noVNC_button_div:hover,
.noVNC_button_div:focus {
background: rgba(255, 255, 255, 0.2);
}
.noVNC_button.noVNC_hidden {
@ -539,6 +550,7 @@ select:active {
transition: 0.5s ease-in-out;
width: 300px;
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
@ -548,11 +560,9 @@ select:active {
padding: 15px;
background: #fff;
background: rgb(9 9 0 / 0.77);
border-radius: 10px;
color: #000;
border: 2px solid #E0E0E0;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
.noVNC_panel.noVNC_open {
visibility: visible;
@ -578,10 +588,11 @@ select:active {
.noVNC_panel label {
display: block;
white-space: nowrap;
color:white;
}
.noVNC_panel .noVNC_heading {
background-color: rgb(110, 132, 163);
background-color: rgb(54,58,64);
border-radius: 5px;
padding: 5px;
/* Compensate for padding in image */
@ -602,12 +613,16 @@ select:active {
/* Expanders */
.noVNC_expander {
cursor: pointer;
color:white;
}
.noVNC_expander::before {
content: url("../images/expander.svg");
display: inline-block;
margin-right: 5px;
transition: 0.2s ease-in-out;
-webkit-filter: invert(.75); /* safari 6.0 - 9.0 */
filter: invert(.75);
}
.noVNC_expander.noVNC_open::before {
transform: rotateZ(90deg);
@ -630,6 +645,14 @@ select:active {
text-align: center;
}
:root:not(.noVNC_disconnected) .noVNC_hide_on_connect {
display: none
}
:root:not(.noVNC_connected) .noVNC_hide_on_disconnect {
display: none;
}
:root:not(.noVNC_connected) #noVNC_view_drag_button {
display: none;
}
@ -687,6 +710,7 @@ select:active {
list-style: none;
margin: 0px;
padding: 0px;
color:white;
}
#noVNC_setting_port {
width: 80px;
@ -776,81 +800,6 @@ select:active {
content: url("../images/warning.svg") " ";
}
/* ----------------------------------------
* Connect Dialog
* ----------------------------------------
*/
#noVNC_connect_dlg {
transition: 0.5s ease-in-out;
transform: scale(0, 0);
visibility: hidden;
opacity: 0;
}
#noVNC_connect_dlg.noVNC_open {
transform: scale(1, 1);
visibility: visible;
opacity: 1;
}
#noVNC_connect_dlg .noVNC_logo {
transition: 0.5s ease-in-out;
padding: 10px;
margin-bottom: 10px;
font-size: 80px;
text-align: center;
border-radius: 5px;
}
@media (max-width: 440px) {
#noVNC_connect_dlg {
max-width: calc(100vw - 100px);
}
#noVNC_connect_dlg .noVNC_logo {
font-size: calc(25vw - 30px);
}
}
#noVNC_connect_button {
cursor: pointer;
/*
padding: 10px;
color: white;
background-color: rgb(110, 132, 163);
border-radius: 12px;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
*/
text-align: center;
font-size: 20px;
margin-top: 130px;
}
#noVNC_connect_button div {
margin: 2px;
padding: 5px 30px;
border: 1px solid rgb(83, 99, 122);
border-bottom-width: 2px;
border-radius: 5px;
background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147));
/* This avoids it jumping around when :active */
vertical-align: middle;
color: white;
}
#noVNC_connect_button div:active {
border-bottom-width: 1px;
margin-top: 3px;
}
:root:not(.noVNC_touch) #noVNC_connect_button div:hover {
background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155));
}
#noVNC_connect_button img {
vertical-align: bottom;
height: 1.3em;
}
/* ----------------------------------------
* Password Dialog
@ -930,21 +879,19 @@ select:active {
#noVNC_container {
width: 100%;
height: 100%;
background-color: rgb(74, 144, 217, 0.5);
border-bottom-right-radius: 800px 600px;
/*border-top-left-radius: 800px 600px;*/
background-image: url('../images/splash.jpg')
}
#noVNC_keyboardinput {
width: 1px;
height: 1px;
background-color: #fff;
color: #fff;
width: 0px;
height: 0px;
background-color: #fff0;
color: rgba(5, 5, 5, 0);
border: 0;
position: absolute;
left: -40px;
left: 35%;
top: 40%;
z-index: -1;
ime-mode: disabled;
}
/*Default noVNC logo.*/
@ -962,6 +909,10 @@ select:active {
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
line-height:90%;
text-shadow: 0.1em 0.1em 0 black;
margin-bottom: 0px;
}
.noVNC_logo img {
width: 45%
}
.noVNC_logo span{
color:green;
@ -1021,6 +972,11 @@ body {
user-select: none;
}
#noVNC_keyboard_control .noVNC_selected {
background-color:rgb(15, 36, 153);
border: 6px rgb(15, 36, 153) solid;
}
.keyboard-controls .button.ctrl {
background-image: url("../images/ctrl.svg");
background-size: contain;
@ -1134,3 +1090,75 @@ body {
font-size: 90px;
}
}
/* ----------------------------------------
* Slider Check boxes
* ----------------------------------------
*/
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 16px;
margin: 5px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 4px;
bottom: 1px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(10px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.slider-label {
padding-left: 26px;
}

534
app/ui.js
View File

@ -34,7 +34,7 @@ import "core-js/stable";
import "regenerator-runtime/runtime";
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS }
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS, supportsPointerLock }
from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
@ -47,6 +47,7 @@ const PAGE_TITLE = "KasmVNC";
var delta = 500;
var lastKeypressTime = 0;
var lastKeypressCode = -1;
var currentEventCount = -1;
var idleCounter = 0;
@ -65,8 +66,6 @@ const UI = {
controlbarMouseDownClientY: 0,
controlbarMouseDownOffsetY: 0,
lastKeyboardinput: null,
defaultKeyboardinputLen: 100,
needToCheckClipboardChange: false,
inhibitReconnect: true,
@ -129,16 +128,13 @@ const UI = {
UI.addControlbarHandlers();
UI.addTouchSpecificHandlers();
UI.addExtraKeysHandlers();
UI.addGamingHandlers();
UI.addMachineHandlers();
UI.addConnectionControlHandlers();
UI.addClipboardHandlers();
UI.addSettingsHandlers();
document.getElementById("noVNC_status")
.addEventListener('click', UI.hideStatus);
// Bootstrap fallback input handler
UI.keyboardinputReset();
UI.openControlbar();
UI.updateVisualState('init');
@ -151,8 +147,6 @@ const UI = {
UI.connect();
} else {
autoconnect = false;
// Show the connect panel on first load unless autoconnecting
UI.openConnectPanel();
}
window.parent.postMessage({
@ -172,6 +166,12 @@ const UI = {
}
});
window.addEventListener("beforeunload", (e) => {
if (UI.rfb) {
UI.disconnect();
}
});
return Promise.resolve(UI.rfb);
},
@ -183,10 +183,9 @@ const UI = {
document.documentElement.mozRequestFullScreen ||
document.documentElement.webkitRequestFullscreen ||
document.body.msRequestFullscreen)) {
document.getElementById('noVNC_fullscreen_button')
.classList.remove("noVNC_hidden");
UI.addFullscreenHandlers();
}
UI.showControlInput("noVNC_fullscreen_button")
UI.addFullscreenHandlers();
}
},
initSettings() {
@ -245,6 +244,9 @@ const UI = {
UI.initSetting('prefer_local_cursor', true);
UI.initSetting('toggle_control_panel', false);
UI.initSetting('enable_perf_stats', false);
UI.initSetting('virtual_keyboard_visible', false);
UI.initSetting('enable_ime', false)
UI.toggleKeyboardControls();
if (WebUtil.isInsideKasmVDI()) {
UI.initSetting('clipboard_up', false);
@ -350,8 +352,7 @@ const UI = {
document.getElementById("noVNC_control_bar")
.addEventListener('keydown', UI.keepControlbar);
document.getElementById("noVNC_view_drag_button")
.addEventListener('click', UI.toggleViewDrag);
UI.addClickHandle('noVNC_view_drag_button', UI.toggleViewDrag);
document.getElementById("noVNC_control_bar_handle")
.addEventListener('mousedown', UI.controlbarHandleMouseDown);
@ -371,12 +372,6 @@ const UI = {
addTouchSpecificHandlers() {
document.getElementById("noVNC_keyboard_button")
.addEventListener('click', UI.toggleVirtualKeyboard);
UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
UI.touchKeyboard.onkeyevent = UI.keyEvent;
UI.touchKeyboard.grab();
document.getElementById("noVNC_keyboardinput")
.addEventListener('input', UI.keyInput);
document.getElementById("noVNC_keyboardinput")
.addEventListener('focus', UI.onfocusVirtualKeyboard);
document.getElementById("noVNC_keyboardinput")
@ -410,8 +405,8 @@ const UI = {
},
addExtraKeysHandlers() {
document.getElementById("noVNC_toggle_extra_keys_button")
.addEventListener('click', UI.toggleExtraKeys);
UI.addClickHandle('noVNC_toggle_extra_keys_button', UI.toggleExtraKeys);
document.getElementById("noVNC_toggle_ctrl_button")
.addEventListener('click', UI.toggleCtrl);
document.getElementById("noVNC_toggle_windows_button")
@ -426,20 +421,27 @@ const UI = {
.addEventListener('click', UI.sendCtrlAltDel);
},
addGamingHandlers() {
UI.addClickHandle('noVNC_game_mode_button', UI.toggleRelativePointer);
document
.getElementById("noVNC_setting_pointer_lock")
.addEventListener("click", UI.togglePointerLock);
},
addMachineHandlers() {
UI.addClickHandle('noVNC_power_button', UI.togglePowerPanel);
document.getElementById("noVNC_shutdown_button")
.addEventListener('click', () => UI.rfb.machineShutdown());
document.getElementById("noVNC_reboot_button")
.addEventListener('click', () => UI.rfb.machineReboot());
document.getElementById("noVNC_reset_button")
.addEventListener('click', () => UI.rfb.machineReset());
document.getElementById("noVNC_power_button")
.addEventListener('click', UI.togglePowerPanel);
},
addConnectionControlHandlers() {
document.getElementById("noVNC_disconnect_button")
.addEventListener('click', UI.disconnect);
UI.addClickHandle('noVNC_disconnect_button', UI.disconnect);
var connect_btn_el = document.getElementById("noVNC_connect_button");
if (typeof(connect_btn_el) != 'undefined' && connect_btn_el != null)
{
@ -453,8 +455,8 @@ const UI = {
},
addClipboardHandlers() {
document.getElementById("noVNC_clipboard_button")
.addEventListener('click', UI.toggleClipboardPanel);
UI.addClickHandle('noVNC_clipboard_button', UI.toggleClipboardPanel);
document.getElementById("noVNC_clipboard_text")
.addEventListener('change', UI.clipboardSend);
document.getElementById("noVNC_clipboard_clear_button")
@ -474,8 +476,7 @@ const UI = {
},
addSettingsHandlers() {
document.getElementById("noVNC_settings_button")
.addEventListener('click', UI.toggleSettingsPanel);
UI.addClickHandle('noVNC_settings_button', UI.toggleSettingsPanel);
document.getElementById("noVNC_setting_enable_perf_stats").addEventListener('click', UI.showStats);
@ -536,11 +537,15 @@ const UI = {
UI.addSettingChangeHandler('clipboard_seamless');
UI.addSettingChangeHandler('clipboard_up');
UI.addSettingChangeHandler('clipboard_down');
UI.addSettingChangeHandler('toggle_control_panel');
UI.addSettingChangeHandler('virtual_keyboard_visible');
UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls);
UI.addSettingChangeHandler('enable_ime');
UI.addSettingChangeHandler('enable_ime', UI.toggleIMEMode);
},
addFullscreenHandlers() {
document.getElementById("noVNC_fullscreen_button")
.addEventListener('click', UI.toggleFullscreen);
UI.addClickHandle('noVNC_fullscreen_button', UI.toggleFullscreen);
window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
@ -553,6 +558,20 @@ const UI = {
* ==============
* VISUAL
* ------v------*/
// Ignore clicks that are propogated from child elements in sub panels
isControlPanelItemClick(e) {
if (!(e && e.target && e.target.classList && e.target.parentNode &&
(
e.target.classList.contains('noVNC_button') && e.target.parentNode.id !== 'noVNC_modifiers' ||
e.target.classList.contains('noVNC_button_div') ||
e.target.classList.contains('noVNC_heading')
)
)) {
return false;
}
return true;
},
// Disable/enable controls depending on connection state
updateVisualState(state) {
@ -561,6 +580,7 @@ const UI = {
document.documentElement.classList.remove("noVNC_connected");
document.documentElement.classList.remove("noVNC_disconnecting");
document.documentElement.classList.remove("noVNC_reconnecting");
document.documentElement.classList.remove("noVNC_disconnected");
const transitionElem = document.getElementById("noVNC_transition_text");
if (WebUtil.isInsideKasmVDI())
@ -583,6 +603,7 @@ const UI = {
document.documentElement.classList.add("noVNC_disconnecting");
break;
case 'disconnected':
document.documentElement.classList.add("noVNC_disconnected");
break;
case 'reconnecting':
transitionElem.textContent = _("Reconnecting...");
@ -616,6 +637,7 @@ const UI = {
UI.updatePowerButton();
UI.keepControlbar();
}
//UI.updatePointerLockButton();
// State change closes dialogs as they may not be relevant
// anymore
@ -642,7 +664,12 @@ const UI = {
},
showStatus(text, statusType, time) {
showStatus(text, statusType, time, kasm = false) {
// If inside the full Kasm CDI framework, don't show messages unless explicitly told to
if (WebUtil.isInsideKasmVDI() && !kasm) {
return;
}
const statusElem = document.getElementById('noVNC_status');
if (typeof statusType === 'undefined') {
@ -737,7 +764,9 @@ const UI = {
UI.closeAllPanels();
document.getElementById('noVNC_control_bar')
.classList.remove("noVNC_open");
UI.rfb.focus();
if (UI.rfb) {
UI.rfb.focus();
}
},
toggleControlbar() {
@ -910,6 +939,48 @@ const UI = {
}
},
addClickHandle(domElementName, funcToCall) {
/* Add click handler, will attach to parent if appropriate */
var control = document.getElementById(domElementName);
if (control.parentNode.classList.contains('noVNC_button_div')) {
control.parentNode.addEventListener('click', funcToCall);
} else {
control.addEventListener('click', funcToCall);
}
},
showControlInput(name) {
var control = document.getElementById(name);
/*var control_label = document.getElementById(name + '_label');
if (control) {
control.classList.remove("noVNC_hidden");
}
if (control_label) {
control_label.classList.remove("noVNC_hidden");
} */
if (control.parentNode.classList.contains('noVNC_button_div')) {
control.parentNode.classList.remove("noVNC_hidden")
} else {
control.classList.remove("noVNC_hidden")
}
},
hideControlInput(name) {
var control = document.getElementById(name);
/*var control_label = document.getElementById(name + '_label');
if (control) {
control.classList.add("noVNC_hidden");
}
if (control_label) {
control_label.classList.add("noVNC_hidden");
}*/
if (control.parentNode.classList.contains('noVNC_button_div')) {
control.parentNode.classList.add("noVNC_hidden")
} else {
control.classList.add("noVNC_hidden")
}
},
/* ------^-------
* /VISUAL
* ==============
@ -1076,7 +1147,11 @@ const UI = {
.classList.remove("noVNC_selected");
},
toggleSettingsPanel() {
toggleSettingsPanel(e) {
if (!UI.isControlPanelItemClick(e)) {
return false;
}
if (document.getElementById('noVNC_settings')
.classList.contains("noVNC_open")) {
UI.closeSettingsPanel();
@ -1108,7 +1183,11 @@ const UI = {
.classList.remove("noVNC_selected");
},
togglePowerPanel() {
togglePowerPanel(e) {
if (!UI.isControlPanelItemClick(e)) {
return false;
}
if (document.getElementById('noVNC_power')
.classList.contains("noVNC_open")) {
UI.closePowerPanel();
@ -1122,11 +1201,9 @@ const UI = {
if (UI.connected &&
UI.rfb.capabilities.power &&
!UI.rfb.viewOnly) {
document.getElementById('noVNC_power_button')
.classList.remove("noVNC_hidden");
UI.showControlInput('noVNC_power_button')
} else {
document.getElementById('noVNC_power_button')
.classList.add("noVNC_hidden");
UI.hideControlInput('noVNC_power_button');
// Close power panel if open
UI.closePowerPanel();
}
@ -1155,7 +1232,11 @@ const UI = {
.classList.remove("noVNC_selected");
},
toggleClipboardPanel() {
toggleClipboardPanel(e) {
if (!UI.isControlPanelItemClick(e)) {
return false;
}
if (document.getElementById('noVNC_clipboard')
.classList.contains("noVNC_open")) {
UI.closeClipboardPanel();
@ -1245,16 +1326,6 @@ const UI = {
* CONNECTION
* ------v------*/
openConnectPanel() {
document.getElementById('noVNC_connect_dlg')
.classList.add("noVNC_open");
},
closeConnectPanel() {
document.getElementById('noVNC_connect_dlg')
.classList.remove("noVNC_open");
},
connect(event, password) {
// Ignore when rfb already exists
@ -1283,8 +1354,6 @@ const UI = {
return;
}
UI.closeConnectPanel();
UI.updateVisualState('connecting');
let url;
@ -1297,7 +1366,9 @@ const UI = {
}
url += '/' + path;
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
UI.rfb = new RFB(document.getElementById('noVNC_container'),
document.getElementById('noVNC_keyboardinput'),
url,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
@ -1310,6 +1381,8 @@ const UI = {
UI.rfb.addEventListener("bottleneck_stats", UI.bottleneckStatsRecieve);
UI.rfb.addEventListener("bell", UI.bell);
UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
UI.rfb.addEventListener("inputlock", UI.inputLockChanged);
UI.rfb.addEventListener("inputlockerror", UI.inputLockError);
UI.rfb.translateShortcuts = UI.getSetting('translate_shortcuts');
UI.rfb.clipViewport = UI.getSetting('view_clip');
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
@ -1330,11 +1403,13 @@ const UI = {
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.rfb.idleDisconnect = UI.getSetting('idle_disconnect');
UI.rfb.pointerRelative = UI.getSetting('pointer_relative');
UI.rfb.videoQuality = parseInt(UI.getSetting('video_quality'));
UI.rfb.antiAliasing = UI.getSetting('anti_aliasing');
UI.rfb.clipboardUp = UI.getSetting('clipboard_up');
UI.rfb.clipboardDown = UI.getSetting('clipboard_down');
UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless');
UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime');
UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless;
//Only explicitly request permission to clipboard on browsers that support binary clipboard access
@ -1368,7 +1443,6 @@ const UI = {
}
UI.rfb.addEventListener("disconnect", UI.disconnectedRx);
document.getElementById('noVNC_control_bar_anchor').setAttribute('style', 'display: none');
document.getElementById('noVNC_connect_dlg').innerHTML = '';
//keep alive for websocket connection to stay open, since we may not control reverse proxies
//send a keep alive within a window that we control
@ -1396,23 +1470,23 @@ const UI = {
document.getElementById('noVNC_status').style.visibility = "visible";
}
// Send an event to the parent document (kasm app) to toggle the control panel when ctl is double clicked
if (UI.getSetting('toggle_control_panel', false)) {
document.addEventListener('keyup', function (event) {
// CTRL and the various implementations of the mac command key
if ([17, 224, 91, 93].indexOf(event.keyCode) > -1) {
var thisKeypressTime = new Date();
if (thisKeypressTime - lastKeypressTime <= delta) {
UI.toggleNav();
thisKeypressTime = 0;
//key events for KasmVNC control
document.addEventListener('keyup', function (event) {
if (event.ctrlKey && event.shiftKey) {
switch(event.keyCode) {
case 49:
UI.toggleNav();
break;
case 50:
UI.toggleRelativePointer();
break;
case 51:
UI.togglePointerLock();
break;
}
}
lastKeypressTime = thisKeypressTime;
}
}, true);
}
}, true);
},
disconnect() {
@ -1449,7 +1523,6 @@ const UI = {
UI.updateVisualState('disconnected');
UI.openControlbar();
UI.openConnectPanel();
},
connectFinished(e) {
@ -1503,7 +1576,6 @@ const UI = {
document.title = PAGE_TITLE;
UI.openControlbar();
UI.openConnectPanel();
if (UI.forceReconnect) {
UI.forceReconnect = false;
@ -1525,18 +1597,19 @@ const UI = {
UI.showStatus(msg, 'error');
},
/*
Menu.js Additions
*/
receiveMessage(event) {
//TODO: UNCOMMENT FOR PRODUCTION
//if (event.origin !== "https://kasmweb.com")
// return;
//send message to parent window
sendMessage(name, value) {
if (WebUtil.isInsideKasmVDI()) {
parent.postMessage({ action: name, value: value }, '*' );
}
},
//receive message from parent window
receiveMessage(event) {
if (event.data && event.data.action) {
switch (event.data.action) {
case 'clipboardsnd':
if (UI.rfb.clipboardUp) {
if (UI.rfb && UI.rfb.clipboardUp) {
UI.rfb.clipboardPasteFrom(event.data.value);
}
break;
@ -1544,6 +1617,50 @@ const UI = {
UI.forceSetting('video_quality', parseInt(event.data.value), false);
UI.updateQuality();
break;
case 'enable_game_mode':
if (UI.rfb && !UI.rfb.pointerRelative) {
UI.toggleRelativePointer();
}
break;
case 'disable_game_mode':
if (UI.rfb && UI.rfb.pointerRelative) {
UI.toggleRelativePointer();
}
break;
case 'enable_pointer_lock':
if (UI.rfb && !UI.rfb.pointerLock) {
UI.togglePointerLock();
}
break;
case 'disable_pointer_lock':
if (UI.rfb && UI.rfb.pointerLock) {
UI.togglePointerLock();
}
break;
case 'show_keyboard_controls':
if (!UI.getSetting('virtual_keyboard_visible')) {
UI.forceSetting('virtual_keyboard_visible', true, false);
UI.showKeyboardControls();
}
break;
case 'hide_keyboard_controls':
if (UI.getSetting('virtual_keyboard_visible')) {
UI.forceSetting('virtual_keyboard_visible', true, false);
UI.hideKeyboardControls();
}
break;
case 'enable_ime_mode':
if (!UI.getSetting('enable_ime')) {
UI.forceSetting('enable_ime', true, false);
UI.toggleIMEMode();
}
break;
case 'disable_ime_mode':
if (UI.getSetting('enable_ime')) {
UI.forceSetting('enable_ime', false, false);
UI.toggleIMEMode();
}
break;
}
}
},
@ -1553,7 +1670,15 @@ const UI = {
},
toggleNav(){
parent.postMessage({ action: 'togglenav', value: null}, '*' );
if (WebUtil.isInsideKasmVDI()) {
parent.postMessage({ action: 'togglenav', value: null}, '*' );
} else {
UI.toggleControlbar();
UI.keepControlbar();
UI.activateControlbar();
UI.controlbarGrabbed = false;
UI.showControlbarHint(false);
}
},
clipboardRx(event) {
@ -1656,6 +1781,7 @@ const UI = {
document.getElementById('noVNC_fullscreen_button')
.classList.remove("noVNC_selected");
}
UI.updatePointerLockButton();
},
/* ------^-------
@ -1708,6 +1834,68 @@ const UI = {
UI.updateViewDrag();
},
/* ------^-------
* /VIEW CLIPPING
* ==============
* POINTER LOCK
* ------v------*/
updatePointerLockButton() {
// Only show the button if the pointer lock API is properly supported
// AND in fullscreen.
if (
UI.connected &&
(document.pointerLockElement !== undefined ||
document.mozPointerLockElement !== undefined)
) {
UI.showControlInput("noVNC_setting_pointer_lock");
UI.showControlInput("noVNC_game_mode_button");
} else {
UI.hideControlInput("noVNC_setting_pointer_lock");
UI.hideControlInput("noVNC_game_mode_button");
}
},
togglePointerLock() {
if (!supportsPointerLock()) {
UI.showStatus('Your browser does not support pointer lock.', 'info', 1500, true);
//force pointer lock in UI to false and disable control
UI.forceSetting('pointer_lock', false, true);
} else {
UI.rfb.pointerLock = !UI.rfb.pointerLock;
if (UI.getSetting('pointer_lock') !== UI.rfb.pointerLock) {
UI.forceSetting('pointer_lock', UI.rfb.pointerLock, false);
}
}
},
toggleRelativePointer(event=null, forcedToggleValue=null) {
if (!supportsPointerLock()) {
UI.showStatus('Your browser does not support pointer lock.', 'info', 1500, true);
return;
}
var togglePosition = !UI.rfb.pointerRelative;
if (UI.rfb.pointerLock !== togglePosition) {
UI.rfb.pointerLock = togglePosition;
}
if (UI.rfb.pointerRelative !== togglePosition) {
UI.rfb.pointerRelative = togglePosition;
}
if (togglePosition) {
document.getElementById('noVNC_game_mode_button').classList.add("noVNC_selected");
} else {
document.getElementById('noVNC_game_mode_button').classList.remove("noVNC_selected");
UI.forceSetting('pointer_lock', false, false);
}
UI.sendMessage('enable_game_mode', togglePosition);
UI.sendMessage('enable_pointer_lock', togglePosition);
},
/* ------^-------
* /VIEW CLIPPING
* ==============
@ -1739,9 +1927,9 @@ const UI = {
}
if (UI.rfb.clipViewport) {
viewDragButton.classList.remove("noVNC_hidden");
UI.showControlInput('noVNC_view_drag_button');
} else {
viewDragButton.classList.add("noVNC_hidden");
UI.hideControlInput('noVNC_view_drag_button');
}
},
@ -1881,18 +2069,41 @@ const UI = {
UI.rfb.translateShortcuts = UI.getSetting('translate_shortcuts');
},
toggleKeyboardControls() {
if (UI.getSetting('virtual_keyboard_visible')) {
UI.showKeyboardControls();
} else {
UI.hideKeyboardControls();
}
},
toggleIMEMode() {
if (UI.rfb) {
if (UI.getSetting('enable_ime')) {
UI.rfb.keyboard.enableIME = true;
} else {
UI.rfb.keyboard.enableIME = false;
}
}
},
showKeyboardControls() {
document.querySelector(".keyboard-controls").classList.add("is-visible");
document.getElementById('noVNC_keyboard_control').classList.add("is-visible");
},
hideKeyboardControls() {
document.querySelector(".keyboard-controls").classList.remove("is-visible");
document.getElementById('noVNC_keyboard_control').classList.remove("is-visible");
},
showVirtualKeyboard() {
const input = document.getElementById('noVNC_keyboardinput');
if (document.activeElement == input) return;
if (document.activeElement == input || !UI.rfb) return;
if (UI.getSetting('virtual_keyboard_visible')) {
document.getElementById('noVNC_keyboard_control_handle')
.classList.add("noVNC_selected");
}
input.focus();
@ -1916,7 +2127,12 @@ const UI = {
hideVirtualKeyboard() {
const input = document.getElementById('noVNC_keyboardinput');
if (document.activeElement != input) return;
if (document.activeElement != input || !UI.rfb) return;
if (UI.getSetting('virtual_keyboard_visible')) {
document.getElementById('noVNC_keyboard_control_handle')
.classList.remove("noVNC_selected");
}
input.blur();
},
@ -1941,6 +2157,12 @@ const UI = {
onblurVirtualKeyboard(event) {
document.getElementById('noVNC_keyboard_button')
.classList.remove("noVNC_selected");
if (UI.getSetting('virtual_keyboard_visible')) {
document.getElementById('noVNC_keyboard_control_handle')
.classList.remove("noVNC_selected");
}
if (UI.rfb) {
UI.rfb.focusOnClick = true;
}
@ -1974,83 +2196,6 @@ const UI = {
event.preventDefault();
},
keyboardinputReset() {
const kbi = document.getElementById('noVNC_keyboardinput');
kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
UI.lastKeyboardinput = kbi.value;
},
keyEvent(keysym, code, down) {
if (!UI.rfb) return;
UI.rfb.sendKey(keysym, code, down);
},
// When normal keyboard events are left uncought, use the input events from
// the keyboardinput element instead and generate the corresponding key events.
// This code is required since some browsers on Android are inconsistent in
// sending keyCodes in the normal keyboard events when using on screen keyboards.
keyInput(event) {
if (!UI.rfb) return;
const newValue = event.target.value;
if (!UI.lastKeyboardinput) {
UI.keyboardinputReset();
}
const oldValue = UI.lastKeyboardinput;
let newLen;
try {
// Try to check caret position since whitespace at the end
// will not be considered by value.length in some browsers
newLen = Math.max(event.target.selectionStart, newValue.length);
} catch (err) {
// selectionStart is undefined in Google Chrome
newLen = newValue.length;
}
const oldLen = oldValue.length;
let inputs = newLen - oldLen;
let backspaces = inputs < 0 ? -inputs : 0;
// Compare the old string with the new to account for
// text-corrections or other input that modify existing text
for (let i = 0; i < Math.min(oldLen, newLen); i++) {
if (newValue.charAt(i) != oldValue.charAt(i)) {
inputs = newLen - i;
backspaces = oldLen - i;
break;
}
}
// Send the key events
for (let i = 0; i < backspaces; i++) {
UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
}
for (let i = newLen - inputs; i < newLen; i++) {
UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
}
// Control the text content length in the keyboardinput element
if (newLen > 2 * UI.defaultKeyboardinputLen) {
UI.keyboardinputReset();
} else if (newLen < 1) {
// There always have to be some text in the keyboardinput
// element with which backspace can interact.
UI.keyboardinputReset();
// This sometimes causes the keyboard to disappear for a second
// but it is required for the android keyboard to recognize that
// text has been added to the field
event.target.blur();
// This has to be ran outside of the input handler in order to work
setTimeout(event.target.focus.bind(event.target), 0);
} else {
UI.lastKeyboardinput = newValue;
}
},
/* ------^-------
* /KEYBOARD
* ==============
@ -2082,7 +2227,11 @@ const UI = {
.classList.remove("noVNC_selected");
},
toggleExtraKeys() {
toggleExtraKeys(e) {
if (!UI.isControlPanelItemClick(e)) {
return false;
}
if (document.getElementById('noVNC_modifiers').classList.contains("noVNC_open")) {
UI.closeExtraKeys();
} else {
@ -2176,19 +2325,15 @@ const UI = {
// Hide input related buttons in view only mode
if (UI.rfb.viewOnly) {
document.getElementById('noVNC_keyboard_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
UI.hideControlInput("noVNC_keyboard_button");
UI.hideControlInput("noVNC_toggle_extra_keys_button");
UI.hideControlInput("noVNC_clipboard_button");
UI.hideControlInput("noVNC_game_mode_button");
} else {
document.getElementById('noVNC_keyboard_button')
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.remove('noVNC_hidden');
UI.showControlInput("noVNC_keyboard_button");
UI.showControlInput("noVNC_toggle_extra_keys_button");
UI.showControlInput("noVNC_clipboard_button");
UI.showControlInput("noVNC_game_mode_button");
}
},
@ -2207,6 +2352,39 @@ const UI = {
document.title = e.detail.name + " - " + PAGE_TITLE;
},
inputLockChanged(e) {
var pointer_lock_el = document.getElementById("noVNC_setting_pointer_lock");
var pointer_rel_el = document.getElementById("noVNC_game_mode_button");
if (e.detail.pointer) {
pointer_lock_el.checked = true;
UI.sendMessage('enable_pointer_lock', true);
UI.closeControlbar();
UI.showStatus('Press Esc Key to Exit Pointer Lock Mode', 'warn', 5000, true);
} else {
//If in game mode
if (UI.rfb.pointerRelative) {
UI.showStatus('Game Mode paused, click on screen to resume Game Mode.', 'warn', 5000, true);
} else {
UI.forceSetting('pointer_lock', false, false);
document.getElementById('noVNC_game_mode_button')
.classList.remove("noVNC_selected");
UI.sendMessage('enable_pointer_lock', false);
}
}
},
inputLockError(e) {
UI.showStatus('Unable to enter pointer lock mode.', 'warn', 5000, true);
UI.rfb.pointerRelative = false;
document.getElementById('noVNC_game_mode_button').classList.remove("noVNC_selected");
UI.forceSetting('pointer_lock', false, false);
UI.sendMessage('enable_game_mode', false);
UI.sendMessage('enable_pointer_lock', false);
},
bell(e) {
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
const promise = document.getElementById('noVNC_bell').play();

View File

@ -55,6 +55,7 @@ export const encodings = {
pseudoEncodingVideoOutTimeLevel100: -1887,
pseudoEncodingVMwareCursor: 0x574d5664,
pseudoEncodingVMwareCursorPosition: 0x574d5666,
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
};

32
core/input/imekeys.js Normal file
View File

@ -0,0 +1,32 @@
/*
* KasmVNC: HTML5 VNC client
* Copyright (C) 2022 Kasm Technologies Inc
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
/*
* Keys that could be interaction with IME input
*/
export default {
0x30: 'Digit0',
0x31: 'Digit1',
0x32: 'Digit2',
0x33: 'Digit3',
0x34: 'Digit4',
0x35: 'Digit5',
0x36: 'Digit6',
0x37: 'Digit7',
0x38: 'Digit8',
0x39: 'Digit9',
0x60: 'Numpad0',
0x61: 'Numpad1',
0x62: 'Numpad2',
0x63: 'Numpad3',
0x64: 'Numpad4',
0x65: 'Numpad5',
0x66: 'Numpad6',
0x67: 'Numpad7',
0x68: 'Numpad8',
0x69: 'Numpad9'
};

View File

@ -8,16 +8,20 @@ import * as Log from '../util/logging.js';
import { stopEvent } from '../util/events.js';
import * as KeyboardUtil from "./util.js";
import KeyTable from "./keysym.js";
import keysyms from "./keysymdef.js";
import imekeys from "./imekeys.js";
import * as browser from "../util/browser.js";
import UI from '../../app/ui.js';
import { isChromiumBased } from '../util/browser.js';
//
// Keyboard event handler
//
export default class Keyboard {
constructor(target) {
this._target = target || null;
constructor(screenInput, touchInput) {
this._screenInput = screenInput;
this._touchInput = touchInput;
this._keyDownList = {}; // List of depressed keys
// (even if they are happy)
@ -28,11 +32,28 @@ export default class Keyboard {
'keyup': this._handleKeyUp.bind(this),
'keydown': this._handleKeyDown.bind(this),
'blur': this._allKeysUp.bind(this),
'compositionstart' : this._handleCompositionStart.bind(this),
'compositionend' : this._handleCompositionEnd.bind(this),
'input' : this._handleInput.bind(this)
};
// ===== EVENT HANDLERS =====
this.onkeyevent = () => {}; // Handler for key press/release
this._enableIME = false;
this._imeHold = false;
this._imeInProgress = false;
this._lastKeyboardInput = null;
this._defaultKeyboardInputLen = 100;
this._keyboardInputReset();
}
// ===== PUBLIC METHODS =====
get enableIME() { return this._enableIME; }
set enableIME(val) {
this._enableIME = val;
this.focus();
}
// ===== PRIVATE METHODS =====
@ -95,10 +116,135 @@ export default class Keyboard {
return 'Unidentified';
}
_handleCompositionStart(e) {
Log.Debug("composition started");
if (this._enableIME) {
this._imeHold = true;
this._imeInProgress = true;
}
}
_handleCompositionEnd(e) {
Log.Debug("Composition ended");
if (this._enableIME) { this._imeInProgress = false; }
if (isChromiumBased()) {
this._imeHold = false;
}
}
_handleInput(e) {
//input event occurs only when keyup keydown events don't prevent default
//IME events will make this happen, for example
//IME changes can back out old characters and replace, thus send differential if IME
//otherwise send new characters
if (this._enableIME && this._imeHold) {
Log.Debug("IME input change, sending differential");
if (!this._imeInProgress) {
this._imeHold = false; //Firefox fires compisitionend before last input change
}
const oldValue = this._lastKeyboardInput;
const newValue = e.target.value;
let diff_start = 0;
//find position where difference starts
for (let i = 0; i < Math.min(oldValue.length, newValue.length); i++) {
if (newValue.charAt(i) != oldValue.charAt(i)) {
break;
}
diff_start++;
}
//send backspaces if needed
for (let bs = oldValue.length - diff_start; bs > 0; bs--) {
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", true);
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", false);
}
//send new keys
for (let i = diff_start; i < newValue.length; i++) {
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', true);
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', false);
}
this._lastKeyboardInput = newValue;
} else {
Log.Debug("Non-IME input change, sending new characters");
const newValue = e.target.value;
if (!this._lastKeyboardInput) {
this._keyboardInputReset();
}
const oldValue = this._lastKeyboardInput;
let newLen;
try {
// Try to check caret position since whitespace at the end
// will not be considered by value.length in some browsers
newLen = Math.max(e.target.selectionStart, newValue.length);
} catch (err) {
// selectionStart is undefined in Google Chrome
newLen = newValue.length;
}
const oldLen = oldValue.length;
let inputs = newLen - oldLen;
let backspaces = inputs < 0 ? -inputs : 0;
// Compare the old string with the new to account for
// text-corrections or other input that modify existing text
for (let i = 0; i < Math.min(oldLen, newLen); i++) {
if (newValue.charAt(i) != oldValue.charAt(i)) {
inputs = newLen - i;
backspaces = oldLen - i;
break;
}
}
// Send the key events
for (let i = 0; i < backspaces; i++) {
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", true);
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", false);
}
for (let i = newLen - inputs; i < newLen; i++) {
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', true);
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', false);
}
// Control the text content length in the keyboardinput element
if (newLen > 2 * this._defaultKeyboardInputLen) {
this._keyboardInputReset();
} else if (newLen < 1) {
// There always have to be some text in the keyboardinput
// element with which backspace can interact.
this._keyboardInputReset();
// This sometimes causes the keyboard to disappear for a second
// but it is required for the android keyboard to recognize that
// text has been added to the field
e.target.blur();
// This has to be ran outside of the input handler in order to work
setTimeout(e.target.focus.bind(e.target), 0);
} else {
this._lastKeyboardInput = newValue;
}
}
}
_keyboardInputReset() {
this._touchInput.value = new Array(this._defaultKeyboardInputLen).join("_");
this._lastKeyboardInput = this._touchInput.value;
}
_handleKeyDown(e) {
const code = this._getKeyCode(e);
let keysym = KeyboardUtil.getKeysym(e);
if (this._isIMEInteraction(e)) {
//skip event if IME related
Log.Debug("Skipping keydown, IME interaction, code: " + code + " keysym: " + keysym + " keycode: " + e.keyCode);
return;
}
// Windows doesn't have a proper AltGr, but handles it using
// fake Ctrl+Alt. However the remote end might not be Windows,
// so we need to merge those in to a single AltGr event. We
@ -220,10 +366,15 @@ export default class Keyboard {
}
_handleKeyUp(e) {
stopEvent(e);
const code = this._getKeyCode(e);
if (this._isIMEInteraction(e)) {
//skip IME related events
Log.Debug("Skipping keyup, IME interaction, code: " + code + " keycode: " + e.keyCode);
return;
}
stopEvent(e);
// We can't get a release in the middle of an AltGr sequence, so
// abort that detection
if (this._altGrArmed) {
@ -271,13 +422,56 @@ export default class Keyboard {
Log.Debug("<< Keyboard.allKeysUp");
}
_isIMEInteraction(e) {
//input must come from touchinput (textarea) and ime must be enabled
if (e.target != this._touchInput || !this._enableIME) { return false; }
//keyCode of 229 is IME composition
if (e.keyCode == 229) {
return true;
}
//unfortunately, IME interactions can come through as events
//generally safe to ignore and let them come in as "input" events instead
//we can't do that with none character keys though
//Firefox does not seem to fire key events for IME interaction but Chrome does
//TODO: potentially skip this for Firefox browsers, needs more testing with different IME types
if (e.keyCode in imekeys) {
return true;
}
return false;
}
// ===== PUBLIC METHODS =====
focus() {
if (this._enableIME) {
this._touchInput.focus();
} else {
this._screenInput.focus();
}
}
blur() {
if (this._enableIME) {
this._touchInput.blur();
} else {
this._screenInput.blur();
}
}
grab() {
//Log.Debug(">> Keyboard.grab");
this._target.addEventListener('keydown', this._eventHandlers.keydown);
this._target.addEventListener('keyup', this._eventHandlers.keyup);
this._screenInput.addEventListener('keydown', this._eventHandlers.keydown);
this._screenInput.addEventListener('keyup', this._eventHandlers.keyup);
this._touchInput.addEventListener('keydown', this._eventHandlers.keydown);
this._touchInput.addEventListener('keyup', this._eventHandlers.keyup);
this._touchInput.addEventListener('compositionstart', this._eventHandlers.compositionstart);
this._touchInput.addEventListener('compositionend', this._eventHandlers.compositionend);
this._touchInput.addEventListener('input', this._eventHandlers.input);
// Release (key up) if window loses focus
window.addEventListener('blur', this._eventHandlers.blur);
@ -288,8 +482,15 @@ export default class Keyboard {
ungrab() {
//Log.Debug(">> Keyboard.ungrab");
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
this._screenInput.removeEventListener('keydown', this._eventHandlers.keydown);
this._screenInput.removeEventListener('keyup', this._eventHandlers.keyup);
this._touchInput.removeEventListener('keydown', this._eventHandlers.keydown);
this._touchInput.removeEventListener('keyup', this._eventHandlers.keyup);
this._touchInput.removeEventListener('compositionstart', this._eventHandlers.compositionstart);
this._touchInput.removeEventListener('compositionend', this._eventHandlers.compositionend);
this._touchInput.removeEventListener('input', this._eventHandlers.input);
window.removeEventListener('blur', this._eventHandlers.blur);
// Release (key up) all keys that are in a down state

View File

@ -34,6 +34,7 @@ import HextileDecoder from "./decoders/hextile.js";
import TightDecoder from "./decoders/tight.js";
import TightPNGDecoder from "./decoders/tightpng.js";
import UDPDecoder from './decoders/udp.js';
import { toSignedRelative16bit } from './util/int.js';
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
@ -43,7 +44,7 @@ var _videoQuality = 2;
var _enableWebP = false;
// Minimum wait (ms) between two mouse moves
const MOUSE_MOVE_DELAY = 17;
const MOUSE_MOVE_DELAY = 17;
// Wheel thresholds
let WHEEL_LINE_HEIGHT = 19; // Pixels for one line step (on Windows)
@ -71,7 +72,7 @@ const extendedClipboardActionNotify = 1 << 27;
const extendedClipboardActionProvide = 1 << 28;
export default class RFB extends EventTargetMixin {
constructor(target, urlOrChannel, options) {
constructor(target, touchInput, urlOrChannel, options) {
if (!target) {
throw new Error("Must specify target");
}
@ -172,6 +173,9 @@ export default class RFB extends EventTargetMixin {
this._mousePos = {};
this._mouseButtonMask = 0;
this._mouseLastMoveTime = 0;
this._pointerLock = false;
this._pointerLockPos = { x: 0, y: 0 };
this._pointerRelativeEnabled = false;
this._mouseLastPinchAndZoomTime = 0;
this._viewportDragging = false;
this._viewportDragPos = {};
@ -191,6 +195,8 @@ export default class RFB extends EventTargetMixin {
focusCanvas: this._focusCanvas.bind(this),
windowResize: this._windowResize.bind(this),
handleMouse: this._handleMouse.bind(this),
handlePointerLockChange: this._handlePointerLockChange.bind(this),
handlePointerLockError: this._handlePointerLockError.bind(this),
handleWheel: this._handleWheel.bind(this),
handleGesture: this._handleGesture.bind(this),
};
@ -247,7 +253,7 @@ export default class RFB extends EventTargetMixin {
}
this._display.onflush = this._onFlush.bind(this);
this._keyboard = new Keyboard(this._canvas);
this._keyboard = new Keyboard(this._canvas, touchInput);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._gestures = new GestureHandler();
@ -336,6 +342,45 @@ export default class RFB extends EventTargetMixin {
// ===== PROPERTIES =====
get pointerLock() { return this._pointerLock; }
set pointerLock(value) {
if (!this._pointerLock) {
if (this._canvas.requestPointerLock) {
this._canvas.requestPointerLock();
this._pointerLockChanging = true;
} else if (this._canvas.mozRequestPointerLock) {
this._canvas.mozRequestPointerLock();
this._pointerLockChanging = true;
}
} else {
if (window.document.exitPointerLock) {
window.document.exitPointerLock();
this._pointerLockChanging = true;
} else if (window.document.mozExitPointerLock) {
window.document.mozExitPointerLock();
this._pointerLockChanging = true;
}
}
}
get pointerRelative() { return this._pointerRelativeEnabled; }
set pointerRelative(value)
{
this._pointerRelativeEnabled = value;
if (value) {
let max_w = ((this._display.scale === 1) ? this._fbWidth : (this._fbWidth * this._display.scale));
let max_h = ((this._display.scale === 1) ? this._fbHeight : (this._fbHeight * this._display.scale));
this._pointerLockPos.x = Math.floor(max_w / 2);
this._pointerLockPos.y = Math.floor(max_h / 2);
// reset the cursor position to center
this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y };
this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y);
}
}
get keyboard() { return this._keyboard; }
get clipboardBinary() { return this._clipboardMode; }
set clipboardBinary(val) { this._clipboardMode = val; }
@ -748,11 +793,11 @@ export default class RFB extends EventTargetMixin {
}
focus() {
this._canvas.focus();
this._keyboard.focus();
}
blur() {
this._canvas.blur();
this._keyboard.blur();
}
clipboardPasteFrom(text) {
@ -914,6 +959,15 @@ export default class RFB extends EventTargetMixin {
// reason so we have to explicitly block it
this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
// Pointer Lock listeners need to be installed in document instead of the canvas.
if (document.onpointerlockchange !== undefined) {
document.addEventListener('pointerlockchange', this._eventHandlers.handlePointerLockChange, false);
document.addEventListener('pointerlockerror', this._eventHandlers.handlePointerLockError, false);
} else if (document.onmozpointerlockchange !== undefined) {
document.addEventListener('mozpointerlockchange', this._eventHandlers.handlePointerLockChange, false);
document.addEventListener('mozpointerlockerror', this._eventHandlers.handlePointerLockError, false);
}
// Wheel events
this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
@ -1036,6 +1090,13 @@ export default class RFB extends EventTargetMixin {
this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
if (document.onpointerlockchange !== undefined) {
document.removeEventListener('pointerlockchange', this._eventHandlers.handlePointerLockChange);
document.removeEventListener('pointerlockerror', this._eventHandlers.handlePointerLockError);
} else if (document.onmozpointerlockchange !== undefined) {
document.removeEventListener('mozpointerlockchange', this._eventHandlers.handlePointerLockChange);
document.removeEventListener('mozpointerlockerror', this._eventHandlers.handlePointerLockError);
}
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
window.removeEventListener('resize', this._eventHandlers.windowResize);
@ -1074,11 +1135,18 @@ export default class RFB extends EventTargetMixin {
value: null
}, "*");
// Re-enable pointerLock if relative cursor is enabled
// pointerLock must come from user initiated event
if (!this._pointerLock && this._pointerRelativeEnabled) {
this.pointerLock = true;
}
if (!this.focusOnClick) {
return;
}
this.focus();
}
_setDesktopName(name) {
@ -1409,8 +1477,34 @@ export default class RFB extends EventTargetMixin {
return;
}
let pos = clientToElement(ev.clientX, ev.clientY,
let pos;
if (this._pointerLock && !this._pointerRelativeEnabled) {
let max_w = ((this._display.scale === 1) ? this._fbWidth : (this._fbWidth * this._display.scale));
let max_h = ((this._display.scale === 1) ? this._fbHeight : (this._fbHeight * this._display.scale));
pos = {
x: this._mousePos.x + ev.movementX,
y: this._mousePos.y + ev.movementY,
};
if (pos.x < 0) {
pos.x = 0;
} else if (pos.x > max_w) {
pos.x = max_w;
}
if (pos.y < 0) {
pos.y = 0;
} else if (pos.y > max_h) {
pos.y = max_h;
}
this._cursor.move(pos.x, pos.y);
} else if (this._pointerLock && this._pointerRelativeEnabled) {
pos = {
x: this._mousePos.x + ev.movementX,
y: this._mousePos.y + ev.movementY,
};
} else {
pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
}
switch (ev.type) {
case 'mousedown':
@ -1526,12 +1620,54 @@ export default class RFB extends EventTargetMixin {
this._mouseLastMoveTime = Date.now();
}
_handlePointerLockChange(env) {
if (
document.pointerLockElement === this._canvas ||
document.mozPointerLockElement === this._canvas
) {
this._pointerLock = true;
this._cursor.setEmulateCursor(true);
} else {
this._pointerLock = false;
this._cursor.setEmulateCursor(false);
}
this.dispatchEvent(new CustomEvent(
"inputlock",
{ detail: { pointer: this._pointerLock }, }));
}
_handlePointerLockError() {
this._pointerLockChanging = false;
this.dispatchEvent(new CustomEvent(
"inputlockerror",
{ detail: { pointer: this._pointerLock }, }));
}
_sendMouse(x, y, mask) {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
if (this._pointerLock && this._pointerRelativeEnabled) {
// Use releative cursor position
var rel_16_x = toSignedRelative16bit(x - this._pointerLockPos.x);
var rel_16_y = toSignedRelative16bit(y - this._pointerLockPos.y);
//console.log("new_pos x" + x + ", y" + y);
//console.log("lock x " + this._pointerLockPos.x + ", y " + this._pointerLockPos.y);
//console.log("rel x " + rel_16_x + ", y " + rel_16_y);
RFB.messages.pointerEvent(this._sock, rel_16_x,
rel_16_y, mask);
// reset the cursor position to center
this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y };
this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y);
} else {
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
}
}
_sendScroll(x, y, dX, dY) {
@ -2352,16 +2488,16 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingVideoScalingLevel0 + this.videoScaling);
encs.push(encodings.pseudoEncodingFrameRateLevel10 + this.frameRate - 10);
encs.push(encodings.pseudoEncodingMaxVideoResolution);
// preferBandwidth choses preset settings. Since we expose all the settings, lets not pass this
// preferBandwidth choses preset settings. Since we expose all the settings, lets not pass this
if (this.preferBandwidth) // must be last - server processes in reverse order
encs.push(encodings.pseudoEncodingPreferBandwidth);
if (supportsCursorURIs && this._fbDepth == 24) {
if (this.preferLocalCursor || !isTouchDevice) {
encs.push(encodings.pseudoEncodingVMwareCursor);
encs.push(encodings.pseudoEncodingCursor);
}
if (this._fbDepth == 24) {
encs.push(encodings.pseudoEncodingVMwareCursor);
encs.push(encodings.pseudoEncodingCursor);
}
encs.push(encodings.pseudoEncodingVMwareCursorPosition);
RFB.messages.clientEncodings(this._sock, encs);
}
@ -2595,6 +2731,10 @@ export default class RFB extends EventTargetMixin {
for (let i = 0; i < num; i++) {
if (this._sock.rQwait("Binary Clipboard op id", 4, buffByteLen)) { return false; }
buffByteLen += 4;
let clipid = this._sock.rQshift32();
if (this._sock.rQwait("Binary Clipboard mimelen", 1, buffByteLen)) { return false; }
buffByteLen++;
let mimelen = this._sock.rQshift8();
@ -2635,10 +2775,10 @@ export default class RFB extends EventTargetMixin {
);
}
if (!this.clipboardBinary) { continue; }
Log.Info("Processed binary clipboard (ID: " + clipid + ") of MIME " + mime + " of length " + len);
if (!this.clipboardBinary) { continue; }
Log.Info("Processed binary clipboard of MIME " + mime + " of length " + len);
clipItemData[mime] = new Blob([data], { type: mime });
break;
default:
@ -2987,6 +3127,9 @@ export default class RFB extends EventTargetMixin {
case encodings.pseudoEncodingVMwareCursor:
return this._handleVMwareCursor();
case encodings.pseudoEncodingVMwareCursorPosition:
return this._handleVMwareCursorPosition();
case encodings.pseudoEncodingCursor:
return this._handleCursor();
@ -3125,6 +3268,19 @@ export default class RFB extends EventTargetMixin {
return true;
}
_handleVMwareCursorPosition() {
const x = this._FBU.x;
const y = this._FBU.y;
if (this._pointerLock) {
// Only attempt to match the server's pointer position if we are in
// pointer lock mode.
this._mousePos = { x: x, y: y };
}
return true;
}
_handleCursor() {
const hotx = this._FBU.x; // hotspot-x
const hoty = this._FBU.y; // hotspot-y

View File

@ -97,6 +97,51 @@ export function isSafari() {
navigator.userAgent.indexOf('Chrome') === -1);
}
// Returns IE version number if IE or older Edge browser
export function isIE() {
var ua = window.navigator.userAgent;
// Test values; Uncomment to check result &
// IE 10
// ua = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)';
// IE 11
// ua = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
// Edge 12 (Spartan)
// ua = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0';
// Edge 13
// ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586';
var msie = ua.indexOf('MSIE ');
var ie_ver = false;
if (msie > 0) {
// IE 10 or older => return version number
ie_ver = parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
}
var trident = ua.indexOf('Trident/');
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:');
ie_ver = parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
var edge = ua.indexOf('Edge/');
if (edge > 0) {
// Edge (IE 12+) => return version number
ie_ver = parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
}
return ie_ver;
}
export function isChromiumBased() {
return (!!window.chrome);
}
export function isFirefox() {
return navigator && !!(/firefox/i).exec(navigator.userAgent);
}
@ -107,3 +152,10 @@ export function supportsBinaryClipboard() {
return (navigator.clipboard && typeof navigator.clipboard.read === "function");
}
export function supportsPointerLock() {
//Older versions of edge do support browser lock, but seems to not behave as expected
//Disable on browsers that don't fully support or work as expected
if (isIOS() || isIE()) { return false; }
return (document.exitPointerLock);
}

View File

@ -6,21 +6,19 @@
import { supportsCursorURIs, isTouchDevice } from './browser.js';
const useFallback = !supportsCursorURIs || isTouchDevice;
const needsFallback = !supportsCursorURIs || isTouchDevice;
export default class Cursor {
constructor() {
this._target = null;
this._canvas = document.createElement('canvas');
if (useFallback) {
this._canvas.style.position = 'fixed';
this._canvas.style.zIndex = '65535';
this._canvas.style.pointerEvents = 'none';
// Can't use "display" because of Firefox bug #1445997
this._canvas.style.visibility = 'hidden';
}
this._canvas.style.position = 'fixed';
this._canvas.style.zIndex = '65535';
this._canvas.style.pointerEvents = 'none';
// Can't use "display" because of Firefox bug #1445997
this._canvas.style.visibility = 'hidden';
this._useFallback = needsFallback;
this._position = { x: 0, y: 0 };
this._hotSpot = { x: 0, y: 0 };
@ -40,9 +38,15 @@ export default class Cursor {
this._target = target;
if (useFallback) {
document.body.appendChild(this._canvas);
document.body.appendChild(this._canvas);
if (needsFallback) {
// Only add the event listeners if this will be responsible for
// rendering the cursor all the time. Otherwise, the cursor will
// only be rendered then the forced emulation is turned on, and
// that doesn't require this class to be adjusting the cursor
// position.
const options = { capture: true, passive: true };
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
@ -58,16 +62,16 @@ export default class Cursor {
return;
}
if (useFallback) {
if (needsFallback) {
const options = { capture: true, passive: true };
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
document.body.removeChild(this._canvas);
}
document.body.removeChild(this._canvas);
this._target = null;
}
@ -91,9 +95,10 @@ export default class Cursor {
ctx.clearRect(0, 0, w, h);
ctx.putImageData(img, 0, 0);
if (useFallback) {
if (this._useFallback || needsFallback) {
this._updatePosition();
} else {
}
if (!needsFallback) {
let url = this._canvas.toDataURL();
this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
}
@ -112,7 +117,7 @@ export default class Cursor {
// Mouse events might be emulated, this allows
// moving the cursor in such cases
move(clientX, clientY) {
if (!useFallback) {
if (!this._useFallback) {
return;
}
// clientX/clientY are relative the _visual viewport_,
@ -130,6 +135,22 @@ export default class Cursor {
this._updateVisibility(target);
}
// Force the use of cursor emulation. This is needed when the pointer lock
// is in use, since the browser will not render the cursor.
setEmulateCursor(emulate) {
if (needsFallback) {
// We need to use the fallback all the time, so we shouldn't update
// the fallback flag.
return;
}
this._useFallback = emulate;
if (this._useFallback) {
this._showCursor();
} else {
this._hideCursor();
}
}
_handleMouseOver(event) {
// This event could be because we're entering the target, or
// moving around amongst its sub elements. Let the move handler

View File

@ -15,12 +15,40 @@ export function toSigned32bit(toConvert) {
}
/*
* Fast hashing function with low entropy, not for security uses.
* Converts a signed 32bit integer to a signed 16bit int
* Uses second most significant bit to represent it is relative
*/
export function toSignedRelative16bit(toConvert) {
// TODO: move these so they are not computed with every func call
var negmask16 = 1 << 15;
var negmask32 = 1 << 31;
var relmask16 = 1 << 14;
var converted16 = toConvert | 0;
// number is negative
if ((toConvert & negmask32) != 0) {
// clear the 32bit negative bit
// not neccessary because the last 16bits will get dropped anyway
converted16 *= -1;
// set the 16bit negative bit
converted16 |= negmask16;
// set the relative bit
converted16 |= relmask16;
} else {
// set the relative bit
converted16 |= relmask16;
}
return converted16;
}
/* Fast hashing function with low entropy */
export function hashUInt8Array(data) {
let h;
for (let i = 0; i < data.length; i++) {
h = Math.imul(31, h) + data[i] | 0;
}
return h;
}
}

View File

@ -113,6 +113,10 @@ protocol stream.
- The `capabilities` event is fired when `RFB.capabilities` is
updated.
[`inputlock`](#inputlock)
- The `inputlock` event is fired when an input lock is acquired (or released)
by the canvas.
### Methods
[`RFB.disconnect()`](#rfbdisconnect)
@ -146,6 +150,10 @@ protocol stream.
[`RFB.clipboardPasteFrom()`](#rfbclipboardPasteFrom)
- Send clipboard contents to server.
[`inputlock`](#inputlock)
- The `inputlock` event is fired when an input lock is acquired (or released)
by the canvas.
### Details
#### RFB()
@ -262,6 +270,15 @@ The `capabilities` event is fired whenever an entry is added or removed
from `RFB.capabilities`. The `detail` property is an `Object` with the
property `capabilities` containing the new value of `RFB.capabilities`.
#### inputlock
The `inputlock` event is fired after a request to acquire an input lock or
whenever the state of the canvas' input lock has changed, the latter typically
occurs because the lock was released by the user pressing the ESC key or
performing a browser-specific gesture. The `detail` property is an `Object`
with the property `pointer` containing whether the Pointer Lock is currently
held or not.
#### RFB.disconnect()
The `RFB.disconnect()` method is used to disconnect from the currently
@ -383,3 +400,24 @@ to the remote server.
**`text`**
- A `DOMString` specifying the clipboard data to send.
#### RFB.requestInputLock()
The `RFB.requestInputLock()` method is used to request that the RFB canvas hold
an input lock. An `inputlock` event will be fired with the result of the
acquisition of the requested locks.
##### Syntax
RFB.requestInputLock( { pointer: true } );
###### Parameters
**`pointer`**
- Requests to acquire a [Pointer
Lock](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API),
which hides the local mouse cursor and provides relative motion events.
This must be called directly from an event handler where a user has
directly interacted with an element through an [engagement
gesture](https://w3c.github.io/pointerlock/#dfn-engagement-gesture) (e.g. a
click or touch event) for the browser to allow this.

View File

@ -2610,6 +2610,27 @@ describe('Remote Frame Buffer Protocol Client', function () {
client._canvas.dispatchEvent(ev);
}
function supportsSendMouseMovementEvent() {
// Some browsers (like Safari) support the movementX /
// movementY properties of MouseEvent, but do not allow creation
// of non-trusted events with those properties.
let ev;
ev = new MouseEvent('mousemove',
{ 'movementX': 100,
'movementY': 100 });
return ev.movementX === 100 && ev.movementY === 100;
}
function sendMouseMovementEvent(dx, dy) {
let ev;
ev = new MouseEvent('mousemove',
{ 'movementX': dx,
'movementY': dy });
client._canvas.dispatchEvent(ev);
}
function sendMouseButtonEvent(x, y, down, button) {
let pos = elementToClient(x, y);
let ev;
@ -2723,6 +2744,62 @@ describe('Remote Frame Buffer Protocol Client', function () {
50, 70, 0x0);
});
it('should ignore remote cursor position updates', function () {
if (!supportsSendMouseMovementEvent()) {
this.skip();
return;
}
// Simple VMware Cursor Position FBU message with pointer coordinates
// (50, 50).
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32,
0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ];
client._resize(100, 100);
const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition');
client._sock._websocket._receiveData(new Uint8Array(incoming));
expect(cursorSpy).to.have.been.calledOnceWith();
cursorSpy.restore();
expect(client._mousePos).to.deep.equal({ });
sendMouseMoveEvent(10, 10);
clock.tick(100);
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
10, 10, 0x0);
});
it('should handle remote mouse position updates in pointer lock mode', function () {
if (!supportsSendMouseMovementEvent()) {
this.skip();
return;
}
// Simple VMware Cursor Position FBU message with pointer coordinates
// (50, 50).
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32,
0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ];
client._resize(100, 100);
const spy = sinon.spy();
client.addEventListener("inputlock", spy);
let stub = sinon.stub(document, 'pointerLockElement');
stub.get(function () { return client._canvas; });
client._handlePointerLockChange();
stub.restore();
client._sock._websocket._receiveData(new Uint8Array([0x02, 0x02]));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.pointer).to.be.true;
const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition');
client._sock._websocket._receiveData(new Uint8Array(incoming));
expect(cursorSpy).to.have.been.calledOnceWith();
cursorSpy.restore();
expect(client._mousePos).to.deep.equal({ x: 50, y: 50 });
sendMouseMovementEvent(10, 10);
clock.tick(100);
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
60, 60, 0x0);
});
describe('Event Aggregation', function () {
it('should send a single pointer event on mouse movement', function () {
sendMouseMoveEvent(50, 70);

709
vnc.html
View File

@ -86,300 +86,434 @@
Loading statistics...
</div>
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
id="noVNC_toggle_ctrl_button" class="noVNC_button"
title="Toggle Ctrl">
<input type="image" alt="Alt" src="app/images/alt.svg"
id="noVNC_toggle_alt_button" class="noVNC_button"
title="Toggle Alt">
<input type="image" alt="Windows" src="app/images/windows.svg"
id="noVNC_toggle_windows_button" class="noVNC_button"
title="Toggle Windows">
<input type="image" alt="Tab" src="app/images/tab.svg"
id="noVNC_send_tab_button" class="noVNC_button"
title="Send Tab">
<input type="image" alt="Esc" src="app/images/esc.svg"
id="noVNC_send_esc_button" class="noVNC_button"
title="Send Escape">
<input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
title="Send Ctrl-Alt-Del">
</div>
</div>
<!-- noVNC Control Bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar">
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar">
<div></div>
</div>
<div class="noVNC_scroll">
<h1 class="noVNC_logo"><img src="app/images/icons/368_kasm_logo_only_24x24.png" /></h1>
<h1 class="noVNC_logo"><img src="app/images/icons/kasm_logo.png" /></h1>
<!-- Drag/Pan the viewport -->
<input type="image" alt="Drag" src="app/images/drag.svg"
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
title="Move/Drag Viewport">
<!--noVNC Touch Device only buttons-->
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
title="Show Extra Keys">
<!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
id="noVNC_power_button" class="noVNC_button"
title="Shutdown/Reboot...">
<div class="noVNC_vcenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
<!-- Drag/Pan the viewport -->
<div class="noVNC_button_div noVNC_hidden noVNC_hide_on_disconnect" >
<input type="image" alt="Drag" src="app/images/drag.svg"
id="noVNC_view_drag_button" class="noVNC_button"
title="Move/Drag Viewport">
Drag Viewport
</div>
<input type="button" id="noVNC_shutdown_button" value="Shutdown">
<input type="button" id="noVNC_reboot_button" value="Reboot">
<input type="button" id="noVNC_reset_button" value="Reset">
</div>
</div>
<!-- Clipboard -->
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
id="noVNC_clipboard_button" class="noVNC_button"
title="Clipboard">
<div class="noVNC_vcenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
<!--noVNC Touch Device only buttons-->
<!-- Extra manual keys -->
<div class="noVNC_button_div noVNC_hide_on_disconnect" >
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
title="Show Extra Keys">
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
id="noVNC_toggle_ctrl_button" class="noVNC_button"
title="Toggle Ctrl">
<input type="image" alt="Alt" src="app/images/alt.svg"
id="noVNC_toggle_alt_button" class="noVNC_button"
title="Toggle Alt">
<input type="image" alt="Windows" src="app/images/windows.svg"
id="noVNC_toggle_windows_button" class="noVNC_button"
title="Toggle Windows">
<input type="image" alt="Tab" src="app/images/tab.svg"
id="noVNC_send_tab_button" class="noVNC_button"
title="Send Tab">
<input type="image" alt="Esc" src="app/images/esc.svg"
id="noVNC_send_esc_button" class="noVNC_button"
title="Send Escape">
<input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
title="Send Ctrl-Alt-Del">
</div>
</div>
Keys
</div>
<textarea id="noVNC_clipboard_text" rows=5></textarea>
<br>
<input id="noVNC_clipboard_clear_button" type="button"
value="Clear" class="noVNC_submit">
</div>
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
title="Fullscreen">
<!-- Shutdown/Reboot -->
<div class="noVNC_button_div noVNC_hide_on_disconnect" >
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
id="noVNC_power_button" class="noVNC_button"
title="Shutdown/Reboot...">
<div class="noVNC_vcenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
</div>
<input type="button" id="noVNC_shutdown_button" value="Shutdown">
<input type="button" id="noVNC_reboot_button" value="Reboot">
<input type="button" id="noVNC_reset_button" value="Reset">
</div>
</div>
Power
</div>
<!-- Settings -->
<input type="image" alt="Settings" src="app/images/settings.svg"
id="noVNC_settings_button" class="noVNC_button"
title="Settings">
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<ul>
<li class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</li>
<li>
<label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
</li>
<li>
<label><input id="noVNC_setting_view_only" type="checkbox" /> View Only</label></li>
<li>
<label><input id="noVNC_setting_clipboard_up" type="checkbox" /> Clipboard Up</label></li>
<li>
<label><input id="noVNC_setting_clipboard_down" type="checkbox" /> Clipboard Down</label></li>
<li>
<label><input id="noVNC_setting_clipboard_seamless" type="checkbox" /> Clipboard Seamless</label></li>
<li>
<label><input id="noVNC_setting_prefer_local_cursor" type="checkbox" /> Prefer Local Cursor</label>
</li>
<li>
<label>
<input id="noVNC_setting_translate_shortcuts" type="checkbox" />Translate keyboard shurtcuts
</label>
</li>
</li>
<li>
<label><input id="noVNC_setting_enable_webp" type="checkbox" /> Enable WebP Compression</label></li>
<li>
<label><input id="noVNC_setting_enable_perf_stats" type="checkbox" /> Enable Performance Stats</label></li>
<li>
<label><input id="noVNC_setting_toggle_control_panel" type="checkbox" /> Toggle Control Panel via Keystrokes</label></li>
<li>
<label for="noVNC_setting_idle_disconnect">Idle Timeout:</label>
<select id="noVNC_setting_idle_disconnect" name="vncIdleDisconnect">
<option value=20>20</option>
</select>
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling Mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local Scaling</option>
<option value="remote">Remote Resizing</option>
</select>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Stream Quality</div>
<div><ul>
<li>
<label for="noVNC_setting_video_quality">Preset Modes:</label>
<select id="noVNC_setting_video_quality" name="vncVideoQuality">
<option value=0>Static</option>
<option value=1>Low</option>
<option value=2>Medium</option>
<option value=3>High</option>
<option value=4>Extreme</option>
<option value=10>Custom</option>
</select>
</li>
<li>
<label for="noVNC_setting_anti_aliasing">Anti-Aliasing:</label>
<select id="noVNC_setting_anti_aliasing" name="vncAntiAliasing">
<option value=0>Auto Dynamic</option>
<option value=1>On</option>
<option value=2>Off</option>
</select>
</li>
<li style="display: none;">
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_dynamic_quality_min">Dynamic Quality Min:</label>
<input id="noVNC_setting_dynamic_quality_min" type="range" min="0" max="9" value="3" onchange="noVNC_setting_dynamic_quality_min_output.value=value">
<output id="noVNC_setting_dynamic_quality_min_output">3</output>
</li>
<li>
<label for="noVNC_setting_dynamic_quality_max">Dynamic Quality Max:</label>
<input id="noVNC_setting_dynamic_quality_max" type="range" min="0" max="9" value="9" onchange="noVNC_setting_dynamic_quality_max_output.value=value">
<output id="noVNC_setting_dynamic_quality_max_output">9</output>
</li>
<li>
<label for="noVNC_setting_treat_lossless">Treat Lossless:</label>
<input id="noVNC_setting_treat_lossless" type="range" min="0" max="9" value="7" onchange="noVNC_setting_treat_lossless_output.value=value">
<output id="noVNC_setting_treat_lossless_output">7</output>
</li>
<li>
<label for="noVNC_setting_framerate">Frame Rate:</label>
<input id="noVNC_setting_framerate" type="number" min="1" max="120" value="30">
</li>
<li>
<label for="noVNC_setting_jpeg_video_quality">Video JPEG Quality:</label>
<input id="noVNC_setting_jpeg_video_quality" type="range" min="0" max="9" value="5" onchange="noVNC_setting_jpeg_video_quality_output.value=value">
<output id="noVNC_setting_jpeg_video_quality_output">5</output>
</li>
<li>
<label for="noVNC_setting_webp_video_quality">Video WEBP Quality:</label>
<input id="noVNC_setting_webp_video_quality" type="range" min="0" max="9" value="5" onchange="noVNC_setting_webp_video_quality_output.value=value">
<output id="noVNC_setting_webp_video_quality_output">5</output>
</li>
<li>
<label for="noVNC_setting_video_area">Video Area:</label>
<input id="noVNC_setting_video_area" type="range" min="0" max="100" value="65" onchange="noVNC_setting_video_area_output.value=value">
<output id="noVNC_setting_video_area_output">65</output>
</li>
<li>
<label for="noVNC_setting_video_time">Video Time:</label>
<input id="noVNC_setting_video_time" type="range" min="0" max="60" value="5" onchange="noVNC_setting_video_time_output.value=value">
<output id="noVNC_setting_video_time_output">5</output>
</li>
<li>
<label for="noVNC_setting_video_out_time">Video Out Time:</label>
<input id="noVNC_setting_video_out_time" type="range" min="0" max="60" value="3" onchange="noVNC_setting_video_out_time_output.value=value">
<output id="noVNC_setting_video_out_time_output">3</output>
</li>
<li>
<label for="noVNC_setting_video_scaling">Scaling Mode:</label>
<select id="noVNC_setting_video_scaling" name="vncVideoModeScaling">
<option value="0">Nearest</option>
<option value="1">Bilinear</option>
<option value="2">Progressive Bilinear</option>
</select>
</li>
<li>
<label for="noVNC_setting_max_video_resolution_x">Video Mode Width:</label>
<input id="noVNC_setting_max_video_resolution_x" type="number" min="100" max="3840" value="960">
</li>
<li>
<label for="noVNC_setting_max_video_resolution_y">Video Mode Height:</label>
<input id="noVNC_setting_max_video_resolution_y" type="number" min="100" max="2160" value="540">
</li>
</ul></div>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">
</li>
<li>
<div class="noVNC_expander">WebSocket</div>
<div><ul>
<li>
<label><input id="noVNC_setting_encrypt" type="checkbox"> Encrypt</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host">
</li>
<li>
<label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" type="number">
</li>
<li>
<label for="noVNC_setting_path">Path:</label>
<input id="noVNC_setting_path" type="text" value="websockify">
</li>
</ul></div>
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic Reconnect</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No Cursor</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
<li>
<label>Logging:
<select id="noVNC_setting_logging" name="vncLogging">
<!-- Clipboard -->
<div class="noVNC_button_div noVNC_hide_on_disconnect" >
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
id="noVNC_clipboard_button" class="noVNC_button"
title="Clipboard">
Clipboard
<div class="noVNC_vcenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
</div>
<textarea id="noVNC_clipboard_text" rows=5></textarea>
<br>
<input id="noVNC_clipboard_clear_button" type="button"
value="Clear" class="noVNC_submit">
</div>
</div>
</div>
<!-- Toggle fullscreen -->
<div class="noVNC_button_div noVNC_hidden" >
<input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
id="noVNC_fullscreen_button" class="noVNC_button"
title="Fullscreen">
Fullscreen
</div>
<!-- Toggle game mode -->
<div class="noVNC_button_div noVNC_hidden noVNC_hide_on_disconnect" >
<input type="image" alt="Game Mode" src="app/images/gamepad.png"
id="noVNC_game_mode_button" class="noVNC_button"
title="Game Pointer Mode">
Game Cursor Mode
</div>
<!-- Settings -->
<div class="noVNC_button_div" >
<input type="image" alt="Settings" src="app/images/settings.svg"
id="noVNC_settings_button" class="noVNC_button"
title="Settings">
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<ul>
<li class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</li>
<li>
<label class="switch"><input id="noVNC_setting_shared" type="checkbox">
<span class="slider round"></span>
<span class="slider-label">Shared Mode</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_view_only" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">View Only</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_clipboard_up" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Clipboard Up</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_clipboard_down" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Clipboard Down</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_clipboard_seamless" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Clipboard Seamless</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_prefer_local_cursor" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Prefer Local Cursor</span>
</label>
</li>
<li>
<label class="switch">
<input id="noVNC_setting_translate_shortcuts" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Translate keyboard shurtcuts</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_enable_webp" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Enable WebP Compression</span>
</label>
</li>
<li>
<label class="switch">
<input id="noVNC_setting_enable_perf_stats" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Enable Performance Stats</span>
</label>
</li>
<li>
<label class="switch">
<input type="checkbox" id="noVNC_setting_pointer_lock" />
<span class="slider round"></span>
<span class="slider-label">Enable Pointer Lock</span>
</label>
</li>
<label class="switch"><input id="noVNC_setting_enable_ime" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">IME Input Mode</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_virtual_keyboard_visible" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Show Virtual Keyboard Control</span>
</label>
</li>
<li>
<label class="switch"><input id="noVNC_setting_toggle_control_panel" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Toggle Control Panel via Keystrokes</span>
</label>
</li>
<li>
<label for="noVNC_setting_idle_disconnect">Idle Timeout:</label>
<select id="noVNC_setting_idle_disconnect" name="vncIdleDisconnect">
<option value=10>10</option>
<option value=20>20</option>
<option value=30>30</option>
<option value=60>60</option>
</select>
</label>
</li>
</ul></div>
</li>
<li class="noVNC_version_separator"><hr></li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li>
</ul>
</div>
</div>
</li>
<li><hr></li>
<li>
<label class="switch">
<input id="noVNC_setting_view_clip" type="checkbox">
<span class="slider round"></span>
<span class="slider-label">Clip to Window</span>
</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling Mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local Scaling</option>
<option value="remote">Remote Resizing</option>
</select>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Keyboard Shortcuts</div>
<div>
<ul>
<li>
<label class="switch">
<input id="noVNC_setting_toggle_control_panel" type="checkbox" />
<span class="slider round"></span>
<span class="slider-label">Enable KasmVNC Keyboard Shortcuts</span>
</label>
</li>
<li>Ctrl+Shift+</li>
<li>1 - Toggle Control Panel</li>
<li>2 - Toggle Game Pointer Mode</li>
<li>3 - Toggle Pointer Lock</li>
</ul>
</div>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Stream Quality</div>
<div>
<ul>
<li>
<label for="noVNC_setting_video_quality">Preset Modes:</label>
<select id="noVNC_setting_video_quality" name="vncVideoQuality">
<option value=0>Static</option>
<option value=1>Low</option>
<option value=2>Medium</option>
<option value=3>High</option>
<option value=4>Extreme</option>
<option value=10>Custom</option>
</select>
</li>
<li>
<label for="noVNC_setting_anti_aliasing">Anti-Aliasing:</label>
<select id="noVNC_setting_anti_aliasing" name="vncAntiAliasing">
<option value=0>Auto Dynamic</option>
<option value=1>On</option>
<option value=2>Off</option>
</select>
</li>
<li style="display: none;">
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_dynamic_quality_min">Dynamic Quality Min:</label>
<input id="noVNC_setting_dynamic_quality_min" type="range" min="0" max="9" value="3" onchange="noVNC_setting_dynamic_quality_min_output.value=value">
<output id="noVNC_setting_dynamic_quality_min_output">3</output>
</li>
<li>
<label for="noVNC_setting_dynamic_quality_max">Dynamic Quality Max:</label>
<input id="noVNC_setting_dynamic_quality_max" type="range" min="0" max="9" value="9" onchange="noVNC_setting_dynamic_quality_max_output.value=value">
<output id="noVNC_setting_dynamic_quality_max_output">9</output>
</li>
<li>
<label for="noVNC_setting_treat_lossless">Treat Lossless:</label>
<input id="noVNC_setting_treat_lossless" type="range" min="0" max="9" value="7" onchange="noVNC_setting_treat_lossless_output.value=value">
<output id="noVNC_setting_treat_lossless_output">7</output>
</li>
<li>
<label for="noVNC_setting_framerate">Frame Rate:</label>
<input id="noVNC_setting_framerate" type="number" min="1" max="120" value="30">
</li>
<li>
<label for="noVNC_setting_jpeg_video_quality">Video JPEG Quality:</label>
<input id="noVNC_setting_jpeg_video_quality" type="range" min="0" max="9" value="5" onchange="noVNC_setting_jpeg_video_quality_output.value=value">
<output id="noVNC_setting_jpeg_video_quality_output">5</output>
</li>
<li>
<label for="noVNC_setting_webp_video_quality">Video WEBP Quality:</label>
<input id="noVNC_setting_webp_video_quality" type="range" min="0" max="9" value="5" onchange="noVNC_setting_webp_video_quality_output.value=value">
<output id="noVNC_setting_webp_video_quality_output">5</output>
</li>
<li>
<label for="noVNC_setting_video_area">Video Area:</label>
<input id="noVNC_setting_video_area" type="range" min="0" max="100" value="65" onchange="noVNC_setting_video_area_output.value=value">
<output id="noVNC_setting_video_area_output">65</output>
</li>
<li>
<label for="noVNC_setting_video_time">Video Time:</label>
<input id="noVNC_setting_video_time" type="range" min="0" max="60" value="5" onchange="noVNC_setting_video_time_output.value=value">
<output id="noVNC_setting_video_time_output">5</output>
</li>
<li>
<label for="noVNC_setting_video_out_time">Video Out Time:</label>
<input id="noVNC_setting_video_out_time" type="range" min="0" max="60" value="3" onchange="noVNC_setting_video_out_time_output.value=value">
<output id="noVNC_setting_video_out_time_output">3</output>
</li>
<li>
<label for="noVNC_setting_video_scaling">Scaling Mode:</label>
<select id="noVNC_setting_video_scaling" name="vncVideoModeScaling">
<option value="0">Nearest</option>
<option value="1">Bilinear</option>
<option value="2">Progressive Bilinear</option>
</select>
</li>
<li>
<label for="noVNC_setting_max_video_resolution_x">Video Mode Width:</label>
<input id="noVNC_setting_max_video_resolution_x" type="number" min="100" max="3840" value="960">
</li>
<li>
<label for="noVNC_setting_max_video_resolution_y">Video Mode Height:</label>
<input id="noVNC_setting_max_video_resolution_y" type="number" min="100" max="2160" value="540">
</li>
</ul>
</div>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Advanced</div>
<div>
<ul>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">
</li>
<li>
<div class="noVNC_expander">WebSocket</div>
<div>
<ul>
<li>
<label class="switch">
<input id="noVNC_setting_encrypt" type="checkbox">
<span class="slider round"></span>
<span class="slider-label">Encrypt</span>
</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host">
</li>
<li>
<label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" type="number">
</li>
<li>
<label for="noVNC_setting_path">Path:</label>
<input id="noVNC_setting_path" type="text" value="websockify">
</li>
</ul>
</div>
</li>
<li><hr></li>
<li>
<label class="switch">
<input id="noVNC_setting_reconnect" type="checkbox">
<span class="slider-label">Automatic Reconnect</span>
</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li><hr></li>
<li>
<label class="switch"><input id="noVNC_setting_show_dot" type="checkbox">
<span class="slider round"></span>
<span class="slider-label">Show Dot when No Cursor</span>
</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
<li>
<label>Logging:
<select id="noVNC_setting_logging" name="vncLogging">
</select>
</label>
</li>
</ul>
</div>
</li>
<li class="noVNC_version_separator"><hr></li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li>
</ul>
</div>
</div>
Settings
</div>
<!-- Connection Controls -->
<input type="image" alt="Disconnect" src="app/images/disconnect.svg"
id="noVNC_disconnect_button" class="noVNC_button"
title="Disconnect">
<!-- Connection Controls -->
<div class="noVNC_button_div noVNC_hide_on_disconnect" >
<input type="image" alt="Disconnect" src="app/images/disconnect.svg"
id="noVNC_disconnect_button" class="noVNC_button"
title="Disconnect">
Disconnect
</div>
<!-- Connection Controls -->
<div class="noVNC_button_div noVNC_hide_on_connect" id="noVNC_connect_button" >
<input type="image" alt="Connect" src="app/images/connect.svg"
class="noVNC_button"
title="Connect">
Connect
</div>
</div>
</div>
@ -389,15 +523,16 @@
</div> <!-- End of noVNC_control_bar -->
<!-- Status Dialog -->
<div style="visibility: hidden" id="noVNC_status"></div>
<div id="noVNC_status"></div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">
<!--div class="noVNC_logo" translate="no"><span>no</span>VNC</div-->
<div id="noVNC_connect_button"><div>
<img alt="" src="app/images/connect.svg"> Connect
</div></div>
<!--div id="noVNC_connect_button">
<div>
<img alt="" src="app/images/connect.svg"> Connect
</div>
</div-->
</div>
</div>
@ -444,7 +579,7 @@
<source src="app/sounds/bell.mp3" type="audio/mpeg">
</audio>
<div class="keyboard-controls">
<div id="noVNC_keyboard_control" class="keyboard-controls">
<div class="buttons">
<div class="button ctrl"></div>
<div class="button alt"></div>
@ -454,7 +589,7 @@
<div class="button ctrlaltdel"></div>
</div>
<div class="button keyboard handle"></div>
<div id="noVNC_keyboard_control_handle" class="button keyboard handle"></div>
</div>
</body>
</html>

View File

@ -1,182 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
noVNC example: lightweight example using minimal UI and features
This is a self-contained file which doesn't import WebUtil or external CSS.
Copyright (C) 2019 The noVNC Authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?host=HOST&port=PORT&scale=true
-->
<title>KasmVNC</title>
<meta charset="utf-8">
<style>
body {
margin: 0;
background-color: dimgrey;
height: 100%;
display: flex;
flex-direction: column;
}
html {
height: 100%;
}
#top_bar {
background-color: #6e84a3;
color: white;
font: bold 12px Helvetica;
padding: 6px 5px 4px 5px;
border-bottom: 1px outset;
}
#status {
text-align: center;
}
#sendCtrlAltDelButton {
position: fixed;
top: 0px;
right: 0px;
border: 1px outset;
padding: 5px 5px 4px 5px;
cursor: pointer;
}
#screen {
flex: 1; /* fill remaining space */
overflow: hidden;
}
</style>
<script type="module" crossorigin="anonymous">
// RFB holds the API to connect and communicate with a VNC server
import RFB from './core/rfb.js';
let rfb;
let desktopName;
// When this function is called we have
// successfully connected to a server
function connectedToServer(e) {
status("Connected to " + desktopName);
}
// This function is called when we are disconnected
function disconnectedFromServer(e) {
if (e.detail.clean) {
status("Disconnected");
} else {
status("Something went wrong, connection is closed");
}
}
// When this function is called, the server requires
// credentials to authenticate
function credentialsAreRequired(e) {
const password = prompt("Password Required:");
rfb.sendCredentials({ password: password });
}
// When this function is called we have received
// a desktop name from the server
function updateDesktopName(e) {
desktopName = e.detail.name;
}
// Since most operating systems will catch Ctrl+Alt+Del
// before they get a chance to be intercepted by the browser,
// we provide a way to emulate this key sequence.
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}
// Show a status text in the top bar
function status(text) {
document.getElementById('status').textContent = text;
}
// This function extracts the value of one variable from the
// query string. If the variable isn't defined in the URL
// it returns the default value instead.
function readQueryVariable(name, defaultValue) {
// A URL with a query parameter can look like this:
// https://www.example.com?myqueryparam=myvalue
//
// Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re);
if (match) {
// We have to decode the URL since want the cleartext value
return decodeURIComponent(match[1]);
}
return defaultValue;
}
document.getElementById('sendCtrlAltDelButton')
.onclick = sendCtrlAltDel;
// Read parameters specified in the URL query string
// By default, use the host and port of server that served this file
const host = readQueryVariable('host', window.location.hostname);
let port = readQueryVariable('port', window.location.port);
const password = readQueryVariable('password');
const path = readQueryVariable('path', 'websockify');
// | | | | | |
// | | | Connect | | |
// v v v v v v
status("Connecting");
// Build the websocket URL used to connect
let url;
if (window.location.protocol === "https:") {
url = 'wss';
} else {
url = 'ws';
}
url += '://' + host;
if(port) {
url += ':' + port;
}
url += '/' + path;
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('screen'), url,
{ credentials: { password: password } });
// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);
// Set parameters that can be changed on an active connection
rfb.viewOnly = readQueryVariable('view_only', false);
rfb.scaleViewport = readQueryVariable('scale', false);
</script>
</head>
<body>
<div id="top_bar">
<div id="status">Loading</div>
<div id="sendCtrlAltDelButton">Send CtrlAltDel</div>
</div>
<div id="screen">
<!-- This is where the remote screen will appear -->
</div>
</body>
</html>