From 4885574357eb82136b728647858c4868b54f7d88 Mon Sep 17 00:00:00 2001 From: J Date: Sun, 11 Aug 2024 16:07:55 +0700 Subject: [PATCH] linting ui.js file --- .prettierrc | 9 + app/ui.js | 3420 +++++++++++++++++++++++++-------------------------- 2 files changed, 1680 insertions(+), 1749 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6d43be81 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "printWidth": 120, + "useTabs": true, + "tabWidth": 4, + "generalConfiguration.sortOnBeforeSave": true +} diff --git a/app/ui.js b/app/ui.js index 90dc7189..ce60c192 100644 --- a/app/ui.js +++ b/app/ui.js @@ -8,1761 +8,1683 @@ import * as Log from '../core/util/logging.js'; import _, { l10n } from './localization.js'; -import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari, - hasScrollbarGutter, dragThreshold } - from '../core/util/browser.js'; +import { + isTouchDevice, + isMac, + isIOS, + isAndroid, + isChromeOS, + isSafari, + hasScrollbarGutter, + 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 Keyboard from "../core/input/keyboard.js"; -import RFB from "../core/rfb.js"; -import * as WebUtil from "./webutil.js"; +import KeyTable from '../core/input/keysym.js'; +import keysyms from '../core/input/keysymdef.js'; +import Keyboard from '../core/input/keyboard.js'; +import RFB from '../core/rfb.js'; +import * as WebUtil from './webutil.js'; -const PAGE_TITLE = "noVNC"; +const PAGE_TITLE = 'noVNC'; const UI = { - - connected: false, - desktopName: "", - - statusTimeout: null, - hideKeyboardTimeout: null, - idleControlbarTimeout: null, - closeControlbarTimeout: null, - - controlbarGrabbed: false, - controlbarDrag: false, - controlbarMouseDownClientY: 0, - controlbarMouseDownOffsetY: 0, - - lastKeyboardinput: null, - defaultKeyboardinputLen: 100, - - inhibitReconnect: true, - reconnectCallback: null, - reconnectPassword: null, - - prime() { - return WebUtil.initSettings().then(() => { - if (document.readyState === "interactive" || document.readyState === "complete") { - return UI.start(); - } - - return new Promise((resolve, reject) => { - document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject)); - }); - }); - }, - - // Render default UI and initialize settings menu - start() { - - UI.initSettings(); - - // Translate the DOM - l10n.translateDOM(); - - // We rely on modern APIs which might not be available in an - // insecure context - if (!window.isSecureContext) { - // FIXME: This gets hidden when connecting - UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error'); - } - - // Try to fetch version number - fetch('./package.json') - .then((response) => { - if (!response.ok) { - throw Error("" + response.status + " " + response.statusText); - } - return response.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) { - // Remove the address bar - setTimeout(() => window.scrollTo(0, 1), 100); - } - - // Restore control bar position - if (WebUtil.readSetting('controlbar_pos') === 'right') { - UI.toggleControlbarSide(); - } - - UI.initFullscreen(); - - // Setup event handlers - UI.addControlbarHandlers(); - UI.addTouchSpecificHandlers(); - UI.addExtraKeysHandlers(); - 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'); - - document.documentElement.classList.remove("noVNC_loading"); - - let autoconnect = WebUtil.getConfigVar('autoconnect', false); - if (autoconnect === 'true' || autoconnect == '1') { - autoconnect = true; - UI.connect(); - } else { - autoconnect = false; - // Show the connect panel on first load unless autoconnecting - UI.openConnectPanel(); - } - - 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 (!isSafari() && - (document.documentElement.requestFullscreen || - document.documentElement.mozRequestFullScreen || - document.documentElement.webkitRequestFullscreen || - document.body.msRequestFullscreen)) { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_hidden"); - UI.addFullscreenHandlers(); - } - }, - - initSettings() { - // Logging selection dropdown - const llevels = ['error', 'warn', 'info', 'debug']; - for (let i = 0; i < llevels.length; i += 1) { - UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]); - } - - // Settings with immediate effects - UI.initSetting('logging', 'warn'); - UI.updateLogging(); - - /* Populate the controls if defaults are provided in the URL */ - UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('view_clip', false); - UI.initSetting('resize', 'off'); - UI.initSetting('quality', 6); - UI.initSetting('compression', 2); - UI.initSetting('shared', true); - UI.initSetting('view_only', false); - UI.initSetting('show_dot', false); - UI.initSetting('path', 'websockify'); - UI.initSetting('repeaterID', ''); - UI.initSetting('reconnect', false); - UI.initSetting('reconnect_delay', 5000); - - UI.setupSettingLabels(); - }, - // Adds a link to the label elements on the corresponding input elements - setupSettingLabels() { - const labels = document.getElementsByTagName('LABEL'); - for (let i = 0; i < labels.length; i++) { - const htmlFor = labels[i].htmlFor; - if (htmlFor != '') { - const elem = document.getElementById(htmlFor); - if (elem) elem.label = labels[i]; - } else { - // If 'for' isn't set, use the first input element child - const children = labels[i].children; - for (let j = 0; j < children.length; j++) { - if (children[j].form !== undefined) { - children[j].label = labels[i]; - break; - } - } - } - } - }, - -/* ------^------- -* /INIT -* ============== -* EVENT HANDLERS -* ------v------*/ - - addControlbarHandlers() { - document.getElementById("noVNC_control_bar") - .addEventListener('mousemove', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mouseup', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mousedown', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('keydown', UI.activateControlbar); - - document.getElementById("noVNC_control_bar") - .addEventListener('mousedown', UI.keepControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('keydown', UI.keepControlbar); - - document.getElementById("noVNC_view_drag_button") - .addEventListener('click', UI.toggleViewDrag); - - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mousedown', UI.controlbarHandleMouseDown); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mouseup', UI.controlbarHandleMouseUp); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mousemove', UI.dragControlbarHandle); - // resize events aren't available for elements - window.addEventListener('resize', UI.updateControlbarHandle); - - const exps = document.getElementsByClassName("noVNC_expander"); - for (let i = 0;i < exps.length;i++) { - exps[i].addEventListener('click', UI.toggleExpander); - } - }, - - 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") - .addEventListener('blur', UI.onblurVirtualKeyboard); - document.getElementById("noVNC_keyboardinput") - .addEventListener('submit', () => false); - - document.documentElement - .addEventListener('mousedown', UI.keepVirtualKeyboard, true); - - document.getElementById("noVNC_control_bar") - .addEventListener('touchstart', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchmove', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchend', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('input', UI.activateControlbar); - - document.getElementById("noVNC_control_bar") - .addEventListener('touchstart', UI.keepControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('input', UI.keepControlbar); - - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchstart', UI.controlbarHandleMouseDown); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchend', UI.controlbarHandleMouseUp); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchmove', UI.dragControlbarHandle); - }, - - addExtraKeysHandlers() { - document.getElementById("noVNC_toggle_extra_keys_button") - .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") - .addEventListener('click', UI.sendTab); - document.getElementById("noVNC_send_esc_button") - .addEventListener('click', UI.sendEsc); - document.getElementById("noVNC_send_ctrl_alt_del_button") - .addEventListener('click', UI.sendCtrlAltDel); - }, - - addMachineHandlers() { - 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); - document.getElementById("noVNC_connect_button") - .addEventListener('click', UI.connect); - document.getElementById("noVNC_cancel_reconnect_button") - .addEventListener('click', UI.cancelReconnect); - - document.getElementById("noVNC_approve_server_button") - .addEventListener('click', UI.approveServer); - document.getElementById("noVNC_reject_server_button") - .addEventListener('click', UI.rejectServer); - document.getElementById("noVNC_credentials_button") - .addEventListener('click', UI.setCredentials); - }, - - addClipboardHandlers() { - document.getElementById("noVNC_clipboard_button") - .addEventListener('click', UI.toggleClipboardPanel); - document.getElementById("noVNC_clipboard_text") - .addEventListener('change', UI.clipboardSend); - }, - - // Add a call to save settings when the element changes, - // unless the optional parameter changeFunc is used instead. - addSettingChangeHandler(name, changeFunc) { - const settingElem = document.getElementById("noVNC_setting_" + name); - if (changeFunc === undefined) { - changeFunc = () => UI.saveSetting(name); - } - settingElem.addEventListener('change', changeFunc); - }, - - addSettingsHandlers() { - document.getElementById("noVNC_settings_button") - .addEventListener('click', UI.toggleSettingsPanel); - - UI.addSettingChangeHandler('encrypt'); - UI.addSettingChangeHandler('resize'); - UI.addSettingChangeHandler('resize', UI.applyResizeMode); - UI.addSettingChangeHandler('resize', UI.updateViewClip); - UI.addSettingChangeHandler('quality'); - UI.addSettingChangeHandler('quality', UI.updateQuality); - UI.addSettingChangeHandler('compression'); - UI.addSettingChangeHandler('compression', UI.updateCompression); - UI.addSettingChangeHandler('view_clip'); - UI.addSettingChangeHandler('view_clip', UI.updateViewClip); - UI.addSettingChangeHandler('shared'); - UI.addSettingChangeHandler('view_only'); - UI.addSettingChangeHandler('view_only', UI.updateViewOnly); - UI.addSettingChangeHandler('show_dot'); - UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); - UI.addSettingChangeHandler('host'); - UI.addSettingChangeHandler('port'); - UI.addSettingChangeHandler('path'); - UI.addSettingChangeHandler('repeaterID'); - UI.addSettingChangeHandler('logging'); - UI.addSettingChangeHandler('logging', UI.updateLogging); - UI.addSettingChangeHandler('reconnect'); - UI.addSettingChangeHandler('reconnect_delay'); - }, - - addFullscreenHandlers() { - document.getElementById("noVNC_fullscreen_button") - .addEventListener('click', UI.toggleFullscreen); - - window.addEventListener('fullscreenchange', UI.updateFullscreenButton); - window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); - window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); - window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); - }, - -/* ------^------- - * /EVENT HANDLERS - * ============== - * VISUAL - * ------v------*/ - - // Disable/enable controls depending on connection state - updateVisualState(state) { - - document.documentElement.classList.remove("noVNC_connecting"); - document.documentElement.classList.remove("noVNC_connected"); - document.documentElement.classList.remove("noVNC_disconnecting"); - document.documentElement.classList.remove("noVNC_reconnecting"); - - const transitionElem = document.getElementById("noVNC_transition_text"); - switch (state) { - case 'init': - break; - case 'connecting': - transitionElem.textContent = _("Connecting..."); - document.documentElement.classList.add("noVNC_connecting"); - break; - case 'connected': - document.documentElement.classList.add("noVNC_connected"); - break; - case 'disconnecting': - transitionElem.textContent = _("Disconnecting..."); - document.documentElement.classList.add("noVNC_disconnecting"); - break; - case 'disconnected': - break; - case 'reconnecting': - transitionElem.textContent = _("Reconnecting..."); - document.documentElement.classList.add("noVNC_reconnecting"); - break; - default: - Log.Error("Invalid visual state: " + state); - UI.showStatus(_("Internal error"), 'error'); - return; - } - - if (UI.connected) { - UI.updateViewClip(); - - UI.disableSetting('encrypt'); - UI.disableSetting('shared'); - UI.disableSetting('host'); - UI.disableSetting('port'); - UI.disableSetting('path'); - UI.disableSetting('repeaterID'); - - // Hide the controlbar after 2 seconds - UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); - } else { - UI.enableSetting('encrypt'); - UI.enableSetting('shared'); - UI.enableSetting('host'); - UI.enableSetting('port'); - UI.enableSetting('path'); - UI.enableSetting('repeaterID'); - UI.updatePowerButton(); - UI.keepControlbar(); - } - - // State change closes dialogs as they may not be relevant - // anymore - UI.closeAllPanels(); - document.getElementById('noVNC_verify_server_dlg') - .classList.remove('noVNC_open'); - document.getElementById('noVNC_credentials_dlg') - .classList.remove('noVNC_open'); - }, - - showStatus(text, statusType, time) { - const statusElem = document.getElementById('noVNC_status'); - - if (typeof statusType === 'undefined') { - statusType = 'normal'; - } - - // Don't overwrite more severe visible statuses and never - // errors. Only shows the first error. - if (statusElem.classList.contains("noVNC_open")) { - if (statusElem.classList.contains("noVNC_status_error")) { - return; - } - if (statusElem.classList.contains("noVNC_status_warn") && - statusType === 'normal') { - return; - } - } - - clearTimeout(UI.statusTimeout); - - switch (statusType) { - case 'error': - statusElem.classList.remove("noVNC_status_warn"); - statusElem.classList.remove("noVNC_status_normal"); - statusElem.classList.add("noVNC_status_error"); - break; - case 'warning': - case 'warn': - statusElem.classList.remove("noVNC_status_error"); - statusElem.classList.remove("noVNC_status_normal"); - statusElem.classList.add("noVNC_status_warn"); - break; - case 'normal': - case 'info': - default: - statusElem.classList.remove("noVNC_status_error"); - statusElem.classList.remove("noVNC_status_warn"); - statusElem.classList.add("noVNC_status_normal"); - break; - } - - statusElem.textContent = text; - statusElem.classList.add("noVNC_open"); - - // If no time was specified, show the status for 1.5 seconds - if (typeof time === 'undefined') { - time = 1500; - } - - // Error messages do not timeout - if (statusType !== 'error') { - UI.statusTimeout = window.setTimeout(UI.hideStatus, time); - } - }, - - hideStatus() { - clearTimeout(UI.statusTimeout); - document.getElementById('noVNC_status').classList.remove("noVNC_open"); - }, - - activateControlbar(event) { - clearTimeout(UI.idleControlbarTimeout); - // We manipulate the anchor instead of the actual control - // bar in order to avoid creating new a stacking group - document.getElementById('noVNC_control_bar_anchor') - .classList.remove("noVNC_idle"); - UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); - }, - - idleControlbar() { - // Don't fade if a child of the control bar has focus - if (document.getElementById('noVNC_control_bar') - .contains(document.activeElement) && document.hasFocus()) { - UI.activateControlbar(); - return; - } - - document.getElementById('noVNC_control_bar_anchor') - .classList.add("noVNC_idle"); - }, - - keepControlbar() { - clearTimeout(UI.closeControlbarTimeout); - }, - - openControlbar() { - document.getElementById('noVNC_control_bar') - .classList.add("noVNC_open"); - }, - - closeControlbar() { - UI.closeAllPanels(); - document.getElementById('noVNC_control_bar') - .classList.remove("noVNC_open"); - UI.rfb.focus(); - }, - - toggleControlbar() { - if (document.getElementById('noVNC_control_bar') - .classList.contains("noVNC_open")) { - UI.closeControlbar(); - } else { - UI.openControlbar(); - } - }, - - toggleControlbarSide() { - // Temporarily disable animation, if bar is displayed, to avoid weird - // movement. The transitionend-event will not fire when display=none. - const bar = document.getElementById('noVNC_control_bar'); - const barDisplayStyle = window.getComputedStyle(bar).display; - if (barDisplayStyle !== 'none') { - bar.style.transitionDuration = '0s'; - bar.addEventListener('transitionend', () => bar.style.transitionDuration = ''); - } - - const anchor = document.getElementById('noVNC_control_bar_anchor'); - if (anchor.classList.contains("noVNC_right")) { - WebUtil.writeSetting('controlbar_pos', 'left'); - anchor.classList.remove("noVNC_right"); - } else { - WebUtil.writeSetting('controlbar_pos', 'right'); - anchor.classList.add("noVNC_right"); - } - - // Consider this a movement of the handle - UI.controlbarDrag = true; - - // The user has "followed" hint, let's hide it until the next drag - UI.showControlbarHint(false, false); - }, - - showControlbarHint(show, animate=true) { - const hint = document.getElementById('noVNC_control_bar_hint'); - - if (animate) { - hint.classList.remove("noVNC_notransition"); - } else { - hint.classList.add("noVNC_notransition"); - } - - if (show) { - hint.classList.add("noVNC_active"); - } else { - hint.classList.remove("noVNC_active"); - } - }, - - dragControlbarHandle(e) { - if (!UI.controlbarGrabbed) return; - - const ptr = getPointerEvent(e); - - const anchor = document.getElementById('noVNC_control_bar_anchor'); - if (ptr.clientX < (window.innerWidth * 0.1)) { - if (anchor.classList.contains("noVNC_right")) { - UI.toggleControlbarSide(); - } - } else if (ptr.clientX > (window.innerWidth * 0.9)) { - if (!anchor.classList.contains("noVNC_right")) { - UI.toggleControlbarSide(); - } - } - - if (!UI.controlbarDrag) { - const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); - - if (dragDistance < dragThreshold) return; - - UI.controlbarDrag = true; - } - - const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; - - UI.moveControlbarHandle(eventY); - - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - }, - - // Move the handle but don't allow any position outside the bounds - moveControlbarHandle(viewportRelativeY) { - const handle = document.getElementById("noVNC_control_bar_handle"); - const handleHeight = handle.getBoundingClientRect().height; - const controlbarBounds = document.getElementById("noVNC_control_bar") - .getBoundingClientRect(); - const margin = 10; - - // These heights need to be non-zero for the below logic to work - if (handleHeight === 0 || controlbarBounds.height === 0) { - return; - } - - let newY = viewportRelativeY; - - // Check if the coordinates are outside the control bar - if (newY < controlbarBounds.top + margin) { - // Force coordinates to be below the top of the control bar - newY = controlbarBounds.top + margin; - - } else if (newY > controlbarBounds.top + - controlbarBounds.height - handleHeight - margin) { - // Force coordinates to be above the bottom of the control bar - newY = controlbarBounds.top + - controlbarBounds.height - handleHeight - margin; - } - - // Corner case: control bar too small for stable position - if (controlbarBounds.height < (handleHeight + margin * 2)) { - newY = controlbarBounds.top + - (controlbarBounds.height - handleHeight) / 2; - } - - // The transform needs coordinates that are relative to the parent - const parentRelativeY = newY - controlbarBounds.top; - handle.style.transform = "translateY(" + parentRelativeY + "px)"; - }, - - updateControlbarHandle() { - // Since the control bar is fixed on the viewport and not the page, - // the move function expects coordinates relative the the viewport. - const handle = document.getElementById("noVNC_control_bar_handle"); - const handleBounds = handle.getBoundingClientRect(); - UI.moveControlbarHandle(handleBounds.top); - }, - - controlbarHandleMouseUp(e) { - if ((e.type == "mouseup") && (e.button != 0)) return; - - // mouseup and mousedown on the same place toggles the controlbar - if (UI.controlbarGrabbed && !UI.controlbarDrag) { - UI.toggleControlbar(); - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - } - UI.controlbarGrabbed = false; - UI.showControlbarHint(false); - }, - - controlbarHandleMouseDown(e) { - if ((e.type == "mousedown") && (e.button != 0)) return; - - const ptr = getPointerEvent(e); - - const handle = document.getElementById("noVNC_control_bar_handle"); - const bounds = handle.getBoundingClientRect(); - - // Touch events have implicit capture - if (e.type === "mousedown") { - setCapture(handle); - } - - UI.controlbarGrabbed = true; - UI.controlbarDrag = false; - - UI.showControlbarHint(true); - - UI.controlbarMouseDownClientY = ptr.clientY; - UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - }, - - toggleExpander(e) { - if (this.classList.contains("noVNC_open")) { - this.classList.remove("noVNC_open"); - } else { - this.classList.add("noVNC_open"); - } - }, - -/* ------^------- - * /VISUAL - * ============== - * SETTINGS - * ------v------*/ - - // Initial page load read/initialization of settings - initSetting(name, defVal) { - // Check Query string followed by cookie - let val = WebUtil.getConfigVar(name); - if (val === null) { - val = WebUtil.readSetting(name, defVal); - } - WebUtil.setSetting(name, val); - UI.updateSetting(name); - return val; - }, - - // Set the new value, update and disable form control setting - forceSetting(name, val) { - WebUtil.setSetting(name, val); - UI.updateSetting(name); - UI.disableSetting(name); - }, - - // Update cookie and form control setting. If value is not set, then - // updates from control to current cookie setting. - updateSetting(name) { - - // Update the settings control - let value = UI.getSetting(name); - - const ctrl = document.getElementById('noVNC_setting_' + name); - if (ctrl.type === 'checkbox') { - ctrl.checked = value; - - } else if (typeof ctrl.options !== 'undefined') { - for (let i = 0; i < ctrl.options.length; i += 1) { - if (ctrl.options[i].value === value) { - ctrl.selectedIndex = i; - break; - } - } - } else { - ctrl.value = value; - } - }, - - // Save control setting to cookie - saveSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - let val; - if (ctrl.type === 'checkbox') { - val = ctrl.checked; - } else if (typeof ctrl.options !== 'undefined') { - val = ctrl.options[ctrl.selectedIndex].value; - } else { - val = ctrl.value; - } - WebUtil.writeSetting(name, val); - //Log.Debug("Setting saved '" + name + "=" + val + "'"); - return val; - }, - - // Read form control compatible setting from cookie - getSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - let val = WebUtil.readSetting(name); - if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { - if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) { - val = false; - } else { - val = true; - } - } - return val; - }, - - // These helpers compensate for the lack of parent-selectors and - // previous-sibling-selectors in CSS which are needed when we want to - // disable the labels that belong to disabled input elements. - disableSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = true; - ctrl.label.classList.add('noVNC_disabled'); - }, - - enableSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = false; - ctrl.label.classList.remove('noVNC_disabled'); - }, - -/* ------^------- - * /SETTINGS - * ============== - * PANELS - * ------v------*/ - - closeAllPanels() { - UI.closeSettingsPanel(); - UI.closePowerPanel(); - UI.closeClipboardPanel(); - UI.closeExtraKeys(); - }, - -/* ------^------- - * /PANELS - * ============== - * SETTINGS (panel) - * ------v------*/ - - openSettingsPanel() { - UI.closeAllPanels(); - UI.openControlbar(); - - // Refresh UI elements from saved cookies - UI.updateSetting('encrypt'); - UI.updateSetting('view_clip'); - UI.updateSetting('resize'); - UI.updateSetting('quality'); - UI.updateSetting('compression'); - UI.updateSetting('shared'); - UI.updateSetting('view_only'); - UI.updateSetting('path'); - UI.updateSetting('repeaterID'); - UI.updateSetting('logging'); - UI.updateSetting('reconnect'); - UI.updateSetting('reconnect_delay'); - - document.getElementById('noVNC_settings') - .classList.add("noVNC_open"); - document.getElementById('noVNC_settings_button') - .classList.add("noVNC_selected"); - }, - - closeSettingsPanel() { - document.getElementById('noVNC_settings') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_settings_button') - .classList.remove("noVNC_selected"); - }, - - toggleSettingsPanel() { - if (document.getElementById('noVNC_settings') - .classList.contains("noVNC_open")) { - UI.closeSettingsPanel(); - } else { - UI.openSettingsPanel(); - } - }, - -/* ------^------- - * /SETTINGS - * ============== - * POWER - * ------v------*/ - - openPowerPanel() { - UI.closeAllPanels(); - UI.openControlbar(); - - document.getElementById('noVNC_power') - .classList.add("noVNC_open"); - document.getElementById('noVNC_power_button') - .classList.add("noVNC_selected"); - }, - - closePowerPanel() { - document.getElementById('noVNC_power') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_power_button') - .classList.remove("noVNC_selected"); - }, - - togglePowerPanel() { - if (document.getElementById('noVNC_power') - .classList.contains("noVNC_open")) { - UI.closePowerPanel(); - } else { - UI.openPowerPanel(); - } - }, - - // Disable/enable power button - updatePowerButton() { - if (UI.connected && - UI.rfb.capabilities.power && - !UI.rfb.viewOnly) { - document.getElementById('noVNC_power_button') - .classList.remove("noVNC_hidden"); - } else { - document.getElementById('noVNC_power_button') - .classList.add("noVNC_hidden"); - // Close power panel if open - UI.closePowerPanel(); - } - }, - -/* ------^------- - * /POWER - * ============== - * CLIPBOARD - * ------v------*/ - - openClipboardPanel() { - UI.closeAllPanels(); - UI.openControlbar(); - - document.getElementById('noVNC_clipboard') - .classList.add("noVNC_open"); - document.getElementById('noVNC_clipboard_button') - .classList.add("noVNC_selected"); - }, - - closeClipboardPanel() { - document.getElementById('noVNC_clipboard') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_clipboard_button') - .classList.remove("noVNC_selected"); - }, - - toggleClipboardPanel() { - if (document.getElementById('noVNC_clipboard') - .classList.contains("noVNC_open")) { - UI.closeClipboardPanel(); - } else { - UI.openClipboardPanel(); - } - }, - - clipboardReceive(e) { - Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "..."); - document.getElementById('noVNC_clipboard_text').value = e.detail.text; - Log.Debug("<< UI.clipboardReceive"); - }, - - clipboardSend() { - const text = document.getElementById('noVNC_clipboard_text').value; - Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "..."); - UI.rfb.clipboardPasteFrom(text); - Log.Debug("<< UI.clipboardSend"); - }, - -/* ------^------- - * /CLIPBOARD - * ============== - * 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 - if (typeof UI.rfb !== 'undefined') { - return; - } - - const host = UI.getSetting('host'); - const port = UI.getSetting('port'); - const path = UI.getSetting('path'); - - if (typeof password === 'undefined') { - password = WebUtil.getConfigVar('password'); - UI.reconnectPassword = password; - } - - if (password === null) { - password = undefined; - } - - UI.hideStatus(); - - UI.closeConnectPanel(); - - UI.updateVisualState('connecting'); - - let url; - - if (host) { - url = new URL("https://" + host); - - url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:'; - if (port) { - url.port = port; - } - url.pathname = '/' + path; - } else { - // Current (May 2024) browsers support relative WebSocket - // URLs natively, but we need to support older browsers for - // some time. - url = new URL(path, location.href); - url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:'; - } - - try { - UI.rfb = new RFB(document.getElementById('noVNC_container'), - url.href, - { shared: UI.getSetting('shared'), - repeaterID: UI.getSetting('repeaterID'), - credentials: { password: password } }); - } catch (exc) { - Log.Error("Failed to connect to server: " + exc); - UI.updateVisualState('disconnected'); - UI.showStatus(_("Failed to connect to server: ") + exc, 'error'); - return; - } - - UI.rfb.addEventListener("connect", UI.connectFinished); - UI.rfb.addEventListener("disconnect", UI.disconnectFinished); - UI.rfb.addEventListener("serververification", UI.serverVerify); - UI.rfb.addEventListener("credentialsrequired", UI.credentials); - UI.rfb.addEventListener("securityfailure", UI.securityFailed); - UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag); - UI.rfb.addEventListener("capabilities", UI.updatePowerButton); - UI.rfb.addEventListener("clipboard", UI.clipboardReceive); - UI.rfb.addEventListener("bell", UI.bell); - UI.rfb.addEventListener("desktopname", UI.updateDesktopName); - UI.rfb.clipViewport = UI.getSetting('view_clip'); - UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; - UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; - UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); - UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); - UI.rfb.showDotCursor = UI.getSetting('show_dot'); - - UI.updateViewOnly(); // requires UI.rfb - }, - - disconnect() { - UI.rfb.disconnect(); - - UI.connected = false; - - // Disable automatic reconnecting - UI.inhibitReconnect = true; - - UI.updateVisualState('disconnecting'); - - // Don't display the connection settings until we're actually disconnected - }, - - reconnect() { - UI.reconnectCallback = null; - - // if reconnect has been disabled in the meantime, do nothing. - if (UI.inhibitReconnect) { - return; - } - - UI.connect(null, UI.reconnectPassword); - }, - - cancelReconnect() { - if (UI.reconnectCallback !== null) { - clearTimeout(UI.reconnectCallback); - UI.reconnectCallback = null; - } - - UI.updateVisualState('disconnected'); - - UI.openControlbar(); - UI.openConnectPanel(); - }, - - connectFinished(e) { - UI.connected = true; - UI.inhibitReconnect = false; - - let msg; - if (UI.getSetting('encrypt')) { - msg = _("Connected (encrypted) to ") + UI.desktopName; - } else { - msg = _("Connected (unencrypted) to ") + UI.desktopName; - } - UI.showStatus(msg); - UI.updateVisualState('connected'); - - // Do this last because it can only be used on rendered elements - UI.rfb.focus(); - }, - - disconnectFinished(e) { - const wasConnected = UI.connected; - - // This variable is ideally set when disconnection starts, but - // when the disconnection isn't clean or if it is initiated by - // the server, we need to do it here as well since - // UI.disconnect() won't be used in those cases. - UI.connected = false; - - UI.rfb = undefined; - - if (!e.detail.clean) { - UI.updateVisualState('disconnected'); - if (wasConnected) { - UI.showStatus(_("Something went wrong, connection is closed"), - 'error'); - } else { - UI.showStatus(_("Failed to connect to server"), 'error'); - } - } - // If reconnecting is allowed process it now - if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) { - UI.updateVisualState('reconnecting'); - - const delay = parseInt(UI.getSetting('reconnect_delay')); - UI.reconnectCallback = setTimeout(UI.reconnect, delay); - return; - } else { - UI.updateVisualState('disconnected'); - UI.showStatus(_("Disconnected"), 'normal'); - } - - document.title = PAGE_TITLE; - - UI.openControlbar(); - UI.openConnectPanel(); - }, - - securityFailed(e) { - let msg = ""; - // On security failures we might get a string with a reason - // directly from the server. Note that we can't control if - // this string is translated or not. - if ('reason' in e.detail) { - msg = _("New connection has been rejected with reason: ") + - e.detail.reason; - } else { - msg = _("New connection has been rejected"); - } - UI.showStatus(msg, 'error'); - }, - -/* ------^------- - * /CONNECTION - * ============== - * SERVER VERIFY - * ------v------*/ - - async serverVerify(e) { - const type = e.detail.type; - if (type === 'RSA') { - const publickey = e.detail.publickey; - let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey); - // The same fingerprint format as RealVNC - fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map( - x => x.toString(16).padStart(2, '0')).join('-'); - document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open'); - document.getElementById('noVNC_fingerprint').innerHTML = fingerprint; - } - }, - - approveServer(e) { - e.preventDefault(); - document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); - UI.rfb.approveServer(); - }, - - rejectServer(e) { - e.preventDefault(); - document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); - UI.disconnect(); - }, - -/* ------^------- - * /SERVER VERIFY - * ============== - * PASSWORD - * ------v------*/ - - credentials(e) { - // FIXME: handle more types - - 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.indexOf("username") === -1) { - document.getElementById("noVNC_username_block").classList.add("noVNC_hidden"); - } else { - inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus; - } - if (e.detail.types.indexOf("password") === -1) { - 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(inputFocus).focus(), 100); - - Log.Warn("Server asked for credentials"); - UI.showStatus(_("Credentials are required"), "warning"); - }, - - setCredentials(e) { - // Prevent actually submitting the form - e.preventDefault(); - - 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 - inputElemPassword.value = ""; - - UI.rfb.sendCredentials({ username: username, password: password }); - UI.reconnectPassword = password; - document.getElementById('noVNC_credentials_dlg') - .classList.remove('noVNC_open'); - }, - -/* ------^------- - * /PASSWORD - * ============== - * FULLSCREEN - * ------v------*/ - - toggleFullscreen() { - if (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || // currently working methods - document.webkitFullscreenElement || - document.msFullscreenElement) { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } - } else { - if (document.documentElement.requestFullscreen) { - document.documentElement.requestFullscreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.documentElement.mozRequestFullScreen(); - } else if (document.documentElement.webkitRequestFullscreen) { - document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); - } else if (document.body.msRequestFullscreen) { - document.body.msRequestFullscreen(); - } - } - UI.updateFullscreenButton(); - }, - - updateFullscreenButton() { - if (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || // currently working methods - document.webkitFullscreenElement || - document.msFullscreenElement ) { - document.getElementById('noVNC_fullscreen_button') - .classList.add("noVNC_selected"); - } else { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_selected"); - } - }, - -/* ------^------- - * /FULLSCREEN - * ============== - * RESIZE - * ------v------*/ - - // Apply remote resizing or local scaling - applyResizeMode() { - if (!UI.rfb) return; - - UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; - UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; - }, - -/* ------^------- - * /RESIZE - * ============== - * VIEW CLIPPING - * ------v------*/ - - // Update viewport clipping property for the connection. The normal - // case is to get the value from the setting. There are special cases - // for when the viewport is scaled or when a touch device is used. - updateViewClip() { - if (!UI.rfb) return; - - const scaling = UI.getSetting('resize') === 'scale'; - - // Some platforms have overlay scrollbars that are difficult - // to use in our case, which means we have to force panning - // FIXME: Working scrollbars can still be annoying to use with - // touch, so we should ideally be able to have both - // panning and scrollbars at the same time - - let brokenScrollbars = false; - - if (!hasScrollbarGutter) { - if (isIOS() || isAndroid() || isMac() || isChromeOS()) { - brokenScrollbars = true; - } - } - - if (scaling) { - // Can't be clipping if viewport is scaled to fit - UI.forceSetting('view_clip', false); - UI.rfb.clipViewport = false; - } else if (brokenScrollbars) { - UI.forceSetting('view_clip', true); - UI.rfb.clipViewport = true; - } else { - UI.enableSetting('view_clip'); - UI.rfb.clipViewport = UI.getSetting('view_clip'); - } - - // Changing the viewport may change the state of - // the dragging button - UI.updateViewDrag(); - }, - -/* ------^------- - * /VIEW CLIPPING - * ============== - * VIEWDRAG - * ------v------*/ - - toggleViewDrag() { - if (!UI.rfb) return; - - UI.rfb.dragViewport = !UI.rfb.dragViewport; - UI.updateViewDrag(); - }, - - updateViewDrag() { - if (!UI.connected) return; - - const viewDragButton = document.getElementById('noVNC_view_drag_button'); - - if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && - UI.rfb.dragViewport) { - // We are no longer clipping the viewport. Make sure - // viewport drag isn't active when it can't be used. - UI.rfb.dragViewport = false; - } - - if (UI.rfb.dragViewport) { - viewDragButton.classList.add("noVNC_selected"); - } else { - viewDragButton.classList.remove("noVNC_selected"); - } - - if (UI.rfb.clipViewport) { - viewDragButton.classList.remove("noVNC_hidden"); - } else { - viewDragButton.classList.add("noVNC_hidden"); - } - - viewDragButton.disabled = !UI.rfb.clippingViewport; - }, - -/* ------^------- - * /VIEWDRAG - * ============== - * QUALITY - * ------v------*/ - - updateQuality() { - if (!UI.rfb) return; - - UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); - }, - -/* ------^------- - * /QUALITY - * ============== - * COMPRESSION - * ------v------*/ - - updateCompression() { - if (!UI.rfb) return; - - UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); - }, - -/* ------^------- - * /COMPRESSION - * ============== - * KEYBOARD - * ------v------*/ - - showVirtualKeyboard() { - if (!isTouchDevice) return; - - const input = document.getElementById('noVNC_keyboardinput'); - - if (document.activeElement == input) return; - - input.focus(); - - try { - const l = input.value.length; - // Move the caret to the end - input.setSelectionRange(l, l); - } catch (err) { - // setSelectionRange is undefined in Google Chrome - } - }, - - hideVirtualKeyboard() { - if (!isTouchDevice) return; - - const input = document.getElementById('noVNC_keyboardinput'); - - if (document.activeElement != input) return; - - input.blur(); - }, - - toggleVirtualKeyboard() { - if (document.getElementById('noVNC_keyboard_button') - .classList.contains("noVNC_selected")) { - UI.hideVirtualKeyboard(); - } else { - UI.showVirtualKeyboard(); - } - }, - - onfocusVirtualKeyboard(event) { - document.getElementById('noVNC_keyboard_button') - .classList.add("noVNC_selected"); - if (UI.rfb) { - UI.rfb.focusOnClick = false; - } - }, - - onblurVirtualKeyboard(event) { - document.getElementById('noVNC_keyboard_button') - .classList.remove("noVNC_selected"); - if (UI.rfb) { - UI.rfb.focusOnClick = true; - } - }, - - keepVirtualKeyboard(event) { - const input = document.getElementById('noVNC_keyboardinput'); - - // Only prevent focus change if the virtual keyboard is active - if (document.activeElement != input) { - return; - } - - // Only allow focus to move to other elements that need - // focus to function properly - if (event.target.form !== undefined) { - switch (event.target.type) { - case 'text': - case 'email': - case 'search': - case 'password': - case 'tel': - case 'url': - case 'textarea': - case 'select-one': - case 'select-multiple': - return; - } - } - - 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 - * ============== - * EXTRA KEYS - * ------v------*/ - - openExtraKeys() { - UI.closeAllPanels(); - UI.openControlbar(); - - document.getElementById('noVNC_modifiers') - .classList.add("noVNC_open"); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.add("noVNC_selected"); - }, - - closeExtraKeys() { - document.getElementById('noVNC_modifiers') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.remove("noVNC_selected"); - }, - - toggleExtraKeys() { - if (document.getElementById('noVNC_modifiers') - .classList.contains("noVNC_open")) { - UI.closeExtraKeys(); - } else { - UI.openExtraKeys(); - } - }, - - sendEsc() { - UI.sendKey(KeyTable.XK_Escape, "Escape"); - }, - - sendTab() { - UI.sendKey(KeyTable.XK_Tab, "Tab"); - }, - - toggleCtrl() { - const btn = document.getElementById('noVNC_toggle_ctrl_button'); - if (btn.classList.contains("noVNC_selected")) { - 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.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.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); - btn.classList.remove("noVNC_selected"); - } else { - 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(); - }, - -/* ------^------- - * /EXTRA KEYS - * ============== - * MISC - * ------v------*/ - - updateViewOnly() { - if (!UI.rfb) return; - UI.rfb.viewOnly = UI.getSetting('view_only'); - - // 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'); - } 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'); - } - }, - - updateShowDotCursor() { - if (!UI.rfb) return; - UI.rfb.showDotCursor = UI.getSetting('show_dot'); - }, - - updateLogging() { - WebUtil.initLogging(UI.getSetting('logging')); - }, - - updateDesktopName(e) { - UI.desktopName = e.detail.name; - // Display the desktop name in the document title - document.title = e.detail.name + " - " + PAGE_TITLE; - }, - - bell(e) { - if (WebUtil.getConfigVar('bell', 'on') === 'on') { - const promise = document.getElementById('noVNC_bell').play(); - // The standards disagree on the return value here - if (promise) { - promise.catch((e) => { - if (e.name === "NotAllowedError") { - // Ignore when the browser doesn't let us play audio. - // It is common that the browsers require audio to be - // initiated from a user action. - } else { - Log.Error("Unable to play bell: " + e); - } - }); - } - } - }, - - //Helper to add options to dropdown. - addOption(selectbox, text, value) { - const optn = document.createElement("OPTION"); - optn.text = text; - optn.value = value; - selectbox.options.add(optn); - }, - -/* ------^------- - * /MISC - * ============== - */ + connected: false, + desktopName: '', + + statusTimeout: null, + hideKeyboardTimeout: null, + idleControlbarTimeout: null, + closeControlbarTimeout: null, + + controlbarGrabbed: false, + controlbarDrag: false, + controlbarMouseDownClientY: 0, + controlbarMouseDownOffsetY: 0, + + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + + inhibitReconnect: true, + reconnectCallback: null, + reconnectPassword: null, + + prime() { + return WebUtil.initSettings().then(() => { + if (document.readyState === 'interactive' || document.readyState === 'complete') { + return UI.start(); + } + + return new Promise((resolve, reject) => { + document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject)); + }); + }); + }, + + // Render default UI and initialize settings menu + start() { + UI.initSettings(); + + // Translate the DOM + l10n.translateDOM(); + + // We rely on modern APIs which might not be available in an + // insecure context + if (!window.isSecureContext) { + // FIXME: This gets hidden when connecting + UI.showStatus(_('Running without HTTPS is not recommended, crashes or other issues are likely.'), 'error'); + } + + // Try to fetch version number + fetch('./package.json') + .then((response) => { + if (!response.ok) { + throw Error('' + response.status + ' ' + response.statusText); + } + return response.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) { + // Remove the address bar + setTimeout(() => window.scrollTo(0, 1), 100); + } + + // Restore control bar position + if (WebUtil.readSetting('controlbar_pos') === 'right') { + UI.toggleControlbarSide(); + } + + UI.initFullscreen(); + + // Setup event handlers + UI.addControlbarHandlers(); + UI.addTouchSpecificHandlers(); + UI.addExtraKeysHandlers(); + 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'); + + document.documentElement.classList.remove('noVNC_loading'); + + let autoconnect = WebUtil.getConfigVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + // Show the connect panel on first load unless autoconnecting + UI.openConnectPanel(); + } + + 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 ( + !isSafari() && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen) + ) { + document.getElementById('noVNC_fullscreen_button').classList.remove('noVNC_hidden'); + UI.addFullscreenHandlers(); + } + }, + + initSettings() { + // Logging selection dropdown + const llevels = ['error', 'warn', 'info', 'debug']; + for (let i = 0; i < llevels.length; i += 1) { + UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + UI.updateLogging(); + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('encrypt', window.location.protocol === 'https:'); + UI.initSetting('view_clip', false); + UI.initSetting('resize', 'off'); + UI.initSetting('quality', 6); + UI.initSetting('compression', 2); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('show_dot', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + UI.initSetting('reconnect', false); + UI.initSetting('reconnect_delay', 5000); + + UI.setupSettingLabels(); + }, + // Adds a link to the label elements on the corresponding input elements + setupSettingLabels() { + const labels = document.getElementsByTagName('LABEL'); + for (let i = 0; i < labels.length; i++) { + const htmlFor = labels[i].htmlFor; + if (htmlFor != '') { + const elem = document.getElementById(htmlFor); + if (elem) elem.label = labels[i]; + } else { + // If 'for' isn't set, use the first input element child + const children = labels[i].children; + for (let j = 0; j < children.length; j++) { + if (children[j].form !== undefined) { + children[j].label = labels[i]; + break; + } + } + } + } + }, + + /* ------^------- + * /INIT + * ============== + * EVENT HANDLERS + * ------v------*/ + + addControlbarHandlers() { + document.getElementById('noVNC_control_bar').addEventListener('mousemove', UI.activateControlbar); + document.getElementById('noVNC_control_bar').addEventListener('mouseup', UI.activateControlbar); + document.getElementById('noVNC_control_bar').addEventListener('mousedown', UI.activateControlbar); + document.getElementById('noVNC_control_bar').addEventListener('keydown', UI.activateControlbar); + + document.getElementById('noVNC_control_bar').addEventListener('mousedown', UI.keepControlbar); + document.getElementById('noVNC_control_bar').addEventListener('keydown', UI.keepControlbar); + + document.getElementById('noVNC_view_drag_button').addEventListener('click', UI.toggleViewDrag); + + document.getElementById('noVNC_control_bar_handle').addEventListener('mousedown', UI.controlbarHandleMouseDown); + document.getElementById('noVNC_control_bar_handle').addEventListener('mouseup', UI.controlbarHandleMouseUp); + document.getElementById('noVNC_control_bar_handle').addEventListener('mousemove', UI.dragControlbarHandle); + // resize events aren't available for elements + window.addEventListener('resize', UI.updateControlbarHandle); + + const exps = document.getElementsByClassName('noVNC_expander'); + for (let i = 0; i < exps.length; i++) { + exps[i].addEventListener('click', UI.toggleExpander); + } + }, + + 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').addEventListener('blur', UI.onblurVirtualKeyboard); + document.getElementById('noVNC_keyboardinput').addEventListener('submit', () => false); + + document.documentElement.addEventListener('mousedown', UI.keepVirtualKeyboard, true); + + document.getElementById('noVNC_control_bar').addEventListener('touchstart', UI.activateControlbar); + document.getElementById('noVNC_control_bar').addEventListener('touchmove', UI.activateControlbar); + document.getElementById('noVNC_control_bar').addEventListener('touchend', UI.activateControlbar); + document.getElementById('noVNC_control_bar').addEventListener('input', UI.activateControlbar); + + document.getElementById('noVNC_control_bar').addEventListener('touchstart', UI.keepControlbar); + document.getElementById('noVNC_control_bar').addEventListener('input', UI.keepControlbar); + + document + .getElementById('noVNC_control_bar_handle') + .addEventListener('touchstart', UI.controlbarHandleMouseDown); + document.getElementById('noVNC_control_bar_handle').addEventListener('touchend', UI.controlbarHandleMouseUp); + document.getElementById('noVNC_control_bar_handle').addEventListener('touchmove', UI.dragControlbarHandle); + }, + + addExtraKeysHandlers() { + document.getElementById('noVNC_toggle_extra_keys_button').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').addEventListener('click', UI.sendTab); + document.getElementById('noVNC_send_esc_button').addEventListener('click', UI.sendEsc); + document.getElementById('noVNC_send_ctrl_alt_del_button').addEventListener('click', UI.sendCtrlAltDel); + }, + + addMachineHandlers() { + 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); + document.getElementById('noVNC_connect_button').addEventListener('click', UI.connect); + document.getElementById('noVNC_cancel_reconnect_button').addEventListener('click', UI.cancelReconnect); + + document.getElementById('noVNC_approve_server_button').addEventListener('click', UI.approveServer); + document.getElementById('noVNC_reject_server_button').addEventListener('click', UI.rejectServer); + document.getElementById('noVNC_credentials_button').addEventListener('click', UI.setCredentials); + }, + + addClipboardHandlers() { + document.getElementById('noVNC_clipboard_button').addEventListener('click', UI.toggleClipboardPanel); + document.getElementById('noVNC_clipboard_text').addEventListener('change', UI.clipboardSend); + }, + + // Add a call to save settings when the element changes, + // unless the optional parameter changeFunc is used instead. + addSettingChangeHandler(name, changeFunc) { + const settingElem = document.getElementById('noVNC_setting_' + name); + if (changeFunc === undefined) { + changeFunc = () => UI.saveSetting(name); + } + settingElem.addEventListener('change', changeFunc); + }, + + addSettingsHandlers() { + document.getElementById('noVNC_settings_button').addEventListener('click', UI.toggleSettingsPanel); + + UI.addSettingChangeHandler('encrypt'); + UI.addSettingChangeHandler('resize'); + UI.addSettingChangeHandler('resize', UI.applyResizeMode); + UI.addSettingChangeHandler('resize', UI.updateViewClip); + UI.addSettingChangeHandler('quality'); + UI.addSettingChangeHandler('quality', UI.updateQuality); + UI.addSettingChangeHandler('compression'); + UI.addSettingChangeHandler('compression', UI.updateCompression); + UI.addSettingChangeHandler('view_clip'); + UI.addSettingChangeHandler('view_clip', UI.updateViewClip); + UI.addSettingChangeHandler('shared'); + UI.addSettingChangeHandler('view_only'); + UI.addSettingChangeHandler('view_only', UI.updateViewOnly); + UI.addSettingChangeHandler('show_dot'); + UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); + UI.addSettingChangeHandler('host'); + UI.addSettingChangeHandler('port'); + UI.addSettingChangeHandler('path'); + UI.addSettingChangeHandler('repeaterID'); + UI.addSettingChangeHandler('logging'); + UI.addSettingChangeHandler('logging', UI.updateLogging); + UI.addSettingChangeHandler('reconnect'); + UI.addSettingChangeHandler('reconnect_delay'); + }, + + addFullscreenHandlers() { + document.getElementById('noVNC_fullscreen_button').addEventListener('click', UI.toggleFullscreen); + + window.addEventListener('fullscreenchange', UI.updateFullscreenButton); + window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); + }, + + /* ------^------- + * /EVENT HANDLERS + * ============== + * VISUAL + * ------v------*/ + + // Disable/enable controls depending on connection state + updateVisualState(state) { + document.documentElement.classList.remove('noVNC_connecting'); + document.documentElement.classList.remove('noVNC_connected'); + document.documentElement.classList.remove('noVNC_disconnecting'); + document.documentElement.classList.remove('noVNC_reconnecting'); + + const transitionElem = document.getElementById('noVNC_transition_text'); + switch (state) { + case 'init': + break; + case 'connecting': + transitionElem.textContent = _('Connecting...'); + document.documentElement.classList.add('noVNC_connecting'); + break; + case 'connected': + document.documentElement.classList.add('noVNC_connected'); + break; + case 'disconnecting': + transitionElem.textContent = _('Disconnecting...'); + document.documentElement.classList.add('noVNC_disconnecting'); + break; + case 'disconnected': + break; + case 'reconnecting': + transitionElem.textContent = _('Reconnecting...'); + document.documentElement.classList.add('noVNC_reconnecting'); + break; + default: + Log.Error('Invalid visual state: ' + state); + UI.showStatus(_('Internal error'), 'error'); + return; + } + + if (UI.connected) { + UI.updateViewClip(); + + UI.disableSetting('encrypt'); + UI.disableSetting('shared'); + UI.disableSetting('host'); + UI.disableSetting('port'); + UI.disableSetting('path'); + UI.disableSetting('repeaterID'); + + // Hide the controlbar after 2 seconds + UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); + } else { + UI.enableSetting('encrypt'); + UI.enableSetting('shared'); + UI.enableSetting('host'); + UI.enableSetting('port'); + UI.enableSetting('path'); + UI.enableSetting('repeaterID'); + UI.updatePowerButton(); + UI.keepControlbar(); + } + + // State change closes dialogs as they may not be relevant + // anymore + UI.closeAllPanels(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + document.getElementById('noVNC_credentials_dlg').classList.remove('noVNC_open'); + }, + + showStatus(text, statusType, time) { + const statusElem = document.getElementById('noVNC_status'); + + if (typeof statusType === 'undefined') { + statusType = 'normal'; + } + + // Don't overwrite more severe visible statuses and never + // errors. Only shows the first error. + if (statusElem.classList.contains('noVNC_open')) { + if (statusElem.classList.contains('noVNC_status_error')) { + return; + } + if (statusElem.classList.contains('noVNC_status_warn') && statusType === 'normal') { + return; + } + } + + clearTimeout(UI.statusTimeout); + + switch (statusType) { + case 'error': + statusElem.classList.remove('noVNC_status_warn'); + statusElem.classList.remove('noVNC_status_normal'); + statusElem.classList.add('noVNC_status_error'); + break; + case 'warning': + case 'warn': + statusElem.classList.remove('noVNC_status_error'); + statusElem.classList.remove('noVNC_status_normal'); + statusElem.classList.add('noVNC_status_warn'); + break; + case 'normal': + case 'info': + default: + statusElem.classList.remove('noVNC_status_error'); + statusElem.classList.remove('noVNC_status_warn'); + statusElem.classList.add('noVNC_status_normal'); + break; + } + + statusElem.textContent = text; + statusElem.classList.add('noVNC_open'); + + // If no time was specified, show the status for 1.5 seconds + if (typeof time === 'undefined') { + time = 1500; + } + + // Error messages do not timeout + if (statusType !== 'error') { + UI.statusTimeout = window.setTimeout(UI.hideStatus, time); + } + }, + + hideStatus() { + clearTimeout(UI.statusTimeout); + document.getElementById('noVNC_status').classList.remove('noVNC_open'); + }, + + activateControlbar(event) { + clearTimeout(UI.idleControlbarTimeout); + // We manipulate the anchor instead of the actual control + // bar in order to avoid creating new a stacking group + document.getElementById('noVNC_control_bar_anchor').classList.remove('noVNC_idle'); + UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); + }, + + idleControlbar() { + // Don't fade if a child of the control bar has focus + if (document.getElementById('noVNC_control_bar').contains(document.activeElement) && document.hasFocus()) { + UI.activateControlbar(); + return; + } + + document.getElementById('noVNC_control_bar_anchor').classList.add('noVNC_idle'); + }, + + keepControlbar() { + clearTimeout(UI.closeControlbarTimeout); + }, + + openControlbar() { + document.getElementById('noVNC_control_bar').classList.add('noVNC_open'); + }, + + closeControlbar() { + UI.closeAllPanels(); + document.getElementById('noVNC_control_bar').classList.remove('noVNC_open'); + UI.rfb.focus(); + }, + + toggleControlbar() { + if (document.getElementById('noVNC_control_bar').classList.contains('noVNC_open')) { + UI.closeControlbar(); + } else { + UI.openControlbar(); + } + }, + + toggleControlbarSide() { + // Temporarily disable animation, if bar is displayed, to avoid weird + // movement. The transitionend-event will not fire when display=none. + const bar = document.getElementById('noVNC_control_bar'); + const barDisplayStyle = window.getComputedStyle(bar).display; + if (barDisplayStyle !== 'none') { + bar.style.transitionDuration = '0s'; + bar.addEventListener('transitionend', () => (bar.style.transitionDuration = '')); + } + + const anchor = document.getElementById('noVNC_control_bar_anchor'); + if (anchor.classList.contains('noVNC_right')) { + WebUtil.writeSetting('controlbar_pos', 'left'); + anchor.classList.remove('noVNC_right'); + } else { + WebUtil.writeSetting('controlbar_pos', 'right'); + anchor.classList.add('noVNC_right'); + } + + // Consider this a movement of the handle + UI.controlbarDrag = true; + + // The user has "followed" hint, let's hide it until the next drag + UI.showControlbarHint(false, false); + }, + + showControlbarHint(show, animate = true) { + const hint = document.getElementById('noVNC_control_bar_hint'); + + if (animate) { + hint.classList.remove('noVNC_notransition'); + } else { + hint.classList.add('noVNC_notransition'); + } + + if (show) { + hint.classList.add('noVNC_active'); + } else { + hint.classList.remove('noVNC_active'); + } + }, + + dragControlbarHandle(e) { + if (!UI.controlbarGrabbed) return; + + const ptr = getPointerEvent(e); + + const anchor = document.getElementById('noVNC_control_bar_anchor'); + if (ptr.clientX < window.innerWidth * 0.1) { + if (anchor.classList.contains('noVNC_right')) { + UI.toggleControlbarSide(); + } + } else if (ptr.clientX > window.innerWidth * 0.9) { + if (!anchor.classList.contains('noVNC_right')) { + UI.toggleControlbarSide(); + } + } + + if (!UI.controlbarDrag) { + const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); + + if (dragDistance < dragThreshold) return; + + UI.controlbarDrag = true; + } + + const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; + + UI.moveControlbarHandle(eventY); + + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, + + // Move the handle but don't allow any position outside the bounds + moveControlbarHandle(viewportRelativeY) { + const handle = document.getElementById('noVNC_control_bar_handle'); + const handleHeight = handle.getBoundingClientRect().height; + const controlbarBounds = document.getElementById('noVNC_control_bar').getBoundingClientRect(); + const margin = 10; + + // These heights need to be non-zero for the below logic to work + if (handleHeight === 0 || controlbarBounds.height === 0) { + return; + } + + let newY = viewportRelativeY; + + // Check if the coordinates are outside the control bar + if (newY < controlbarBounds.top + margin) { + // Force coordinates to be below the top of the control bar + newY = controlbarBounds.top + margin; + } else if (newY > controlbarBounds.top + controlbarBounds.height - handleHeight - margin) { + // Force coordinates to be above the bottom of the control bar + newY = controlbarBounds.top + controlbarBounds.height - handleHeight - margin; + } + + // Corner case: control bar too small for stable position + if (controlbarBounds.height < handleHeight + margin * 2) { + newY = controlbarBounds.top + (controlbarBounds.height - handleHeight) / 2; + } + + // The transform needs coordinates that are relative to the parent + const parentRelativeY = newY - controlbarBounds.top; + handle.style.transform = 'translateY(' + parentRelativeY + 'px)'; + }, + + updateControlbarHandle() { + // Since the control bar is fixed on the viewport and not the page, + // the move function expects coordinates relative the the viewport. + const handle = document.getElementById('noVNC_control_bar_handle'); + const handleBounds = handle.getBoundingClientRect(); + UI.moveControlbarHandle(handleBounds.top); + }, + + controlbarHandleMouseUp(e) { + if (e.type == 'mouseup' && e.button != 0) return; + + // mouseup and mousedown on the same place toggles the controlbar + if (UI.controlbarGrabbed && !UI.controlbarDrag) { + UI.toggleControlbar(); + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + } + UI.controlbarGrabbed = false; + UI.showControlbarHint(false); + }, + + controlbarHandleMouseDown(e) { + if (e.type == 'mousedown' && e.button != 0) return; + + const ptr = getPointerEvent(e); + + const handle = document.getElementById('noVNC_control_bar_handle'); + const bounds = handle.getBoundingClientRect(); + + // Touch events have implicit capture + if (e.type === 'mousedown') { + setCapture(handle); + } + + UI.controlbarGrabbed = true; + UI.controlbarDrag = false; + + UI.showControlbarHint(true); + + UI.controlbarMouseDownClientY = ptr.clientY; + UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, + + toggleExpander(e) { + if (this.classList.contains('noVNC_open')) { + this.classList.remove('noVNC_open'); + } else { + this.classList.add('noVNC_open'); + } + }, + + /* ------^------- + * /VISUAL + * ============== + * SETTINGS + * ------v------*/ + + // Initial page load read/initialization of settings + initSetting(name, defVal) { + // Check Query string followed by cookie + let val = WebUtil.getConfigVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + WebUtil.setSetting(name, val); + UI.updateSetting(name); + return val; + }, + + // Set the new value, update and disable form control setting + forceSetting(name, val) { + WebUtil.setSetting(name, val); + UI.updateSetting(name); + UI.disableSetting(name); + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting(name) { + // Update the settings control + let value = UI.getSetting(name); + + const ctrl = document.getElementById('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + } else if (typeof ctrl.options !== 'undefined') { + for (let i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + let val; + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Log.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Read form control compatible setting from cookie + getSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + let val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in { 0: 1, no: 1, false: 1 }) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // These helpers compensate for the lack of parent-selectors and + // previous-sibling-selectors in CSS which are needed when we want to + // disable the labels that belong to disabled input elements. + disableSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = true; + ctrl.label.classList.add('noVNC_disabled'); + }, + + enableSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = false; + ctrl.label.classList.remove('noVNC_disabled'); + }, + + /* ------^------- + * /SETTINGS + * ============== + * PANELS + * ------v------*/ + + closeAllPanels() { + UI.closeSettingsPanel(); + UI.closePowerPanel(); + UI.closeClipboardPanel(); + UI.closeExtraKeys(); + }, + + /* ------^------- + * /PANELS + * ============== + * SETTINGS (panel) + * ------v------*/ + + openSettingsPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + // Refresh UI elements from saved cookies + UI.updateSetting('encrypt'); + UI.updateSetting('view_clip'); + UI.updateSetting('resize'); + UI.updateSetting('quality'); + UI.updateSetting('compression'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('logging'); + UI.updateSetting('reconnect'); + UI.updateSetting('reconnect_delay'); + + document.getElementById('noVNC_settings').classList.add('noVNC_open'); + document.getElementById('noVNC_settings_button').classList.add('noVNC_selected'); + }, + + closeSettingsPanel() { + document.getElementById('noVNC_settings').classList.remove('noVNC_open'); + document.getElementById('noVNC_settings_button').classList.remove('noVNC_selected'); + }, + + toggleSettingsPanel() { + if (document.getElementById('noVNC_settings').classList.contains('noVNC_open')) { + UI.closeSettingsPanel(); + } else { + UI.openSettingsPanel(); + } + }, + + /* ------^------- + * /SETTINGS + * ============== + * POWER + * ------v------*/ + + openPowerPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_power').classList.add('noVNC_open'); + document.getElementById('noVNC_power_button').classList.add('noVNC_selected'); + }, + + closePowerPanel() { + document.getElementById('noVNC_power').classList.remove('noVNC_open'); + document.getElementById('noVNC_power_button').classList.remove('noVNC_selected'); + }, + + togglePowerPanel() { + if (document.getElementById('noVNC_power').classList.contains('noVNC_open')) { + UI.closePowerPanel(); + } else { + UI.openPowerPanel(); + } + }, + + // Disable/enable power button + updatePowerButton() { + if (UI.connected && UI.rfb.capabilities.power && !UI.rfb.viewOnly) { + document.getElementById('noVNC_power_button').classList.remove('noVNC_hidden'); + } else { + document.getElementById('noVNC_power_button').classList.add('noVNC_hidden'); + // Close power panel if open + UI.closePowerPanel(); + } + }, + + /* ------^------- + * /POWER + * ============== + * CLIPBOARD + * ------v------*/ + + openClipboardPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_clipboard').classList.add('noVNC_open'); + document.getElementById('noVNC_clipboard_button').classList.add('noVNC_selected'); + }, + + closeClipboardPanel() { + document.getElementById('noVNC_clipboard').classList.remove('noVNC_open'); + document.getElementById('noVNC_clipboard_button').classList.remove('noVNC_selected'); + }, + + toggleClipboardPanel() { + if (document.getElementById('noVNC_clipboard').classList.contains('noVNC_open')) { + UI.closeClipboardPanel(); + } else { + UI.openClipboardPanel(); + } + }, + + clipboardReceive(e) { + Log.Debug('>> UI.clipboardReceive: ' + e.detail.text.substr(0, 40) + '...'); + document.getElementById('noVNC_clipboard_text').value = e.detail.text; + Log.Debug('<< UI.clipboardReceive'); + }, + + clipboardSend() { + const text = document.getElementById('noVNC_clipboard_text').value; + Log.Debug('>> UI.clipboardSend: ' + text.substr(0, 40) + '...'); + UI.rfb.clipboardPasteFrom(text); + Log.Debug('<< UI.clipboardSend'); + }, + + /* ------^------- + * /CLIPBOARD + * ============== + * 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 + if (typeof UI.rfb !== 'undefined') { + return; + } + + const host = UI.getSetting('host'); + const port = UI.getSetting('port'); + const path = UI.getSetting('path'); + + if (typeof password === 'undefined') { + password = WebUtil.getConfigVar('password'); + UI.reconnectPassword = password; + } + + if (password === null) { + password = undefined; + } + + UI.hideStatus(); + + UI.closeConnectPanel(); + + UI.updateVisualState('connecting'); + + let url; + + if (host) { + url = new URL('https://' + host); + + url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:'; + if (port) { + url.port = port; + } + url.pathname = '/' + path; + } else { + // Current (May 2024) browsers support relative WebSocket + // URLs natively, but we need to support older browsers for + // some time. + url = new URL(path, location.href); + url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + } + + try { + UI.rfb = new RFB(document.getElementById('noVNC_container'), url.href, { + shared: UI.getSetting('shared'), + repeaterID: UI.getSetting('repeaterID'), + credentials: { password: password }, + }); + } catch (exc) { + Log.Error('Failed to connect to server: ' + exc); + UI.updateVisualState('disconnected'); + UI.showStatus(_('Failed to connect to server: ') + exc, 'error'); + return; + } + + UI.rfb.addEventListener('connect', UI.connectFinished); + UI.rfb.addEventListener('disconnect', UI.disconnectFinished); + UI.rfb.addEventListener('serververification', UI.serverVerify); + UI.rfb.addEventListener('credentialsrequired', UI.credentials); + UI.rfb.addEventListener('securityfailure', UI.securityFailed); + UI.rfb.addEventListener('clippingviewport', UI.updateViewDrag); + UI.rfb.addEventListener('capabilities', UI.updatePowerButton); + UI.rfb.addEventListener('clipboard', UI.clipboardReceive); + UI.rfb.addEventListener('bell', UI.bell); + UI.rfb.addEventListener('desktopname', UI.updateDesktopName); + UI.rfb.clipViewport = UI.getSetting('view_clip'); + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); + UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); + UI.rfb.showDotCursor = UI.getSetting('show_dot'); + + UI.updateViewOnly(); // requires UI.rfb + }, + + disconnect() { + UI.rfb.disconnect(); + + UI.connected = false; + + // Disable automatic reconnecting + UI.inhibitReconnect = true; + + UI.updateVisualState('disconnecting'); + + // Don't display the connection settings until we're actually disconnected + }, + + reconnect() { + UI.reconnectCallback = null; + + // if reconnect has been disabled in the meantime, do nothing. + if (UI.inhibitReconnect) { + return; + } + + UI.connect(null, UI.reconnectPassword); + }, + + cancelReconnect() { + if (UI.reconnectCallback !== null) { + clearTimeout(UI.reconnectCallback); + UI.reconnectCallback = null; + } + + UI.updateVisualState('disconnected'); + + UI.openControlbar(); + UI.openConnectPanel(); + }, + + connectFinished(e) { + UI.connected = true; + UI.inhibitReconnect = false; + + let msg; + if (UI.getSetting('encrypt')) { + msg = _('Connected (encrypted) to ') + UI.desktopName; + } else { + msg = _('Connected (unencrypted) to ') + UI.desktopName; + } + UI.showStatus(msg); + UI.updateVisualState('connected'); + + // Do this last because it can only be used on rendered elements + UI.rfb.focus(); + }, + + disconnectFinished(e) { + const wasConnected = UI.connected; + + // This variable is ideally set when disconnection starts, but + // when the disconnection isn't clean or if it is initiated by + // the server, we need to do it here as well since + // UI.disconnect() won't be used in those cases. + UI.connected = false; + + UI.rfb = undefined; + + if (!e.detail.clean) { + UI.updateVisualState('disconnected'); + if (wasConnected) { + UI.showStatus(_('Something went wrong, connection is closed'), 'error'); + } else { + UI.showStatus(_('Failed to connect to server'), 'error'); + } + } + // If reconnecting is allowed process it now + if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) { + UI.updateVisualState('reconnecting'); + + const delay = parseInt(UI.getSetting('reconnect_delay')); + UI.reconnectCallback = setTimeout(UI.reconnect, delay); + return; + } else { + UI.updateVisualState('disconnected'); + UI.showStatus(_('Disconnected'), 'normal'); + } + + document.title = PAGE_TITLE; + + UI.openControlbar(); + UI.openConnectPanel(); + }, + + securityFailed(e) { + let msg = ''; + // On security failures we might get a string with a reason + // directly from the server. Note that we can't control if + // this string is translated or not. + if ('reason' in e.detail) { + msg = _('New connection has been rejected with reason: ') + e.detail.reason; + } else { + msg = _('New connection has been rejected'); + } + UI.showStatus(msg, 'error'); + }, + + /* ------^------- + * /CONNECTION + * ============== + * SERVER VERIFY + * ------v------*/ + + async serverVerify(e) { + const type = e.detail.type; + if (type === 'RSA') { + const publickey = e.detail.publickey; + let fingerprint = await window.crypto.subtle.digest('SHA-1', publickey); + // The same fingerprint format as RealVNC + fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)) + .map((x) => x.toString(16).padStart(2, '0')) + .join('-'); + document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open'); + document.getElementById('noVNC_fingerprint').innerHTML = fingerprint; + } + }, + + approveServer(e) { + e.preventDefault(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + UI.rfb.approveServer(); + }, + + rejectServer(e) { + e.preventDefault(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + UI.disconnect(); + }, + + /* ------^------- + * /SERVER VERIFY + * ============== + * PASSWORD + * ------v------*/ + + credentials(e) { + // FIXME: handle more types + + 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.indexOf('username') === -1) { + document.getElementById('noVNC_username_block').classList.add('noVNC_hidden'); + } else { + inputFocus = inputFocus === 'none' ? 'noVNC_username_input' : inputFocus; + } + if (e.detail.types.indexOf('password') === -1) { + 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(inputFocus).focus(), 100); + + Log.Warn('Server asked for credentials'); + UI.showStatus(_('Credentials are required'), 'warning'); + }, + + setCredentials(e) { + // Prevent actually submitting the form + e.preventDefault(); + + 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 + inputElemPassword.value = ''; + + UI.rfb.sendCredentials({ username: username, password: password }); + UI.reconnectPassword = password; + document.getElementById('noVNC_credentials_dlg').classList.remove('noVNC_open'); + }, + + /* ------^------- + * /PASSWORD + * ============== + * FULLSCREEN + * ------v------*/ + + toggleFullscreen() { + if ( + document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement + ) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + } + UI.updateFullscreenButton(); + }, + + updateFullscreenButton() { + if ( + document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement + ) { + document.getElementById('noVNC_fullscreen_button').classList.add('noVNC_selected'); + } else { + document.getElementById('noVNC_fullscreen_button').classList.remove('noVNC_selected'); + } + }, + + /* ------^------- + * /FULLSCREEN + * ============== + * RESIZE + * ------v------*/ + + // Apply remote resizing or local scaling + applyResizeMode() { + if (!UI.rfb) return; + + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + }, + + /* ------^------- + * /RESIZE + * ============== + * VIEW CLIPPING + * ------v------*/ + + // Update viewport clipping property for the connection. The normal + // case is to get the value from the setting. There are special cases + // for when the viewport is scaled or when a touch device is used. + updateViewClip() { + if (!UI.rfb) return; + + const scaling = UI.getSetting('resize') === 'scale'; + + // Some platforms have overlay scrollbars that are difficult + // to use in our case, which means we have to force panning + // FIXME: Working scrollbars can still be annoying to use with + // touch, so we should ideally be able to have both + // panning and scrollbars at the same time + + let brokenScrollbars = false; + + if (!hasScrollbarGutter) { + if (isIOS() || isAndroid() || isMac() || isChromeOS()) { + brokenScrollbars = true; + } + } + + if (scaling) { + // Can't be clipping if viewport is scaled to fit + UI.forceSetting('view_clip', false); + UI.rfb.clipViewport = false; + } else if (brokenScrollbars) { + UI.forceSetting('view_clip', true); + UI.rfb.clipViewport = true; + } else { + UI.enableSetting('view_clip'); + UI.rfb.clipViewport = UI.getSetting('view_clip'); + } + + // Changing the viewport may change the state of + // the dragging button + UI.updateViewDrag(); + }, + + /* ------^------- + * /VIEW CLIPPING + * ============== + * VIEWDRAG + * ------v------*/ + + toggleViewDrag() { + if (!UI.rfb) return; + + UI.rfb.dragViewport = !UI.rfb.dragViewport; + UI.updateViewDrag(); + }, + + updateViewDrag() { + if (!UI.connected) return; + + const viewDragButton = document.getElementById('noVNC_view_drag_button'); + + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && UI.rfb.dragViewport) { + // We are no longer clipping the viewport. Make sure + // viewport drag isn't active when it can't be used. + UI.rfb.dragViewport = false; + } + + if (UI.rfb.dragViewport) { + viewDragButton.classList.add('noVNC_selected'); + } else { + viewDragButton.classList.remove('noVNC_selected'); + } + + if (UI.rfb.clipViewport) { + viewDragButton.classList.remove('noVNC_hidden'); + } else { + viewDragButton.classList.add('noVNC_hidden'); + } + + viewDragButton.disabled = !UI.rfb.clippingViewport; + }, + + /* ------^------- + * /VIEWDRAG + * ============== + * QUALITY + * ------v------*/ + + updateQuality() { + if (!UI.rfb) return; + + UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); + }, + + /* ------^------- + * /QUALITY + * ============== + * COMPRESSION + * ------v------*/ + + updateCompression() { + if (!UI.rfb) return; + + UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); + }, + + /* ------^------- + * /COMPRESSION + * ============== + * KEYBOARD + * ------v------*/ + + showVirtualKeyboard() { + if (!isTouchDevice) return; + + const input = document.getElementById('noVNC_keyboardinput'); + + if (document.activeElement == input) return; + + input.focus(); + + try { + const l = input.value.length; + // Move the caret to the end + input.setSelectionRange(l, l); + } catch (err) { + // setSelectionRange is undefined in Google Chrome + } + }, + + hideVirtualKeyboard() { + if (!isTouchDevice) return; + + const input = document.getElementById('noVNC_keyboardinput'); + + if (document.activeElement != input) return; + + input.blur(); + }, + + toggleVirtualKeyboard() { + if (document.getElementById('noVNC_keyboard_button').classList.contains('noVNC_selected')) { + UI.hideVirtualKeyboard(); + } else { + UI.showVirtualKeyboard(); + } + }, + + onfocusVirtualKeyboard(event) { + document.getElementById('noVNC_keyboard_button').classList.add('noVNC_selected'); + if (UI.rfb) { + UI.rfb.focusOnClick = false; + } + }, + + onblurVirtualKeyboard(event) { + document.getElementById('noVNC_keyboard_button').classList.remove('noVNC_selected'); + if (UI.rfb) { + UI.rfb.focusOnClick = true; + } + }, + + keepVirtualKeyboard(event) { + const input = document.getElementById('noVNC_keyboardinput'); + + // Only prevent focus change if the virtual keyboard is active + if (document.activeElement != input) { + return; + } + + // Only allow focus to move to other elements that need + // focus to function properly + if (event.target.form !== undefined) { + switch (event.target.type) { + case 'text': + case 'email': + case 'search': + case 'password': + case 'tel': + case 'url': + case 'textarea': + case 'select-one': + case 'select-multiple': + return; + } + } + + 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 + * ============== + * EXTRA KEYS + * ------v------*/ + + openExtraKeys() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_modifiers').classList.add('noVNC_open'); + document.getElementById('noVNC_toggle_extra_keys_button').classList.add('noVNC_selected'); + }, + + closeExtraKeys() { + document.getElementById('noVNC_modifiers').classList.remove('noVNC_open'); + document.getElementById('noVNC_toggle_extra_keys_button').classList.remove('noVNC_selected'); + }, + + toggleExtraKeys() { + if (document.getElementById('noVNC_modifiers').classList.contains('noVNC_open')) { + UI.closeExtraKeys(); + } else { + UI.openExtraKeys(); + } + }, + + sendEsc() { + UI.sendKey(KeyTable.XK_Escape, 'Escape'); + }, + + sendTab() { + UI.sendKey(KeyTable.XK_Tab, 'Tab'); + }, + + toggleCtrl() { + const btn = document.getElementById('noVNC_toggle_ctrl_button'); + if (btn.classList.contains('noVNC_selected')) { + 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.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.sendKey(KeyTable.XK_Alt_L, 'AltLeft', false); + btn.classList.remove('noVNC_selected'); + } else { + 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(); + }, + + /* ------^------- + * /EXTRA KEYS + * ============== + * MISC + * ------v------*/ + + updateViewOnly() { + if (!UI.rfb) return; + UI.rfb.viewOnly = UI.getSetting('view_only'); + + // 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'); + } 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'); + } + }, + + updateShowDotCursor() { + if (!UI.rfb) return; + UI.rfb.showDotCursor = UI.getSetting('show_dot'); + }, + + updateLogging() { + WebUtil.initLogging(UI.getSetting('logging')); + }, + + updateDesktopName(e) { + UI.desktopName = e.detail.name; + // Display the desktop name in the document title + document.title = e.detail.name + ' - ' + PAGE_TITLE; + }, + + bell(e) { + if (WebUtil.getConfigVar('bell', 'on') === 'on') { + const promise = document.getElementById('noVNC_bell').play(); + // The standards disagree on the return value here + if (promise) { + promise.catch((e) => { + if (e.name === 'NotAllowedError') { + // Ignore when the browser doesn't let us play audio. + // It is common that the browsers require audio to be + // initiated from a user action. + } else { + Log.Error('Unable to play bell: ' + e); + } + }); + } + } + }, + + //Helper to add options to dropdown. + addOption(selectbox, text, value) { + const optn = document.createElement('OPTION'); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, + + /* ------^------- + * /MISC + * ============== + */ }; // Set up translations -const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; -l10n.setup(LINGUAS, "app/locale/") - .catch(err => Log.Error("Failed to load translations: " + err)) - .then(UI.prime); +const LINGUAS = [ + 'cs', + 'de', + 'el', + 'es', + 'fr', + 'it', + 'ja', + 'ko', + 'nl', + 'pl', + 'pt_BR', + 'ru', + 'sv', + 'tr', + 'zh_CN', + 'zh_TW', +]; +l10n.setup(LINGUAS, 'app/locale/') + .catch((err) => Log.Error('Failed to load translations: ' + err)) + .then(UI.prime); export default UI;