/*
* noVNC: HTML5 VNC client
- * Copyright (C) 2018 The noVNC Authors
+ * Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
-import { isTouchDevice, dragThreshold } from '../core/util/browser.js';
+import { isTouchDevice, isSafari, isIOS, isAndroid, dragThreshold }
+ from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
import keysyms from "../core/input/keysymdef.js";
import RFB from "../core/rfb.js";
import * as WebUtil from "./webutil.js";
+const PAGE_TITLE = "noVNC";
+
const UI = {
connected: false,
controlbarMouseDownClientY: 0,
controlbarMouseDownOffsetY: 0,
- isSafari: false,
lastKeyboardinput: null,
defaultKeyboardinputLen: 100,
reconnect_callback: null,
reconnect_password: null,
- prime(callback) {
- if (document.readyState === "interactive" || document.readyState === "complete") {
- UI.load(callback);
- } else {
- document.addEventListener('DOMContentLoaded', UI.load.bind(UI, callback));
- }
- },
+ prime() {
+ return WebUtil.initSettings().then(() => {
+ if (document.readyState === "interactive" || document.readyState === "complete") {
+ return UI.start();
+ }
- // Setup rfb object, load settings from browser storage, then call
- // UI.init to setup the UI/menus
- load(callback) {
- WebUtil.initSettings(UI.start, callback);
+ return new Promise((resolve, reject) => {
+ document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject));
+ });
+ });
},
// Render default UI and initialize settings menu
- start(callback) {
-
- // Setup global variables first
- UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 &&
- navigator.userAgent.indexOf('Chrome') === -1);
+ start() {
UI.initSettings();
// Translate the DOM
l10n.translateDOM();
+ WebUtil.fetchJSON('../package.json')
+ .then((packageInfo) => {
+ Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
+ })
+ .catch((err) => {
+ Log.Error("Couldn't fetch package.json: " + err);
+ Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
+ .concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
+ .forEach(el => el.style.display = 'none');
+ });
+
// Adapt the interface for touch screen devices
if (isTouchDevice) {
document.documentElement.classList.add("noVNC_touch");
UI.openConnectPanel();
}
- if (typeof callback === "function") {
- callback(UI.rfb);
- }
+ return Promise.resolve(UI.rfb);
},
initFullscreen() {
// Only show the button if fullscreen is properly supported
// * Safari doesn't support alphanumerical input while in fullscreen
- if (!UI.isSafari &&
+ if (!isSafari() &&
(document.documentElement.requestFullscreen ||
document.documentElement.mozRequestFullScreen ||
document.documentElement.webkitRequestFullscreen ||
.addEventListener('click', UI.toggleExtraKeys);
document.getElementById("noVNC_toggle_ctrl_button")
.addEventListener('click', UI.toggleCtrl);
+ document.getElementById("noVNC_toggle_windows_button")
+ .addEventListener('click', UI.toggleWindows);
document.getElementById("noVNC_toggle_alt_button")
.addEventListener('click', UI.toggleAlt);
document.getElementById("noVNC_send_tab_button")
document.getElementById("noVNC_cancel_reconnect_button")
.addEventListener('click', UI.cancelReconnect);
- document.getElementById("noVNC_password_button")
- .addEventListener('click', UI.setPassword);
+ document.getElementById("noVNC_credentials_button")
+ .addEventListener('click', UI.setCredentials);
},
addClipboardHandlers() {
}
// State change closes the password dialog
- document.getElementById('noVNC_password_dlg')
+ document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open');
},
showStatus(text, status_type, time) {
const statusElem = document.getElementById('noVNC_status');
- clearTimeout(UI.statusTimeout);
-
if (typeof status_type === 'undefined') {
status_type = 'normal';
}
// Don't overwrite more severe visible statuses and never
// errors. Only shows the first error.
- let visible_status_type = 'none';
if (statusElem.classList.contains("noVNC_open")) {
if (statusElem.classList.contains("noVNC_status_error")) {
- visible_status_type = 'error';
- } else if (statusElem.classList.contains("noVNC_status_warn")) {
- visible_status_type = 'warn';
- } else {
- visible_status_type = 'normal';
+ return;
+ }
+ if (statusElem.classList.contains("noVNC_status_warn") &&
+ status_type === 'normal') {
+ return;
}
}
- if (visible_status_type === 'error' ||
- (visible_status_type === 'warn' && status_type === 'normal')) {
- return;
- }
+
+ clearTimeout(UI.statusTimeout);
switch (status_type) {
case 'error':
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
{ shared: UI.getSetting('shared'),
- showDotCursor: UI.getSetting('show_dot'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
UI.rfb.addEventListener("connect", UI.connectFinished);
UI.rfb.clipViewport = UI.getSetting('view_clip');
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
+ UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
},
UI.showStatus(_("Disconnected"), 'normal');
}
+ document.title = PAGE_TITLE;
+
UI.openControlbar();
UI.openConnectPanel();
},
credentials(e) {
// FIXME: handle more types
- document.getElementById('noVNC_password_dlg')
+
+ document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
+ document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
+
+ let inputFocus = "none";
+ if (!e.detail.types.includes("username")) {
+ document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
+ } else {
+ inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
+ }
+ if (!e.detail.types.includes("password")) {
+ document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
+ } else {
+ inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
+ }
+ document.getElementById('noVNC_credentials_dlg')
.classList.add('noVNC_open');
setTimeout(() => document
- .getElementById('noVNC_password_input').focus(), 100);
+ .getElementById(inputFocus).focus(), 100);
- Log.Warn("Server asked for a password");
- UI.showStatus(_("Password is required"), "warning");
+ Log.Warn("Server asked for credentials");
+ UI.showStatus(_("Credentials are required"), "warning");
},
- setPassword(e) {
+ setCredentials(e) {
// Prevent actually submitting the form
e.preventDefault();
- const inputElem = document.getElementById('noVNC_password_input');
- const password = inputElem.value;
+ let inputElemUsername = document.getElementById('noVNC_username_input');
+ const username = inputElemUsername.value;
+
+ let inputElemPassword = document.getElementById('noVNC_password_input');
+ const password = inputElemPassword.value;
// Clear the input after reading the password
- inputElem.value = "";
- UI.rfb.sendCredentials({ password: password });
+ inputElemPassword.value = "";
+
+ UI.rfb.sendCredentials({ username: username, password: password });
UI.reconnect_password = password;
- document.getElementById('noVNC_password_dlg')
+ document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open');
},
// Can't be clipping if viewport is scaled to fit
UI.forceSetting('view_clip', false);
UI.rfb.clipViewport = false;
- } else if (isTouchDevice) {
- // Touch devices usually have shit scrollbars
+ } else if (isIOS() || isAndroid()) {
+ // iOS and Android usually have shit scrollbars
UI.forceSetting('view_clip', true);
UI.rfb.clipViewport = true;
} else {
viewDragButton.classList.remove("noVNC_selected");
}
- // Different behaviour for touch vs non-touch
- // The button is disabled instead of hidden on touch devices
- if (isTouchDevice) {
+ if (UI.rfb.clipViewport) {
viewDragButton.classList.remove("noVNC_hidden");
-
- if (UI.rfb.clipViewport) {
- viewDragButton.disabled = false;
- } else {
- viewDragButton.disabled = true;
- }
} else {
- viewDragButton.disabled = false;
-
- if (UI.rfb.clipViewport) {
- viewDragButton.classList.remove("noVNC_hidden");
- } else {
- viewDragButton.classList.add("noVNC_hidden");
- }
+ viewDragButton.classList.add("noVNC_hidden");
}
},
},
sendEsc() {
- UI.rfb.sendKey(KeyTable.XK_Escape, "Escape");
+ UI.sendKey(KeyTable.XK_Escape, "Escape");
},
sendTab() {
- UI.rfb.sendKey(KeyTable.XK_Tab);
+ UI.sendKey(KeyTable.XK_Tab, "Tab");
},
toggleCtrl() {
const btn = document.getElementById('noVNC_toggle_ctrl_button');
if (btn.classList.contains("noVNC_selected")) {
- UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+ UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+ btn.classList.remove("noVNC_selected");
+ } else {
+ UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+ btn.classList.add("noVNC_selected");
+ }
+ },
+
+ toggleWindows() {
+ const btn = document.getElementById('noVNC_toggle_windows_button');
+ if (btn.classList.contains("noVNC_selected")) {
+ UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
btn.classList.remove("noVNC_selected");
} else {
- UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+ UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
btn.classList.add("noVNC_selected");
}
},
toggleAlt() {
const btn = document.getElementById('noVNC_toggle_alt_button');
if (btn.classList.contains("noVNC_selected")) {
- UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+ UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
btn.classList.remove("noVNC_selected");
} else {
- UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+ UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
btn.classList.add("noVNC_selected");
}
},
sendCtrlAltDel() {
UI.rfb.sendCtrlAltDel();
+ // See below
+ UI.rfb.focus();
+ UI.idleControlbar();
+ },
+
+ sendKey(keysym, code, down) {
+ UI.rfb.sendKey(keysym, code, down);
+
+ // Move focus to the screen in order to be able to use the
+ // keyboard right after these extra keys.
+ // The exception is when a virtual keyboard is used, because
+ // if we focus the screen the virtual keyboard would be closed.
+ // In this case we focus our special virtual keyboard input
+ // element instead.
+ if (document.getElementById('noVNC_keyboard_button')
+ .classList.contains("noVNC_selected")) {
+ document.getElementById('noVNC_keyboardinput').focus();
+ } else {
+ UI.rfb.focus();
+ }
+ // fade out the controlbar to highlight that
+ // the focus has been moved to the screen
+ UI.idleControlbar();
},
/* ------^-------
.classList.add('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.add('noVNC_hidden');
+ document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
+ .classList.add('noVNC_hidden');
} else {
document.getElementById('noVNC_keyboard_button')
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden');
+ document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
+ .classList.remove('noVNC_hidden');
}
- UI.setMouseButton(1); //has it's own logic for hiding/showing
},
updateShowDotCursor() {
updateDesktopName(e) {
UI.desktopName = e.detail.name;
// Display the desktop name in the document title
- document.title = e.detail.name + " - noVNC";
+ document.title = e.detail.name + " - " + PAGE_TITLE;
},
bell(e) {
};
// Set up translations
-const LINGUAS = ["cs", "de", "el", "es", "nl", "pl", "sv", "tr", "zh_CN", "zh_TW"];
+const LINGUAS = ["cs", "de", "el", "es", "ja", "ko", "nl", "pl", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS);
-if (l10n.language !== "en" && l10n.dictionary === undefined) {
- WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', (translations) => {
- l10n.dictionary = translations;
-
- // wait for translations to load before loading the UI
- UI.prime();
- }, (err) => {
- Log.Error("Failed to load translations: " + err);
- UI.prime();
- });
-} else {
+if (l10n.language === "en" || l10n.dictionary !== undefined) {
UI.prime();
+} else {
+ WebUtil.fetchJSON('app/locale/' + l10n.language + '.json')
+ .then((translations) => { l10n.dictionary = translations; })
+ .catch(err => Log.Error("Failed to load translations: " + err))
+ .then(UI.prime);
}
export default UI;